diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..66f646d --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,204 @@ +env: + browser: false + commonjs: true + es2021: true + node: true +extends: eslint:recommended +parserOptions: + ecmaVersion: latest +rules: { + "no-var": "warn", + "object-shorthand": ["warn", "properties"], + + "accessor-pairs": ["error", { "setWithoutGet": true, "enforceForClassMembers": true }], + "array-bracket-spacing": ["error", "never"], + "array-callback-return": ["error", { + "allowImplicit": false, + "checkForEach": false + }], + "arrow-spacing": ["error", { "before": true, "after": true }], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "camelcase": ["error", { + "allow": ["^UNSAFE_"], + "properties": "never", + "ignoreGlobals": true + }], + "comma-dangle": ["error", { + "arrays": "never", + "objects": "never", + "imports": "never", + "exports": "never", + "functions": "never" + }], + "comma-spacing": ["error", { "before": false, "after": true }], + "comma-style": ["error", "last"], + "computed-property-spacing": ["error", "never", { "enforceForClassMembers": true }], + "constructor-super": "error", + "curly": ["error", "multi-line"], + "default-case-last": "error", + "dot-location": ["error", "property"], + "dot-notation": ["error", { "allowKeywords": true }], + "eol-last": "error", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "func-call-spacing": ["error", "never"], + "generator-star-spacing": ["error", { "before": true, "after": true }], + "indent": ["error", 2, { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "MemberExpression": 1, + "FunctionDeclaration": { "parameters": 1, "body": 1 }, + "FunctionExpression": { "parameters": 1, "body": 1 }, + "CallExpression": { "arguments": 1 }, + "ArrayExpression": 1, + "ObjectExpression": 1, + "ImportDeclaration": 1, + "flatTernaryExpressions": false, + "ignoreComments": false, + "ignoredNodes": ["TemplateLiteral *", "JSXElement", "JSXElement > *", "JSXAttribute", "JSXIdentifier", "JSXNamespacedName", "JSXMemberExpression", "JSXSpreadAttribute", "JSXExpressionContainer", "JSXOpeningElement", "JSXClosingElement", "JSXFragment", "JSXOpeningFragment", "JSXClosingFragment", "JSXText", "JSXEmptyExpression", "JSXSpreadChild"], + "offsetTernaryExpressions": true + }], + "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], + "keyword-spacing": ["error", { "before": true, "after": true }], + "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], + "multiline-ternary": ["error", "always-multiline"], + "new-cap": ["error", { "newIsCap": true, "capIsNew": false, "properties": true }], + "new-parens": "error", + "no-array-constructor": "error", + "no-async-promise-executor": "error", + "no-caller": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-condition": ["error", { "checkLoops": false }], + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-useless-backreference": "error", + "no-empty": ["error", { "allowEmptyCatch": true }], + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-eval": "error", + "no-ex-assign": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": ["error", "functions"], + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-iterator": "error", + "no-labels": ["error", { "allowLoop": false, "allowSwitch": false }], + "no-lone-blocks": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-prototype-builtins": "error", + "no-useless-catch": "error", + "no-mixed-operators": ["error", { + "groups": [ + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"] + ], + "allowSamePrecedence": true + }], + "no-mixed-spaces-and-tabs": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-symbol": "error", + "no-new-wrappers": "error", + "no-obj-calls": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-proto": "error", + "no-redeclare": ["error", { "builtinGlobals": false }], + "no-regex-spaces": "error", + "no-return-assign": ["error", "except-parens"], + "no-self-assign": ["error", { "props": true }], + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-tabs": "error", + "no-template-curly-in-string": "error", + "no-this-before-super": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-unexpected-multiline": "error", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": ["error", { "defaultAssignment": false }], + "no-unreachable": "error", + "no-unreachable-loop": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unused-expressions": ["error", { + "allowShortCircuit": true, + "allowTernary": true, + "allowTaggedTemplates": true + }], + "no-unused-vars": ["error", { + "args": "none", + "caughtErrors": "none", + "ignoreRestSiblings": true, + "vars": "all" + }], + "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-escape": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-void": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "object-curly-newline": ["error", { "multiline": true, "consistent": true }], + "object-curly-spacing": ["error", "always"], + "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], + "one-var": ["error", { "initialized": "never" }], + "operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before", "|>": "before" } }], + "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], + "prefer-const": ["error", {"destructuring": "all"}], + "prefer-promise-reject-errors": "error", + "prefer-regex-literals": ["error", { "disallowRedundantWrapping": true }], + "quote-props": ["error", "as-needed"], + "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": false }], + "rest-spread-spacing": ["error", "never"], + "semi": ["error", "never"], + "semi-spacing": ["error", { "before": false, "after": true }], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", "always"], + "space-in-parens": ["error", "never"], + "space-infix-ops": "error", + "space-unary-ops": ["error", { "words": true, "nonwords": false }], + "spaced-comment": ["error", "always", { + "line": { "markers": ["*package", "!", "/", ",", "="] }, + "block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] } + }], + "symbol-description": "error", + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": ["error", "never"], + "unicode-bom": ["error", "never"], + "use-isnan": ["error", { + "enforceForSwitchCase": true, + "enforceForIndexOf": true + }], +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f13a023 --- /dev/null +++ b/.gitignore @@ -0,0 +1,332 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,intellij +# Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode,intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope + +# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,intellij + +# Created by https://www.toptal.com/developers/gitignore/api/macos,windows +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/README.md b/README.md index 70db4aa..2492d41 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ With this plugin, you can improve testing of your code 💯, reduce time by crea **Disclaimer**: this is a beta-release of the Amplify GraphQL Seed Plugin and may contain unforeseen bugs. This is a 3rd-party plugin and is not associated with the official Amplify project. Use this plugin at your own discretion. Please let us know if you encounter any bugs, or have feedback / suggestions. +![Demo-gif](./demo.gif) + **Contents:** - [Installation 🛠️](#installation) - [Prerequisites](#prerequisites) @@ -48,7 +50,7 @@ We assume that by this stage you already have a GraphQL API configured in your p amplify graphql-seed init ``` ### Step 2. Adjust the generated `seed-data.js` file to your needs -The init file has created the `amplify/backend/seeding/seed-data.js` file. Adjust mutations and data to run your custom seeding. See [this section](#customizing-your-seed-file) of the readme for more details. +The init file has created the `amplify/backend/seeding/seed-data.js` file. Adjust mutations and data to run your custom seeding. See [this section](#customizing-your-seed-file) of the readme for more details. ### Step 3. Run the plugin to seed your database **Option 1:** Start your mock database, and seed it: @@ -175,6 +177,7 @@ Our Amplify GraphQL seeding plugin supports 3 different authentication modes. Ea ## Common errors ⛔ * If you see the "GraphQL error: The conditional request failed" error, it is likely that you're trying to create an item with an existing index to your local or remote database. The plugin will skip these elements automatically. +* If you see an error like "fsPromises.rm is not a function", make sure that your npm version >= 14.14.0 ## How to use this plugin in CI/CD pipelines 🏗️ You can also use this plugin to seed your remote databases as part of your deployment pipelines. For example, if you're using the Amplify pipelines, you can adjust your `amplify.yml` file (in build settings), to include the following: ```yaml @@ -213,4 +216,4 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform ## License -This project is licensed under the Apache-2.0 License. +This project is licensed under the Apache-2.0 License. \ No newline at end of file diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000..3351a43 Binary files /dev/null and b/demo.gif differ diff --git a/utils/cli-input-queries.js b/utils/cli-input-queries.js new file mode 100644 index 0000000..c1e610e --- /dev/null +++ b/utils/cli-input-queries.js @@ -0,0 +1,42 @@ +const inquirer = require('inquirer') + +export async function askCLIConfirmation (message) { + const answer = await inquirer.prompt([ + { + name: 'providedAnswer', + type: 'confirm', + message, + default: true + } + ]) + return answer.providedAnswer.toString() +} + +export async function askCLIOptions (message, options) { + // E.g. expect options as { name: api.name, value: api.id, checked (opt): [bool] } + const answer = await inquirer.prompt([ + { + name: 'providedOption', + message, + type: 'checkbox', + choices: options + } + ]) + return answer.providedOption +} + +export const getCredentialsFromCLI = async () => { + const question = [ + { + name: 'username', + message: 'Username', + type: 'input' + }, + { + name: 'password', + message: 'Password', + type: 'password' + } + ] + return await inquirer.prompt(question) +} diff --git a/utils/constants.js b/utils/constants.js new file mode 100644 index 0000000..881e8fb --- /dev/null +++ b/utils/constants.js @@ -0,0 +1,127 @@ +const SEED_FILE_NAME = 'seed-data.js' +const SEED_BACKEND_FOLDER = 'seeding' + +const SAMPLE_MUTATIONS_FILE_NAME = 'example-mutation-file.js' + +const SAMPLE_SEED_FILE_NAME = 'example-seed-file.js' +const SAMPLE_DIRECTORY = 'sample-files' + +const DEFAULT_MUTATION_FILENAME = 'customMutations.js' +const CONFIGURATIONS_FILENAME = 'configuration.json' + +const SAMPLE_POST_MOCK_FILENAME = 'example-post-mock-file.sh' +const POST_MOCK_FILENAME = 'post-mock.sh' + +const SAMPLE_PRE_MOCK_FILENAME = 'example-pre-mock-file.sh' +const PRE_MOCK_FILENAME = 'pre-mock.sh' + +const SAMPLE_POST_PUSH_FILENAME = 'example-post-push-file.sh' +const POST_PUSH_FILENAME = 'post-push.sh' + +const REMOTE_SEED_ARGUMENT = 'remote' +const LOCAL_SEED_ARGUMENT = 'local' + +const MOCKCREDENTIALS = { + accessKeyId: 'ASIAVJKIAM-AuthRole', + secretAccessKey: 'fake' +} + +const OVERWRITE_FILES_ARGUMENT = 'overwrite-files' + +const REMOTE_ENVIRONMENT = 'remote' +const LOCAL_ENVIRONMENT = 'local' + +const ENVIROMENT_HOOK_FILES = { + [LOCAL_ENVIRONMENT]: [ + { + source: SAMPLE_POST_MOCK_FILENAME, + dest: POST_MOCK_FILENAME + }, + { + source: SAMPLE_PRE_MOCK_FILENAME, + dest: PRE_MOCK_FILENAME + } + ], + [REMOTE_ENVIRONMENT]: [ + { + source: SAMPLE_POST_PUSH_FILENAME, + dest: POST_PUSH_FILENAME + } + ] +} + +const COGNITO_AUTHENTICATION = 'AMAZON_COGNITO_USER_POOLS' +const AWS_IAM_AUTHENTICATION = 'AWS_IAM' +const API_KEY_AUTHENTICATION = 'API_KEY' + +const EXAMPLE_CREDENTIALS_FILENAME = 'example-credentials.json' +const CREDENTIALS_FILENAME = 'credentials.json' + +const EXAMPLE_GITIGNORE_FILENAME = 'example-gitignore' +const GITIGNORE_FILENAME = '.gitignore' + +// THE INIT HELP should match the validation for schema in INITARGSCHEMA +const RUNHELP = [ + { + name: 'Run', + description: 'Runs the seeding script using the data from seed-data.js file and using the mutations from mutations.js file.' + }, + { + name: '\n Available options: \n', + description: '' + }, + { + name: '--remote', + description: 'Seeds your remote database instead of the local one.' + }, + { + name: '--username ', + description: '\tPass the username to authenticate with Cognito User Pools. Used in conjunction with --password argument.' + }, + { + name: '--password ', + description: '\tPass the password in the CLI to authenticate with Cognito User Pools. Used in conjunction with --username argument.' + } +] + +const RUNARGUMENTSCHEMA = { + type: 'object', + properties: { + remote: { type: 'boolean' }, + username: { type: 'string' }, + password: { type: 'string' }, + yes: { type: 'boolean' } + }, + required: [], + additionalProperties: false +} + +export { + SEED_FILE_NAME, + SEED_BACKEND_FOLDER, + SAMPLE_SEED_FILE_NAME, + SAMPLE_DIRECTORY, + SAMPLE_MUTATIONS_FILE_NAME, + DEFAULT_MUTATION_FILENAME, + CONFIGURATIONS_FILENAME, + SAMPLE_POST_MOCK_FILENAME, + POST_MOCK_FILENAME, + SAMPLE_PRE_MOCK_FILENAME, + PRE_MOCK_FILENAME, + REMOTE_SEED_ARGUMENT, + OVERWRITE_FILES_ARGUMENT, + REMOTE_ENVIRONMENT, + LOCAL_ENVIRONMENT, + ENVIROMENT_HOOK_FILES, + LOCAL_SEED_ARGUMENT, + COGNITO_AUTHENTICATION, + AWS_IAM_AUTHENTICATION, + API_KEY_AUTHENTICATION, + MOCKCREDENTIALS, + EXAMPLE_CREDENTIALS_FILENAME, + CREDENTIALS_FILENAME, + EXAMPLE_GITIGNORE_FILENAME, + GITIGNORE_FILENAME, + RUNHELP, + RUNARGUMENTSCHEMA +} diff --git a/utils/directory-functions.js b/utils/directory-functions.js new file mode 100644 index 0000000..5234006 --- /dev/null +++ b/utils/directory-functions.js @@ -0,0 +1,111 @@ +const path = require('path') +const fs = require('fs') +const crypto = require('crypto') +const fg = require('fast-glob') +const fsPromises = fs.promises +const constants = require('../utils/constants') + +const { + SEED_BACKEND_FOLDER, + SAMPLE_DIRECTORY +} = require('./constants') + +const getFileHash = async (path) => { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha1') + const stream = fs.createReadStream(path) + stream.on('error', err => reject(err)) + stream.on('data', chunk => hash.update(chunk)) + stream.on('end', () => resolve(hash.digest('hex'))) + }) +} + +export const checkFilesAreTheSame = async (file1, file2) => { + return await getFileHash(file1) === await getFileHash(file2) +} + +export const getProjectRoot = (context) => { + return path.normalize(path.join(context.amplify.pathManager.searchProjectRootPath())) +} + +export const getMockDirectory = (context) => { + const backendDir = getBackendDirectory(context) + return path.normalize(path.join(backendDir, '../mock-data/dynamodb')) +} + +export const getHooksDirectory = (context) => { + const backendDir = getBackendDirectory(context) + return path.normalize(path.join(backendDir, '../hooks')) +} + +export const getHooksFileLocation = (context, fileName) => { + const getHooksFileLocation = getHooksDirectory(context) + return path.normalize(path.join(getHooksFileLocation, fileName)) +} + +export const getBackendDirectory = (context) => { + return context.amplify.pathManager.getBackendDirPath() +} + +export const getSeedingDirectory = (context) => { + const backendDir = getBackendDirectory(context) + return path.normalize(path.join(backendDir, SEED_BACKEND_FOLDER)) +} + +export const getSampleDirectory = () => { + return path.normalize(path.join(__dirname, SAMPLE_DIRECTORY)) +} + +export const getSampleFileLocation = (context, fileName) => { + const sampleDir = getSampleDirectory(context) + return path.normalize(path.join(sampleDir, fileName)) +} + +export const getSeedingFileLocation = (context, fileName) => { + const seedDir = getSeedingDirectory(context) + return path.normalize(path.join(seedDir, fileName)) +} + +export const getSrcFolder = (context) => { + const projectRoot = getProjectRoot(context) + return path.normalize(path.join(projectRoot, 'src')) +} + +export const getAwsExportsFile = async (context) => { + const srcFolder = path.normalize(path.join(context.amplify.pathManager.searchProjectRootPath(), 'src/')) + const awsExportsFiles = await fg([`${srcFolder}/**/aws-exports.*`]) + if (awsExportsFiles.length === 0) { + context.print.error('No aws-exports file found.') + process.exit() + } + const awsExportsFile = awsExportsFiles[0] + return await import(awsExportsFile) +} + +export const getCredentialsFileData = async (context) => { + const seedDir = await getSeedingDirectory(context) + try { + const data = await fsPromises.readFile(`${seedDir}/${constants.CREDENTIALS_FILENAME}`) + return JSON.parse(data) + } catch (error) { + if (error.code === 'ENOENT') { + context.print.info("The credentials.json file does not exist. That's ok, we'll get credentials from CLI prompt or command arguments.") + } else { + context.print.error('Error parsing the credentials.json') + context.print.error(error) + throw error + } + } +} + +export const getConfigurationData = async (context) => { + const seedDir = await getSeedingDirectory(context) + try { + const data = await fsPromises.readFile(`${seedDir}/${constants.CONFIGURATIONS_FILENAME}`) + return JSON.parse(data) + } catch (error) { + context.print.error("Error fetching configuration file. Make sure to run the 'amplify graphql-seed init' command first") + context.print.error(error) + throw error + } +} diff --git a/utils/help.js b/utils/help.js new file mode 100644 index 0000000..75ea8b9 --- /dev/null +++ b/utils/help.js @@ -0,0 +1,34 @@ + +const Table = require('cli-table') + +export function showHelp (input, context) { + // Take help context in the format: + + const table = new Table({ + chars: { + top: '', + 'top-mid': '', + 'top-left': '', + 'top-right': '', + bottom: '', + 'bottom-mid': '', + 'bottom-left': '', + 'bottom-right': '', + left: '', + 'left-mid': '', + mid: '', + 'mid-mid': '', + right: '', + 'right-mid': '', + middle: ' ' + }, + style: { 'padding-left': 0, 'padding-right': 0 } + }) + + for (const item of input) { + table.push([item.name, item.description, '']) + } + + context.print.info(table.toString()) + return table.toString() +} diff --git a/utils/sample-files/example-credentials.json b/utils/sample-files/example-credentials.json new file mode 100644 index 0000000..b20c53c --- /dev/null +++ b/utils/sample-files/example-credentials.json @@ -0,0 +1,4 @@ +{ + "username": "", + "password": "" +} diff --git a/utils/sample-files/example-gitignore b/utils/sample-files/example-gitignore new file mode 100644 index 0000000..83f6e39 --- /dev/null +++ b/utils/sample-files/example-gitignore @@ -0,0 +1 @@ +credentials.json diff --git a/utils/sample-files/example-mutation-file.js b/utils/sample-files/example-mutation-file.js new file mode 100644 index 0000000..427cb62 --- /dev/null +++ b/utils/sample-files/example-mutation-file.js @@ -0,0 +1,17 @@ +// In this file, you can add custom mutations for your seeding script. +// If you've added codegen to your Amplify project, the seed file in this directory will try to import the auto-generated mutations automatically. +// In that case, this file can be used to add additional custom mutations to complement the mutations from codegen. +// Typically they would reside in your "src/graphql" folder + +export const createTodo = /* GraphQL */ ` + mutation createTodo( + $input: CreateTodoInput! + $condition: ModelTodoConditionInput + ) { + createTodo(input: $input, condition: $condition) { + id + name + description + } + } +` diff --git a/utils/sample-files/example-post-mock-file.sh b/utils/sample-files/example-post-mock-file.sh new file mode 100644 index 0000000..ecb94e1 --- /dev/null +++ b/utils/sample-files/example-post-mock-file.sh @@ -0,0 +1,27 @@ +#!/bin/bash +parameters=`cat` +data=$(jq -r '.data' <<< "$parameters") + +if ! command -v jq &> /dev/null +then + echo "Install or add jq CLI binary into your path to use the hooks." + exit 1 +fi + +check_if_argument_exists() { + ARG=$1 + echo $(jq -r '.amplify.argv' <<< "$data" | jq -e 'any(.[]; . == "--'$ARG'" or . == "-'$ARG'" or . == "--'$ARG'=true")') +} + +refresh=$(check_if_argument_exists "refresh") +seed=$(check_if_argument_exists "seed") + +if [ "$seed" = true ] || [ "$refresh" = true ] +then + echo "INFO: Seeding database" + amplify graphql-seed run + exit 0 +else + echo "INFO: Not seeding database" + exit 0 +fi diff --git a/utils/sample-files/example-post-push-file.sh b/utils/sample-files/example-post-push-file.sh new file mode 100644 index 0000000..3655740 --- /dev/null +++ b/utils/sample-files/example-post-push-file.sh @@ -0,0 +1,26 @@ +#!/bin/bash +parameters=`cat` +data=$(jq -r '.data' <<< "$parameters") + +if ! command -v jq &> /dev/null +then + echo "Install or add jq CLI binary into your path to use the hooks." + exit 1 +fi + +check_if_argument_exists() { + ARG=$1 + echo $(jq -r '.amplify.argv' <<< "$data" | jq -e 'any(.[]; . == "--'$ARG'" or . == "-'$ARG'" or . == "--'$ARG'=true")') +} + +seed=$(check_if_argument_exists "seed") + +if [ "$seed" = true ] +then + echo "INFO: Seeding database" + amplify graphql-seed run --remote + exit 0 +else + echo "INFO: Not seeding database" + exit 0 +fi diff --git a/utils/sample-files/example-pre-mock-file.sh b/utils/sample-files/example-pre-mock-file.sh new file mode 100644 index 0000000..b2dd313 --- /dev/null +++ b/utils/sample-files/example-pre-mock-file.sh @@ -0,0 +1,27 @@ +#!/bin/bash +parameters=`cat` +data=$(jq -r '.data' <<< "$parameters") + +if ! command -v jq &> /dev/null +then + echo "Install or add jq CLI binary into your path to use the hooks." + exit 1 +fi + +check_if_argument_exists() { + ARG=$1 + echo $(jq -r '.amplify.argv' <<< "$data" | jq -e 'any(.[]; . == "--'$ARG'" or . == "-'$ARG'" or . == "--'$ARG'=true")') +} + +refresh=$(check_if_argument_exists "refresh") +delete=$(check_if_argument_exists "delete") + +if [ "$refresh" = true ] || [ "$delete" = true ] +then + echo "INFO: Deleting mock database" + amplify graphql-seed delete-mock + exit 0 +else + echo "INFO: No pre-mock action required" + exit 0 +fi diff --git a/utils/sample-files/example-seed-file.js b/utils/sample-files/example-seed-file.js new file mode 100644 index 0000000..26045ab --- /dev/null +++ b/utils/sample-files/example-seed-file.js @@ -0,0 +1,38 @@ +$IMPORTS + +export const createTodo = { + mutation: mutations.createTodo, + // override_auth: "API_KEY", // One of ["AWS_IAM", "API_KEY", "AMAZON_COGNITO_USER_POOLS"] + data: [ + { id: 1, name: 'some', description: 'Lorem ipsum stuff' }, + { id: 2, name: 'nothing', description: 'Lorem ipsum stuff' } + ] +} + +/* + // TO USE, uncomment by removing '/*' and '\*' + // + // ANOTHER SEED-DATA EXAMPLE: + // CREATE A 100 TODOs, using the faker library. + // Note: install faker by adding it to the package.json of your root-project or using npm install (https://www.npmjs.com/package/faker/v/5.5.3) + // Note 2: the latest faker library has been emptied, and is no longer maintained. Use at your own discretion + // + // Example items: + // { + // "id": "31", + // "name": "omnis-ut-neque", + // "description": "Aliquid eum dolorem eos quisquam iusto ratione eos." + // }, + // { + // "id": "32", + // "name": "eveniet-culpa-eius", + // "description": "Impedit sit animi." + // }, + + const faker = require('faker'); + + export const createTodo = { + mutation: mutations.createTodo, + data: Array.apply(null, {length: 100}).map((_, index) => ({ id: index, name: faker.lorem.slug(), description: faker.lorem.sentence() })) + } +*/ diff --git a/utils/validation.js b/utils/validation.js new file mode 100644 index 0000000..0a71e00 --- /dev/null +++ b/utils/validation.js @@ -0,0 +1,55 @@ + +const Ajv = require('ajv') + +const { + RUNHELP, + RUNARGUMENTSCHEMA +} = require('./constants') + +const { showHelp } = require('../utils/help') + +export const validateGraphqlEndpoint = (graphqlEndpoint, context) => { + if (!graphqlEndpoint) { + context.print.error('Could not find GraphQL endpoint in aws-exports.js') + process.exit() + } + + if (graphqlEndpoint.startsWith('https://') && context.parameters.options.remote === undefined) { + context.print.error(`${graphqlEndpoint} seems like a remote endpoint - cannot continue. Please run the 'amplify graphql-seed run --remote' command directly`) + process.exit() + } +} + +export const validateInputArguments = (instance, context) => { + const ajv = new Ajv() + + const validate = ajv.compile(RUNARGUMENTSCHEMA) + const valid = validate(instance) + if (!valid) { + context.print.info('Invalid input parameters. Please take a look below: \n') + showHelp(RUNHELP, context) + process.exit() + } + return valid +} + +export const validateCredentialsSchema = (instance, context) => { + const ajv = new Ajv() + const schema = { + type: 'object', + properties: { + username: { type: 'string' }, + password: { type: 'string' } + }, + required: ['username', 'password'], + additionalProperties: false + } + + const validate = ajv.compile(schema) + const valid = validate(instance) + if (!valid) { + context.print.warning('Invalid credentials.json file. It has to be in the format {username: xx, password: xx}') + process.exit() + } + return valid +}