From 18f04d1fc0a1e3c630160d1d21f20f7c61e3c108 Mon Sep 17 00:00:00 2001 From: SwaySway Date: Thu, 10 Oct 2019 11:56:51 -0700 Subject: [PATCH] style: run prettier through code --- commitlint.config.js | 2 +- cypress/integration/api_spec.js | 37 +- cypress/integration/auth_spec.js | 20 +- cypress/plugins/index.js | 2 +- cypress/support/index.js | 2 +- .../src/__tests__/velocity/util/time.test.ts | 64 +- .../velocity/value-mapper/map.test.ts | 5 +- .../velocity/value-mapper/string.test.ts | 6 +- .../velocity/value-mapper/to-json.test.ts | 2 +- .../src/data-loader/dynamo-db/index.ts | 44 +- .../src/data-loader/dynamo-db/utils/index.ts | 2 +- .../amplify-appsync-simulator/src/index.ts | 18 +- .../src/mqtt-server/client.ts | 7 +- .../src/mqtt-server/util.ts | 5 +- .../src/resolvers/function.ts | 34 +- .../src/resolvers/pipeline-resolver.ts | 28 +- .../src/resolvers/unit-resolver.ts | 22 +- .../src/schema/appsync-scalars/index.ts | 15 +- .../src/schema/directives/auth.ts | 43 +- .../src/schema/directives/aws-subscribe.ts | 3 +- .../src/schema/directives/index.ts | 2 +- .../src/schema/index.ts | 9 +- .../src/server/subscription.ts | 15 +- .../src/type-definition.ts | 10 +- .../src/velocity/index.ts | 20 +- .../src/velocity/util/dynamodb-utils.ts | 2 +- .../src/velocity/util/errors.ts | 22 +- .../src/velocity/util/map-utils.ts | 40 +- .../src/velocity/util/time.ts | 18 +- .../util/transform/dynamodb-filter.ts | 36 +- .../src/velocity/value-mapper/array.ts | 3 +- .../src/velocity/value-mapper/map.ts | 11 +- .../src/velocity/value-mapper/mapper.ts | 6 +- .../captcha-create-challenge.js | 2 +- .../triggers/CustomMessage/assets/spinner.js | 232 +- .../triggers/CustomMessage/assets/verify.js | 24 +- .../__mocks__/mockAwsProviderModule.js | 35 +- .../amplify-category-hosting/__mocks__/ora.js | 12 +- .../__tests__/index.test.js | 352 +- .../configuration-manager.test.js | 156 +- .../helpers/cloudfront-manager.test.js | 159 +- .../helpers/configure-CloudFront.test.js | 526 +- .../helpers/configure-Publish.test.js | 170 +- .../helpers/configure-Website.test.js | 54 +- .../helpers/file-scanner.test.js | 304 +- .../helpers/file-uploader.test.js | 162 +- .../helpers/validate-bucket-name.test.js | 100 +- .../helpers/validate-website-doc-name.test.js | 38 +- .../lib/S3AndCloudFront/s3Index.test.js | 256 +- .../__tests__/lib/category-manager.test.js | 225 +- .../function-template-dir/cfn-response.js | 6 +- .../__tests__/lib/apns-cert-config.test.js | 41 +- .../__tests__/lib/apns-key-config.test.js | 49 +- .../__tests__/lib/channel-APNS.test.js | 227 +- .../awscloudformation/triggers/s3/index.js | 3 +- .../__tests__/commands/add.test.js | 6 +- .../__tests__/commands/push.test.js | 8 +- .../__tests__/commands/remove.test.js | 6 +- .../__tests__/commands/update.test.js | 6 +- .../__tests__/lib/xr-manager.test.js | 76 +- packages/amplify-cli/__mocks__/fs-extra.js | 1 - packages/amplify-cli/__mocks__/fs.js | 2 - packages/amplify-cli/__mocks__/inquirer.js | 1 - .../src/__test__/context-manager.test.ts | 2 +- packages/amplify-cli/src/commands/plugin.ts | 6 +- .../amplify-cli/src/commands/plugin/add.ts | 60 +- .../src/commands/plugin/configure.ts | 78 +- .../amplify-cli/src/commands/plugin/help.ts | 2 +- .../amplify-cli/src/commands/plugin/list.ts | 12 +- .../amplify-cli/src/commands/plugin/new.ts | 13 +- .../amplify-cli/src/commands/plugin/remove.ts | 12 +- .../amplify-cli/src/commands/plugin/scan.ts | 2 +- .../amplify-cli/src/commands/plugin/verify.ts | 4 +- packages/amplify-cli/src/commands/version.ts | 2 +- .../amplify-cli/src/context-extensions.ts | 147 +- packages/amplify-cli/src/context-manager.ts | 5 +- .../src/domain/add-plugin-result.ts | 12 +- .../amplify-cli/src/domain/amplify-event.ts | 30 +- .../src/domain/amplify-plugin-type.ts | 8 +- .../amplify-cli/src/domain/amplify-toolkit.ts | 239 +- packages/amplify-cli/src/domain/constants.ts | 2 +- packages/amplify-cli/src/domain/context.ts | 2 +- .../src/domain/input-verification-result.ts | 4 +- packages/amplify-cli/src/domain/input.ts | 6 +- .../amplify-cli/src/domain/inquirer-helper.ts | 8 +- .../src/domain/plugin-collection.ts | 4 +- .../amplify-cli/src/domain/plugin-info.ts | 9 +- .../amplify-cli/src/domain/plugin-manifest.ts | 8 +- .../amplify-cli/src/domain/plugin-platform.ts | 12 +- .../src/domain/plugin-verification-result.ts | 16 +- packages/amplify-cli/src/execution-manager.ts | 89 +- packages/amplify-cli/src/index.ts | 11 +- packages/amplify-cli/src/input-manager.ts | 13 +- .../src/plugin-helpers/access-plugins-file.ts | 7 +- .../src/plugin-helpers/compare-plugins.ts | 5 +- .../src/plugin-helpers/create-new-plugin.ts | 73 +- .../plugin-helpers/display-plugin-platform.ts | 47 +- .../plugin-helpers/scan-plugin-platform.ts | 84 +- .../src/plugin-helpers/verify-plugin.ts | 116 +- packages/amplify-cli/src/plugin-manager.ts | 146 +- .../amplify-cli/src/utils/global-prefix.ts | 2 +- .../amplify-cli/src/utils/is-child-path.ts | 2 +- .../amplify-cli/src/utils/readJsonFile.ts | 4 +- .../event-handlers/handle-PostInit.js | 2 +- .../event-handlers/handle-PostPush.js | 2 +- .../event-handlers/handle-PreInit.js | 2 +- .../event-handlers/handle-PrePush.js | 2 +- .../plugin-template-frontend/index.js | 18 +- .../event-handlers/handle-PostInit.js | 2 +- .../event-handlers/handle-PostPush.js | 2 +- .../event-handlers/handle-PreInit.js | 2 +- .../event-handlers/handle-PrePush.js | 2 +- .../plugin-template-provider/index.js | 18 +- .../event-handlers/handle-PostInit.js | 2 +- .../event-handlers/handle-PostPush.js | 2 +- .../event-handlers/handle-PreInit.js | 2 +- .../event-handlers/handle-PrePush.js | 2 +- .../amplify-helpers/input-validation.test.js | 1 - .../amplify-helpers/trigger-flow.test.js | 169 +- .../amplify-codegen/commands/codegen/add.js | 4 +- .../commands/codegen/codegen.js | 4 +- .../commands/codegen/configure.js | 2 +- .../commands/codegen/remove.js | 2 +- .../commands/codegen/statements.js | 2 +- .../amplify-codegen/commands/codegen/types.js | 2 +- .../__test__/index.test.js | 146 +- packages/amplify-dynamodb-simulator/index.js | 344 +- .../amplify-e2e-tests/__tests__/api.test.ts | 25 +- .../amplify-e2e-tests/__tests__/auth.test.ts | 38 +- .../__tests__/function.test.ts | 4 +- .../amplify-e2e-tests/__tests__/init.test.ts | 14 +- .../__tests__/interactions.test.ts | 6 +- .../api.connection.migration.test.ts | 22 +- .../migration/api.key.migration.test.ts | 26 +- .../__tests__/predictions.test.ts | 81 +- .../__tests__/storage.test.ts | 23 +- .../src/aws-matchers/iamMatcher.ts | 6 +- .../src/aws-matchers/index.ts | 2 +- .../src/aws-matchers/s3matcher.ts | 6 +- .../amplify-e2e-tests/src/categories/api.ts | 34 +- .../amplify-e2e-tests/src/categories/auth.ts | 119 +- .../src/categories/function.ts | 22 +- .../src/categories/interactions.ts | 15 +- .../src/categories/predictions.ts | 202 +- .../src/categories/storage.ts | 48 +- .../amplify-e2e-tests/src/configure/index.ts | 17 +- .../amplify-e2e-tests/src/configure_tests.ts | 6 +- .../amplify-e2e-tests/src/init/amplifyPush.ts | 16 +- .../src/init/deleteProject.ts | 10 +- packages/amplify-e2e-tests/src/init/index.ts | 2 +- .../src/init/initProjectHelper.ts | 30 +- packages/amplify-e2e-tests/src/utils/api.ts | 10 +- .../amplify-e2e-tests/src/utils/awsExports.ts | 2 +- packages/amplify-e2e-tests/src/utils/index.ts | 14 +- .../src/utils/nexpect-modified/index.d.ts | 31 +- .../src/utils/nexpect-modified/lib/nexpect.js | 130 +- .../nexpect-modified/test/nexpect-test.js | 212 +- .../src/utils/projectMeta.ts | 2 +- .../amplify-e2e-tests/src/utils/sdk-calls.ts | 33 +- .../src/serviceWorker.ts | 44 +- .../src/utils/jwt.ts | 16 +- .../__integration__/e2e.test.ts | 14 +- .../__tests__/generator/generate.test.ts | 77 +- .../generator/generateAllOperations.test.ts | 85 +- .../generator/generateOperation.test.ts | 36 +- .../__tests__/generator/getArgs.test.ts | 56 +- .../__tests__/generator/getBody.test.ts | 47 +- .../__tests__/generator/getFields.test.ts | 149 +- .../__tests__/generator/getFragments.test.ts | 102 +- .../__tests__/generator/utils/getType.test.ts | 30 +- .../generator/utils/isRequired.test.ts | 18 +- .../amplify-graphql-docs-generator/src/cli.ts | 33 +- .../src/generator/generate.ts | 28 +- .../src/generator/generateAllOperations.ts | 82 +- .../src/generator/generateOperation.ts | 14 +- .../src/generator/getBody.ts | 14 +- .../src/generator/getFragment.ts | 16 +- .../src/generator/index.ts | 10 +- .../src/generator/types.ts | 74 +- .../src/generator/utils/getType.ts | 4 +- .../src/generator/utils/isList.ts | 2 +- .../src/generator/utils/isRequiredList.ts | 4 +- .../src/generator/utils/isS3Object.ts | 10 +- .../src/generator/utils/loading.ts | 43 +- .../src/logger.ts | 4 +- .../fixtures/angular.ts | 58 +- .../fixtures/output.ts | 58 +- .../fixtures/starwars.service.ts | 65 +- .../fixtures/test.ts | 3990 +++---- .../fixtures/todo.service.ts | 54 +- .../src/angular/index.ts | 34 +- .../src/cli.js | 72 +- .../src/compiler/index.ts | 46 +- .../src/compiler/legacyIR.ts | 30 +- .../visitors/collectAndMergeFields.ts | 31 +- .../compiler/visitors/generateOperationId.ts | 2 +- .../visitors/inlineRedundantTypeConditions.ts | 2 +- .../src/compiler/visitors/typeCase.ts | 51 +- .../src/errors.ts | 7 +- .../flow-modern/__tests__/codeGeneration.ts | 23 +- .../src/flow-modern/__tests__/helpers.ts | 361 +- .../src/flow-modern/codeGeneration.ts | 183 +- .../src/flow-modern/helpers.ts | 40 +- .../src/flow-modern/language.ts | 109 +- .../src/flow-modern/printer.ts | 63 +- .../flow-modern/types/augment-babel-types.ts | 4 +- .../src/flow/codeGeneration.js | 308 +- .../src/flow/language.js | 78 +- .../src/flow/types.js | 18 +- .../src/generate.ts | 49 +- .../src/loading.ts | 67 +- .../src/scala/codeGeneration.js | 359 +- .../src/scala/language.js | 41 +- .../src/scala/naming.js | 35 +- .../src/scala/types.js | 17 +- .../src/scala/values.js | 31 +- .../src/serializeToJSON.ts | 65 +- .../src/swift/aws-scalar-helper.ts | 28 +- .../src/swift/codeGeneration.ts | 259 +- .../src/swift/helpers.ts | 40 +- .../src/swift/language.ts | 4 +- .../src/typescript/codeGeneration.ts | 348 +- .../src/typescript/language.ts | 133 +- .../src/typescript/types.ts | 20 +- .../src/utilities/CodeGenerator.ts | 3 +- .../src/utilities/complextypes.ts | 5 +- .../src/utilities/graphql.ts | 48 +- .../src/utilities/printing.ts | 8 +- .../src/validation.ts | 17 +- .../test/angular/index.js | 26 +- .../test/compiler/legacyIR.js | 232 +- .../compiler/visitors/conditionalFields.ts | 44 +- .../test/compiler/visitors/typeCase.ts | 89 +- .../test/fixtures/misc/invalid-gqlQueries.js | 4 +- .../test/fixtures/starwars/gqlQueries.js | 12 +- .../test/flow/codeGeneration.js | 28 +- .../test/jsonOutput.ts | 2 +- .../test/loading.ts | 12 +- .../test/scala/codeGeneration.js | 129 +- .../test/scala/language.js | 34 +- .../test/scala/types.js | 24 +- .../test/swift/language.ts | 10 +- .../test/swift/typeNameFromGraphQLType.ts | 46 +- .../test/test-utils/helpers.ts | 2 +- .../test/test-utils/matchers.ts | 8 +- .../test/typescript/codeGeneration.js | 26 +- .../test/validation.ts | 16 +- .../test/valueFromValueNode.ts | 2 +- .../src/__tests__/S3server.test.ts | 18 +- .../amplify-storage-simulator/src/index.ts | 2 +- .../src/server/S3server.ts | 19 +- .../src/server/utils.ts | 5 +- .../__tests__/js/auth.test.ts | 39 +- .../__tests__/js/predictions.test.ts | 117 +- .../__tests__/js/storage.test.ts | 111 +- .../amplify-ui-tests/src/categories/api.ts | 16 +- .../amplify-ui-tests/src/categories/auth.ts | 53 +- .../src/categories/predictions.ts | 104 +- .../src/categories/storage.ts | 56 +- .../amplify-ui-tests/src/configure/index.ts | 17 +- .../amplify-ui-tests/src/configure_tests.ts | 6 +- .../amplify-ui-tests/src/init/amplifyPush.ts | 12 +- .../src/init/deleteProject.ts | 10 +- packages/amplify-ui-tests/src/init/index.ts | 10 +- .../src/init/initProjectHelper.ts | 40 +- packages/amplify-ui-tests/src/main/delete.ts | 16 +- .../src/main/setup_aws_resources.ts | 98 +- .../amplify-ui-tests/src/utils/command.ts | 201 +- packages/amplify-ui-tests/src/utils/index.ts | 28 +- .../amplify-ui-tests/src/utils/projectMeta.ts | 22 +- .../amplify-util-mock/commands/mock/api.js | 8 +- .../commands/mock/function.js | 10 +- .../amplify-util-mock/commands/mock/mock.js | 4 +- .../commands/mock/storage.js | 8 +- .../CFNParser/appsync-resource-processor.ts | 127 +- .../src/CFNParser/field-parser.ts | 11 +- .../src/CFNParser/intrinsic-functions.ts | 160 +- .../CFNParser/lambda-resource-processor.ts | 16 +- .../connections-with-auth-tests.e2e.test.ts | 4 +- .../dynamo-db-model-transformer.e2e.test.ts | 26 +- .../src/__e2e__/key-transformer.e2e.test.ts | 52 +- .../src/__e2e__/key-with-auth.e2e.test.ts | 10 +- .../model-auth-transformer.e2e.test.ts | 52 +- .../model-connection-transformer.e2e.test.ts | 114 +- ...onnection-with-key-transformer.e2e.test.ts | 251 +- ...ti-auth-model-auth-transformer.e2e.test.ts | 12 +- .../__e2e__/per-field-auth-tests.e2e.test.ts | 10 +- .../subscriptions-with-auth.e2e.test.ts | 577 +- .../src/__e2e__/utils/graphql-client.ts | 40 +- .../src/__e2e__/utils/lambda-helper.ts | 20 +- .../utils/lambda_functions/echoFunction.js | 8 +- .../__e2e__/utils/lambda_functions/hello.js | 8 +- .../src/__e2e__/utils/test-storage.ts | 54 +- .../versioned-model-transformer.e2e.test.ts | 13 +- .../CFNParser/intrinsic-functions.test.ts | 107 +- .../src/amplify-plugin-index.ts | 2 +- packages/amplify-util-mock/src/api/api.ts | 35 +- .../src/api/resolver-overrides.ts | 4 +- .../src/api/run-graphql-transformer.ts | 17 +- packages/amplify-util-mock/src/index.ts | 7 +- packages/amplify-util-mock/src/mockAll.ts | 26 +- .../amplify-util-mock/src/storage/index.ts | 48 +- .../amplify-util-mock/src/storage/storage.ts | 7 +- .../amplify-util-mock/src/utils/ddb-utils.ts | 20 +- .../src/utils/lambda/execute.ts | 5 +- .../src/utils/lambda/invoke.ts | 36 +- .../src/utils/lambda/load.ts | 10 +- .../bin/build-tpl.js | 2 +- .../bin/velocity-cli.js | 36 +- .../amplify-velocity-template/examples/app.js | 4 +- .../examples/test.js | 8 +- .../examples/webpack.config.js | 5 +- .../src/parse/index.js | 2885 ++++-- .../tests/comment.test.js | 2 +- .../tests/compile.js | 792 +- .../tests/foreach.test.js | 157 +- .../amplify-velocity-template/tests/helper.js | 86 +- .../amplify-velocity-template/tests/index.ts | 6 +- .../amplify-velocity-template/tests/parse.js | 294 +- .../tests/return.test.js | 41 +- .../tests/runner/assert.js | 599 +- .../tests/runner/index-debug.js | 5131 +++++---- .../tests/runner/index.js | 2464 ++++- .../tests/runner/mocha.js | 9142 ++++++++--------- .../tests/runner/spec.js | 1697 ++- .../tests/runner/standalone-debug.js | 151 +- .../tests/script/syncRunner.js | 11 +- .../tests/set.test.js | 230 +- .../amplify-velocity-template/tests/stop.js | 6 +- .../graphql-auth-transformer/src/AuthRule.ts | 22 +- .../src/ModelAuthTransformer.ts | 3686 +++---- .../src/ModelDirectiveConfiguration.ts | 323 +- .../__tests__/GroupAuthTransformer.test.ts | 166 +- .../src/__tests__/MultiAuth.test.ts | 630 +- .../src/__tests__/OperationsArgument.test.ts | 371 +- .../__tests__/OwnerAuthTransformer.test.ts | 128 +- .../__tests__/PerFieldAuthArgument.test.ts | 76 +- .../SearchableAuthTransformer.test.ts | 131 +- .../graphql-auth-transformer/src/constants.ts | 18 +- .../src/graphQlApi.ts | 104 +- .../graphql-auth-transformer/src/index.ts | 6 +- .../graphql-auth-transformer/src/resources.ts | 1983 ++-- .../ModelConnectionTransformer.test.ts | 685 +- .../NewConnectionTransformer.test.ts | 483 +- .../src/definitions.ts | 65 +- .../src/index.ts | 6 +- .../src/resources.ts | 808 +- .../src/DynamoDBModelTransformer.ts | 829 +- .../src/ModelDirectiveArgs.ts | 30 +- .../DynamoDBModelTransformer.test.ts | 623 +- .../src/definitions.ts | 981 +- .../graphql-dynamodb-transformer/src/index.ts | 10 +- .../src/resources.ts | 1260 ++- .../src/SearchableModelTransformer.ts | 392 +- .../SearchableModelTransformer.test.ts | 5 +- .../src/definitions.ts | 377 +- .../src/index.ts | 6 +- .../src/resources.ts | 1038 +- .../src/FunctionTransformer.ts | 332 +- .../src/__tests__/FunctionTransformer.test.ts | 172 +- .../graphql-function-transformer/src/index.ts | 2 +- .../src/lambdaArns.ts | 36 +- .../src/HttpTransformer.ts | 462 +- .../src/__tests__/HttpTransformer.test.ts | 275 +- .../src/definitions.ts | 207 +- .../graphql-http-transformer/src/index.ts | 6 +- .../graphql-http-transformer/src/resources.ts | 610 +- .../src/KeyTransformer.ts | 1427 +-- .../src/__tests__/KeyTransformer.test.ts | 101 +- packages/graphql-key-transformer/src/index.ts | 2 +- .../src/__tests__/ast.test.ts | 86 +- packages/graphql-mapping-template/src/ast.ts | 400 +- .../graphql-mapping-template/src/dynamodb.ts | 442 +- .../src/elasticsearch.ts | 106 +- packages/graphql-mapping-template/src/http.ts | 136 +- .../graphql-mapping-template/src/print.ts | 251 +- .../src/AuroraDataAPIClient.ts | 195 +- .../AuroraServerlessMySQLDatabaseReader.ts | 309 +- .../src/IRelationalDBReader.ts | 12 +- .../src/RelationalDBMappingTemplate.ts | 31 +- .../src/RelationalDBParsingException.ts | 12 +- .../src/RelationalDBResolverGenerator.ts | 622 +- .../src/RelationalDBSchemaTransformer.ts | 448 +- .../src/RelationalDBSchemaTransformerUtils.ts | 268 +- .../src/RelationalDBTemplateGenerator.ts | 344 +- .../src/ResourceConstants.ts | 67 +- .../src/__tests__/AuroraDataAPIClient.test.ts | 902 +- .../AuroraServerlessMySQLDBReader.test.ts | 279 +- .../RelationalDBMappingTemplate.test.ts | 44 +- .../RelationalDBResolverGenerator.test.ts | 62 +- .../RelationalDBSchemaTransformer.test.ts | 334 +- ...RelationalDBSchemaTransformerUtils.test.ts | 276 +- .../RelationalDBTemplateGenerator.test.ts | 89 +- .../src/index.ts | 6 +- .../src/FunctionResourceIDs.ts | 19 +- .../src/HttpResourceIDs.ts | 8 +- .../src/ModelResourceIDs.ts | 175 +- .../src/ResolverResourceIDs.ts | 46 +- .../src/ResourceConstants.ts | 190 +- .../src/SearchableResourceIDs.ts | 22 +- .../src/__tests__/common.test.ts | 2 +- .../src/connectionUtils.ts | 20 +- .../src/definition.ts | 532 +- .../src/dynamodbUtils.ts | 957 +- .../graphql-transformer-common/src/index.ts | 20 +- .../src/nodeUtils.ts | 40 +- .../graphql-transformer-common/src/util.ts | 37 +- .../src/DeploymentResources.ts | 40 +- .../src/GraphQLTransform.ts | 1003 +- .../src/ITransformer.ts | 196 +- .../src/TransformFormatter.ts | 354 +- .../src/Transformer.ts | 249 +- .../src/TransformerContext.ts | 1310 +-- .../src/__tests__/GraphQLTransform.test.ts | 164 +- .../src/__tests__/TransformFormatter.test.ts | 156 +- .../__tests__/getTemplateReferences.test.ts | 148 +- .../src/collectDirectives.ts | 221 +- .../src/defaultSchema.ts | 72 +- .../graphql-transformer-core/src/errors.ts | 97 +- .../graphql-transformer-core/src/index.ts | 68 +- .../src/polyfills/Object.assign.ts | 42 +- .../src/stripDirectives.ts | 205 +- .../src/util/SchemaResourceUtil.ts | 193 +- .../src/util/amplifyUtils.ts | 1022 +- .../src/util/blankTemplate.ts | 22 +- .../src/util/fileUtils.ts | 160 +- .../src/util/getDirectiveArguments.ts | 30 +- .../src/util/getFieldArguments.ts | 28 +- .../src/util/getIn.ts | 18 +- .../src/util/getTemplateReferences.ts | 82 +- .../graphql-transformer-core/src/util/gql.ts | 18 +- .../src/util/makeExportName.ts | 2 +- .../src/util/sanity-check.ts | 498 +- .../src/util/setIn.ts | 16 +- .../src/util/splitStack.ts | 641 +- .../src/util/transformConfig.ts | 267 +- .../src/validation.ts | 144 +- .../src/CloudFormationClient.ts | 267 +- .../src/GraphQLClient.ts | 40 +- .../src/IAMHelper.ts | 82 +- .../src/LambdaHelper.ts | 50 +- .../src/S3Client.ts | 311 +- .../src/TestStorage.ts | 54 +- .../ConnectionsWithAuthTests.e2e.test.ts | 595 +- .../src/__tests__/CustomRoots.e2e.test.ts | 245 +- .../DynamoDBModelTransformer.e2e.test.ts | 740 +- .../FunctionTransformerTests.e2e.test.ts | 321 +- .../src/__tests__/HttpTransformer.e2e.test.ts | 435 +- .../src/__tests__/KeyTransformer.e2e.test.ts | 894 +- .../__tests__/KeyTransformerLocal.e2e.test.ts | 602 +- .../src/__tests__/KeyWithAuth.e2e.test.ts | 473 +- .../ModelAuthTransformer.e2e.test.ts | 2203 ++-- .../ModelConnectionTransformer.e2e.test.ts | 66 +- ...elConnectionWithKeyTransformer.e2e.test.ts | 416 +- .../MultiAuthModelAuthTransformer.e2e.test.ts | 1647 +-- .../__tests__/NestedStacksTest.e2e.test.ts | 115 +- .../NewConnectionTransformer.e2e.test.ts | 551 +- .../NewConnectionWithAuth.e2e.test.ts | 620 +- .../NoneEnvFunctionTransformer.e2e.test.ts | 245 +- .../__tests__/PerFieldAuthTests.e2e.test.ts | 713 +- .../SearchableModelTransformer.e2e.test.ts | 800 +- .../SearchableWithAuthTests.e2e.test.ts | 907 +- .../TestComplexStackMappingsLocal.e2e.test.ts | 280 +- .../VersionedModelTransformer.e2e.test.ts | 299 +- .../src/cognitoUtils.ts | 663 +- .../src/deployNestedStacks.ts | 354 +- .../src/emptyBucket.ts | 84 +- .../src/setupUserPool.ts | 70 +- .../src/testUtil.ts | 112 +- .../src/testfunctions/echoFunction.js | 8 +- .../src/testfunctions/hello.js | 8 +- .../src/VersionedModelTransformer.ts | 421 +- .../VersionedModelTransformer.test.ts | 103 +- .../src/index.ts | 6 +- 474 files changed, 48425 insertions(+), 44236 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index aff2bf3144..d41f4c6676 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ['@commitlint/config-lerna-scopes', '@commitlint/config-conventional'] + extends: ['@commitlint/config-lerna-scopes', '@commitlint/config-conventional'], }; diff --git a/cypress/integration/api_spec.js b/cypress/integration/api_spec.js index c67f4b14a3..e3d0584dc9 100644 --- a/cypress/integration/api_spec.js +++ b/cypress/integration/api_spec.js @@ -1,19 +1,22 @@ - describe('API test post and get', function() { - beforeEach(function() { - cy.visit('/') - }) - - it('successfully adds data to dynamodb', function() { - // Check for user not signed up - cy.get('input[name=itemNo]').type('1') - cy.get('.amplify-submit-put-button').contains('Put').click() - cy.get('.amplify-put-result').contains('post call succeed!') - }) + beforeEach(function() { + cy.visit('/'); + }); + + it('successfully adds data to dynamodb', function() { + // Check for user not signed up + cy.get('input[name=itemNo]').type('1'); + cy.get('.amplify-submit-put-button') + .contains('Put') + .click(); + cy.get('.amplify-put-result').contains('post call succeed!'); + }); - it('successfully get data from dynamodb', function() { - // Check for user not signed up - cy.get('.amplify-submit-get-button').contains('Get').click() - cy.get('.amplify-get-result').contains('"itemNo":1') - }) -}) \ No newline at end of file + it('successfully get data from dynamodb', function() { + // Check for user not signed up + cy.get('.amplify-submit-get-button') + .contains('Get') + .click(); + cy.get('.amplify-get-result').contains('"itemNo":1'); + }); +}); diff --git a/cypress/integration/auth_spec.js b/cypress/integration/auth_spec.js index 3c40ab7098..0edebf725e 100644 --- a/cypress/integration/auth_spec.js +++ b/cypress/integration/auth_spec.js @@ -1,15 +1,15 @@ - describe('withAuthenticator Sign In', function() { beforeEach(function() { - cy.visit('/') - }) + cy.visit('/'); + }); it('throws error when user is not signed up', function() { // Check for user not signed up - cy.get('input[name=username]').type('testuser') - cy.get('input[name=password]').type('testPassword') - cy.get('button').contains('Sign In').click() - cy.get('div').contains('User does not exist') - }) -}) - + cy.get('input[name=username]').type('testuser'); + cy.get('input[name=password]').type('testPassword'); + cy.get('button') + .contains('Sign In') + .click(); + cy.get('div').contains('User does not exist'); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index fd170fba69..dffed2532f 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -14,4 +14,4 @@ module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config -} +}; diff --git a/cypress/support/index.js b/cypress/support/index.js index d68db96df2..37a498fb5b 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/amplify-appsync-simulator/src/__tests__/velocity/util/time.test.ts b/packages/amplify-appsync-simulator/src/__tests__/velocity/util/time.test.ts index 8179cb943a..5a7a675355 100644 --- a/packages/amplify-appsync-simulator/src/__tests__/velocity/util/time.test.ts +++ b/packages/amplify-appsync-simulator/src/__tests__/velocity/util/time.test.ts @@ -37,47 +37,25 @@ describe('Velocity $context.util.time', () => { }); it('parseFormattedToEpochMilliSeconds', () => { - expect( - time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_UTC, FORMAT_CUSTOM_ZONED) - ).toEqual(TEST_TIMESTAMP_MILLIS); - expect( - time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_PLUS8, FORMAT_CUSTOM_ZONED) - ).toEqual(TEST_TIMESTAMP_MILLIS); - expect( - time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_UTC, FORMAT_CUSTOM_ZONED) - ).toEqual(TEST_TIMESTAMP_MILLIS); - expect( - time.parseFormattedToEpochMilliSeconds( - TEST_TIMESTAMP_CUSTOM_PLUS8, - FORMAT_CUSTOM_ZONED, - 'Australia/Perth' - ) - ).toEqual(TEST_TIMESTAMP_MILLIS); - expect( - time.parseFormattedToEpochMilliSeconds( - TEST_TIMESTAMP_CUSTOM_UTC_UNZONED, - FORMAT_CUSTOM_UNZONED, - 'UTC' - ) - ).toEqual(TEST_TIMESTAMP_MILLIS); - - expect( - time.parseFormattedToEpochMilliSeconds( - TEST_TIMESTAMP_CUSTOM_PLUS8_UNZONED, - FORMAT_CUSTOM_UNZONED, - 'Australia/Perth' - ) - ).toEqual(TEST_TIMESTAMP_MILLIS); - }); - - it('parseISO8601ToEpochMilliSeconds', () => { - expect(time.parseISO8601ToEpochMilliSeconds(TEST_TIMESTAMP_ZULU)).toEqual( + expect(time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_UTC, FORMAT_CUSTOM_ZONED)).toEqual(TEST_TIMESTAMP_MILLIS); + expect(time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_PLUS8, FORMAT_CUSTOM_ZONED)).toEqual(TEST_TIMESTAMP_MILLIS); + expect(time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_UTC, FORMAT_CUSTOM_ZONED)).toEqual(TEST_TIMESTAMP_MILLIS); + expect(time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_PLUS8, FORMAT_CUSTOM_ZONED, 'Australia/Perth')).toEqual( + TEST_TIMESTAMP_MILLIS + ); + expect(time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_UTC_UNZONED, FORMAT_CUSTOM_UNZONED, 'UTC')).toEqual( TEST_TIMESTAMP_MILLIS ); - expect(time.parseISO8601ToEpochMilliSeconds(TEST_TIMESTAMP_PLUS8)).toEqual( + + expect(time.parseFormattedToEpochMilliSeconds(TEST_TIMESTAMP_CUSTOM_PLUS8_UNZONED, FORMAT_CUSTOM_UNZONED, 'Australia/Perth')).toEqual( TEST_TIMESTAMP_MILLIS ); }); + + it('parseISO8601ToEpochMilliSeconds', () => { + expect(time.parseISO8601ToEpochMilliSeconds(TEST_TIMESTAMP_ZULU)).toEqual(TEST_TIMESTAMP_MILLIS); + expect(time.parseISO8601ToEpochMilliSeconds(TEST_TIMESTAMP_PLUS8)).toEqual(TEST_TIMESTAMP_MILLIS); + }); it('epochMilliSecondsToSeconds', () => { expect(time.epochMilliSecondsToSeconds(TEST_TIMESTAMP_MILLIS)).toEqual(TEST_TIMESTAMP_SECS); }); @@ -86,16 +64,10 @@ describe('Velocity $context.util.time', () => { }); it('epochMilliSecondsToFormatted', () => { - expect(time.epochMilliSecondsToFormatted(TEST_TIMESTAMP_MILLIS, FORMAT_CUSTOM_ZONED)).toEqual( - TEST_TIMESTAMP_CUSTOM_UTC - ); + expect(time.epochMilliSecondsToFormatted(TEST_TIMESTAMP_MILLIS, FORMAT_CUSTOM_ZONED)).toEqual(TEST_TIMESTAMP_CUSTOM_UTC); - expect( - time.epochMilliSecondsToFormatted( - TEST_TIMESTAMP_MILLIS, - FORMAT_CUSTOM_ZONED, - 'Australia/Perth' - ) - ).toEqual(TEST_TIMESTAMP_CUSTOM_PLUS8); + expect(time.epochMilliSecondsToFormatted(TEST_TIMESTAMP_MILLIS, FORMAT_CUSTOM_ZONED, 'Australia/Perth')).toEqual( + TEST_TIMESTAMP_CUSTOM_PLUS8 + ); }); }); diff --git a/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/map.test.ts b/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/map.test.ts index bb68ab82b8..2cfbbcb56e 100644 --- a/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/map.test.ts +++ b/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/map.test.ts @@ -36,10 +36,7 @@ describe('JavaMap', () => { it('entrySet', () => { const obj = { foo: 'Foo Value', bar: 'Bar Value' }; const map = new JavaMap(obj, identityMapper); - expect(map.entrySet().toJSON()).toEqual([ - { key: 'foo', value: 'Foo Value' }, - { key: 'bar', value: 'Bar Value' }, - ]); + expect(map.entrySet().toJSON()).toEqual([{ key: 'foo', value: 'Foo Value' }, { key: 'bar', value: 'Bar Value' }]); }); it('equal', () => { diff --git a/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/string.test.ts b/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/string.test.ts index cfd680f79f..1547284943 100644 --- a/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/string.test.ts +++ b/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/string.test.ts @@ -2,10 +2,10 @@ import { JavaString } from '../../../velocity/value-mapper/string'; describe('JavaString', () => { it('replaceAll', () => { - const str = new JavaString('foo bar foo bar foo bar Foo') + const str = new JavaString('foo bar foo bar foo bar Foo'); const replacedStr = str.replaceAll('foo', 'baz'); expect(replacedStr.toString()).toEqual('baz bar baz bar baz bar Foo'); expect(replacedStr.toIdString()).toEqual('baz bar baz bar baz bar Foo'); expect(replacedStr.toJSON()).toEqual('baz bar baz bar baz bar Foo'); - }) -}); \ No newline at end of file + }); +}); diff --git a/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/to-json.test.ts b/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/to-json.test.ts index d756c68ecf..3a32bdae34 100644 --- a/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/to-json.test.ts +++ b/packages/amplify-appsync-simulator/src/__tests__/velocity/value-mapper/to-json.test.ts @@ -13,7 +13,7 @@ describe('Velocity - ValueMapper toJSON', () => { it('should not call toJSON if the object is null', () => { expect(toJSON(null)).toEqual(null); }); - + it('should return the source object if it doesnot implement toJSON', () => { const testObj = { foo: 'Foo', diff --git a/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/index.ts b/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/index.ts index f82372d26d..775ccd5fe7 100644 --- a/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/index.ts +++ b/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/index.ts @@ -102,16 +102,7 @@ export class DynamoDBDataLoader implements AmplifyAppSyncSimulatorDataLoader { return this.getItem({ key, consistentRead: true }); } - private async query({ - query: keyCondition, - filter, - index, - nextToken, - limit, - scanIndexForward = true, - consistentRead = false, - select, - }) { + private async query({ query: keyCondition, filter, index, nextToken, limit, scanIndexForward = true, consistentRead = false, select }) { keyCondition = keyCondition || { expression: null }; filter = filter || { expression: null }; const params = { @@ -133,18 +124,14 @@ export class DynamoDBDataLoader implements AmplifyAppSyncSimulatorDataLoader { ScanIndexForward: scanIndexForward, Select: select || 'ALL_ATTRIBUTES', }; - const { - Items: items, - ScannedCount: scannedCount, - LastEvaluatedKey: resultNextToken = null, - } = await this.client.query(params as any).promise(); + const { Items: items, ScannedCount: scannedCount, LastEvaluatedKey: resultNextToken = null } = await this.client + .query(params as any) + .promise(); return { items: items.map(item => unmarshall(item)), scannedCount, - nextToken: resultNextToken - ? Buffer.from(JSON.stringify(resultNextToken)).toString('base64') - : null, + nextToken: resultNextToken ? Buffer.from(JSON.stringify(resultNextToken)).toString('base64') : null, }; } @@ -194,16 +181,7 @@ export class DynamoDBDataLoader implements AmplifyAppSyncSimulatorDataLoader { return unmarshall(deleted); } private async scan(payload) { - const { - filter, - index, - limit, - consistentRead = false, - nextToken, - select, - totalSegments, - segment, - } = payload; + const { filter, index, limit, consistentRead = false, nextToken, select, totalSegments, segment } = payload; const params = { TableName: this.tableName, @@ -226,18 +204,12 @@ export class DynamoDBDataLoader implements AmplifyAppSyncSimulatorDataLoader { }, }); } - const { - Items: items, - ScannedCount: scannedCount, - LastEvaluatedKey: resultNextToken = null, - } = await this.client.scan(params).promise(); + const { Items: items, ScannedCount: scannedCount, LastEvaluatedKey: resultNextToken = null } = await this.client.scan(params).promise(); return { items: items.map(item => unmarshall(item)), scannedCount, - nextToken: resultNextToken - ? Buffer.from(JSON.stringify(resultNextToken)).toString('base64') - : null, + nextToken: resultNextToken ? Buffer.from(JSON.stringify(resultNextToken)).toString('base64') : null, }; } } diff --git a/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/utils/index.ts b/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/utils/index.ts index 88989fc892..a15d6c2a61 100644 --- a/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/utils/index.ts +++ b/packages/amplify-appsync-simulator/src/data-loader/dynamo-db/utils/index.ts @@ -26,7 +26,7 @@ export function unmarshall(raw, isRaw: boolean = true) { ...sum, [key]: unmarshall(value, false), }), - {}, + {} ); } diff --git a/packages/amplify-appsync-simulator/src/index.ts b/packages/amplify-appsync-simulator/src/index.ts index df4ed5ff5b..cc593291e8 100644 --- a/packages/amplify-appsync-simulator/src/index.ts +++ b/packages/amplify-appsync-simulator/src/index.ts @@ -41,7 +41,7 @@ export class AmplifyAppSyncSimulator { serverConfig: AppSyncSimulatorServerConfig = { port: 0, wsPort: 0, - }, + } ) { this._serverConfig = serverConfig; this._pubsub = new PubSub(); @@ -77,11 +77,7 @@ export class AmplifyAppSyncSimulator { }, new Map()); this.functions = (config.functions || []).reduce((map, fn) => { - const { - dataSourceName, - requestMappingTemplateLocation, - responseMappingTemplateLocation, - } = fn; + const { dataSourceName, requestMappingTemplateLocation, responseMappingTemplateLocation } = fn; map.set( fn.name, new AmplifySimulatorFunction( @@ -90,8 +86,8 @@ export class AmplifyAppSyncSimulator { requestMappingTemplateLocation: requestMappingTemplateLocation, responseMappingTemplateLocation: responseMappingTemplateLocation, }, - this, - ), + this + ) ); return map; }, new Map()); @@ -109,11 +105,7 @@ export class AmplifyAppSyncSimulator { return map; }, new Map()); - this._schema = generateResolvers( - new Source(config.schema.content, config.schema.path), - config.resolvers, - this, - ); + this._schema = generateResolvers(new Source(config.schema.content, config.schema.path), config.resolvers, this); this._config = config; } catch (e) { this._schema = lastSchema; diff --git a/packages/amplify-appsync-simulator/src/mqtt-server/client.ts b/packages/amplify-appsync-simulator/src/mqtt-server/client.ts index 9ae43c7ca9..e5ceb07ca4 100644 --- a/packages/amplify-appsync-simulator/src/mqtt-server/client.ts +++ b/packages/amplify-appsync-simulator/src/mqtt-server/client.ts @@ -175,12 +175,7 @@ export class Client { } // skip delivery of messages in $SYS for wildcards - forward = - forward && - !( - topic.indexOf('$SYS') >= 0 && - ((indexWildcard >= 0 && indexWildcard < 2) || (indexPlus >= 0 && indexPlus < 2)) - ); + forward = forward && !(topic.indexOf('$SYS') >= 0 && ((indexWildcard >= 0 && indexWildcard < 2) || (indexPlus >= 0 && indexPlus < 2))); if (forward) { if (options._dedupId === undefined) { diff --git a/packages/amplify-appsync-simulator/src/mqtt-server/util.ts b/packages/amplify-appsync-simulator/src/mqtt-server/util.ts index b806c12d91..df70b9cd3b 100644 --- a/packages/amplify-appsync-simulator/src/mqtt-server/util.ts +++ b/packages/amplify-appsync-simulator/src/mqtt-server/util.ts @@ -1,6 +1,5 @@ -export function defer (done) { - if (typeof done === "function") { +export function defer(done) { + if (typeof done === 'function') { setImmediate(done); } } - diff --git a/packages/amplify-appsync-simulator/src/resolvers/function.ts b/packages/amplify-appsync-simulator/src/resolvers/function.ts index 945c54d8b0..3c07d8b27b 100644 --- a/packages/amplify-appsync-simulator/src/resolvers/function.ts +++ b/packages/amplify-appsync-simulator/src/resolvers/function.ts @@ -2,15 +2,8 @@ import { AmplifyAppSyncSimulator } from '..'; import { AppSyncSimulatorFunctionResolverConfig } from '../type-definition'; export class AmplifySimulatorFunction { - constructor( - private config: AppSyncSimulatorFunctionResolverConfig, - private simulatorContext: AmplifyAppSyncSimulator - ) { - const { - dataSourceName, - requestMappingTemplateLocation, - responseMappingTemplateLocation, - } = config; + constructor(private config: AppSyncSimulatorFunctionResolverConfig, private simulatorContext: AmplifyAppSyncSimulator) { + const { dataSourceName, requestMappingTemplateLocation, responseMappingTemplateLocation } = config; if (!dataSourceName || !requestMappingTemplateLocation || !responseMappingTemplateLocation) { throw new Error(`Invalid configuration parameter for function ${JSON.stringify(config)}`); } @@ -29,29 +22,14 @@ export class AmplifySimulatorFunction { this.config = config; } - async resolve( - source, - args, - stash, - prevResult, - context, - info - ): Promise<{ result: any; stash: any }> { + async resolve(source, args, stash, prevResult, context, info): Promise<{ result: any; stash: any }> { let result = null; let error = null; - const requestMappingTemplate = this.simulatorContext.getMappingTemplate( - this.config.requestMappingTemplateLocation - ); - const responseMappingTemplate = this.simulatorContext.getMappingTemplate( - this.config.responseMappingTemplateLocation - ); + const requestMappingTemplate = this.simulatorContext.getMappingTemplate(this.config.requestMappingTemplateLocation); + const responseMappingTemplate = this.simulatorContext.getMappingTemplate(this.config.responseMappingTemplateLocation); const dataLoader = this.simulatorContext.getDataLoader(this.config.dataSourceName); - const requestTemplateResult = await requestMappingTemplate.render( - { source, arguments: args, stash, prevResult }, - context, - info - ); + const requestTemplateResult = await requestMappingTemplate.render({ source, arguments: args, stash, prevResult }, context, info); context.appsyncErrors = [...context.appsyncErrors, ...requestTemplateResult.errors]; try { diff --git a/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts b/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts index 8f4c08c6b0..787ca42b85 100644 --- a/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts +++ b/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts @@ -3,10 +3,7 @@ import { AppSyncSimulatorPipelineResolverConfig } from '../type-definition'; export class AppSyncPipelineResolver { private config: AppSyncSimulatorPipelineResolverConfig; - constructor( - config: AppSyncSimulatorPipelineResolverConfig, - private simulatorContext: AmplifyAppSyncSimulator - ) { + constructor(config: AppSyncSimulatorPipelineResolverConfig, private simulatorContext: AmplifyAppSyncSimulator) { try { simulatorContext.getMappingTemplate(config.requestMappingTemplateLocation); simulatorContext.getMappingTemplate(config.responseMappingTemplateLocation); @@ -22,23 +19,15 @@ export class AppSyncPipelineResolver { } async resolve(source, args, context, info) { - const requestMappingTemplate = this.simulatorContext.getMappingTemplate( - this.config.requestMappingTemplateLocation - ); - const responseMappingTemplate = this.simulatorContext.getMappingTemplate( - this.config.responseMappingTemplateLocation - ); + const requestMappingTemplate = this.simulatorContext.getMappingTemplate(this.config.requestMappingTemplateLocation); + const responseMappingTemplate = this.simulatorContext.getMappingTemplate(this.config.responseMappingTemplateLocation); let result = {}; let stash = {}; let templateErrors; // Pipeline request mapping template - ({ result, stash, errors: templateErrors } = requestMappingTemplate.render( - { source, arguments: args, stash }, - context, - info - )); + ({ result, stash, errors: templateErrors } = requestMappingTemplate.render({ source, arguments: args, stash }, context, info)); context.appsyncErrors = [...context.appsyncErrors, ...templateErrors]; // Pipeline functions @@ -46,14 +35,7 @@ export class AppSyncPipelineResolver { .reduce((chain, fn) => { const fnResolver = this.simulatorContext.getFunction(fn); const p = chain.then(async ({ prevResult, stash }) => { - ({ result: prevResult, stash } = await fnResolver.resolve( - source, - args, - stash, - prevResult, - context, - info - )); + ({ result: prevResult, stash } = await fnResolver.resolve(source, args, stash, prevResult, context, info)); return Promise.resolve({ prevResult, stash }); }); return p; diff --git a/packages/amplify-appsync-simulator/src/resolvers/unit-resolver.ts b/packages/amplify-appsync-simulator/src/resolvers/unit-resolver.ts index bda2910bbe..82ea4a4f61 100644 --- a/packages/amplify-appsync-simulator/src/resolvers/unit-resolver.ts +++ b/packages/amplify-appsync-simulator/src/resolvers/unit-resolver.ts @@ -3,10 +3,7 @@ import { AppSyncSimulatorUnitResolverConfig } from '../type-definition'; export class AppSyncUnitResolver { private config: AppSyncSimulatorUnitResolverConfig; - constructor( - config: AppSyncSimulatorUnitResolverConfig, - private simulatorContext: AmplifyAppSyncSimulator - ) { + constructor(config: AppSyncSimulatorUnitResolverConfig, private simulatorContext: AmplifyAppSyncSimulator) { try { simulatorContext.getMappingTemplate(config.requestMappingTemplateLocation); simulatorContext.getMappingTemplate(config.responseMappingTemplateLocation); @@ -22,12 +19,8 @@ export class AppSyncUnitResolver { } async resolve(source, args, context, info) { - const requestMappingTemplate = this.simulatorContext.getMappingTemplate( - this.config.requestMappingTemplateLocation - ); - const responseMappingTemplate = this.simulatorContext.getMappingTemplate( - this.config.responseMappingTemplateLocation - ); + const requestMappingTemplate = this.simulatorContext.getMappingTemplate(this.config.requestMappingTemplateLocation); + const responseMappingTemplate = this.simulatorContext.getMappingTemplate(this.config.responseMappingTemplateLocation); const dataLoader = this.simulatorContext.getDataLoader(this.config.dataSourceName); const { result: requestPayload, errors: requestTemplateErrors } = requestMappingTemplate.render( { source, arguments: args }, @@ -51,10 +44,11 @@ export class AppSyncUnitResolver { return; } - const { - result: responseTemplateResult, - errors: responseTemplateErrors, - } = responseMappingTemplate.render({ source, arguments: args, result, error }, context, info); + const { result: responseTemplateResult, errors: responseTemplateErrors } = responseMappingTemplate.render( + { source, arguments: args, result, error }, + context, + info + ); context.appsyncErrors = [...context.appsyncErrors, ...responseTemplateErrors]; return responseTemplateResult; diff --git a/packages/amplify-appsync-simulator/src/schema/appsync-scalars/index.ts b/packages/amplify-appsync-simulator/src/schema/appsync-scalars/index.ts index d42582c8b5..84cebdd25d 100644 --- a/packages/amplify-appsync-simulator/src/schema/appsync-scalars/index.ts +++ b/packages/amplify-appsync-simulator/src/schema/appsync-scalars/index.ts @@ -93,7 +93,8 @@ const AWSDateTime = new GraphQLScalarType({ const AWSTimestamp = new GraphQLScalarType({ name: 'AWSTimestamp', - description: 'The AWSTimestamp scalar type represents the number of seconds that have elapsed \ + description: + 'The AWSTimestamp scalar type represents the number of seconds that have elapsed \ since 1970-01-01T00:00Z. Timestamps are serialized and deserialized as numbers. Negative values \ are also accepted and these represent the number of seconds till 1970-01-01T00:00Z.', serialize(value) { @@ -108,14 +109,14 @@ are also accepted and these represent the number of seconds till 1970-01-01T00:0 }); // Unified the code for both types from graphql-scalars library. -const validateIPAddress = (value) => { +const validateIPAddress = value => { if (typeof value !== 'string') { throw new TypeError(`Value is not string: ${value}`); } - if (!(IPV4_REGEX.test(value))) { + if (!IPV4_REGEX.test(value)) { throw new TypeError(`Value is not a valid IPv4 address: ${value}`); } - if (!(IPV6_REGEX.test(value))) { + if (!IPV6_REGEX.test(value)) { throw new TypeError(`Value is not a valid IPv6 address: ${value}`); } return value; @@ -132,10 +133,10 @@ const AWSIPAddress = new GraphQLScalarType({ }, parseLiteral(ast) { if (ast.kind !== Kind.STRING) { - throw new GraphQLError(`Can only validate strings as IPv4 or IPv6 addresses but got a: ${ast.kind}`); + throw new GraphQLError(`Can only validate strings as IPv4 or IPv6 addresses but got a: ${ast.kind}`); } return validateIPAddress(ast.value); - } + }, }); export const scalars = { @@ -147,7 +148,7 @@ export const scalars = { AWSEmail: EmailAddressResolver, AWSURL: URLResolver, AWSTimestamp, - AWSIPAddress + AWSIPAddress, }; export function wrapSchema(schemaString) { diff --git a/packages/amplify-appsync-simulator/src/schema/directives/auth.ts b/packages/amplify-appsync-simulator/src/schema/directives/auth.ts index 523d5c1f7c..bbe7835b63 100644 --- a/packages/amplify-appsync-simulator/src/schema/directives/auth.ts +++ b/packages/amplify-appsync-simulator/src/schema/directives/auth.ts @@ -1,17 +1,7 @@ -import { - defaultFieldResolver, - DirectiveNode, - GraphQLObjectType, - GraphQLSchema, - valueFromASTUntyped, - GraphQLResolveInfo, -} from 'graphql'; +import { defaultFieldResolver, DirectiveNode, GraphQLObjectType, GraphQLSchema, valueFromASTUntyped, GraphQLResolveInfo } from 'graphql'; import { buildSchemaFromTypeDefinitions, forEachField } from 'graphql-tools'; import { AmplifyAppSyncSimulator } from '../..'; -import { - AmplifyAppSyncSimulatorAuthenticationType, - AmplifyAppSyncSimulatorRequestContext, -} from '../../type-definition'; +import { AmplifyAppSyncSimulatorAuthenticationType, AmplifyAppSyncSimulatorRequestContext } from '../../type-definition'; import { Unauthorized } from '../../velocity/util'; import AppSyncSimulatorDirectiveBase from './directive-base'; @@ -19,8 +9,7 @@ const AUTH_DIRECTIVES = { aws_api_key: 'directive @aws_api_key on FIELD_DEFINITION | OBJECT', aws_iam: 'directive @aws_iam on FIELD_DEFINITION | OBJECT', aws_oidc: 'directive @aws_oidc on FIELD_DEFINITION | OBJECT', - aws_cognito_user_pools: - 'directive @aws_cognito_user_pools(cognito_groups: [String!]) on FIELD_DEFINITION | OBJECT', + aws_cognito_user_pools: 'directive @aws_cognito_user_pools(cognito_groups: [String!]) on FIELD_DEFINITION | OBJECT', aws_auth: 'directive @aws_auth(cognito_groups: [String!]!) on FIELD_DEFINITION', }; @@ -61,21 +50,10 @@ function getAuthDirectiveForField( const parentField = schema.getType(typeName); const fieldAuthDirectives = getAuthDirective(fieldDirectives); const parentAuthDirectives = getAuthDirective(parentField.astNode.directives); - const allowedDirectives = fieldAuthDirectives.length - ? fieldAuthDirectives - : parentAuthDirectives.length - ? parentAuthDirectives - : []; + const allowedDirectives = fieldAuthDirectives.length ? fieldAuthDirectives : parentAuthDirectives.length ? parentAuthDirectives : []; const allowedAuthModes: Set = new Set(); return allowedDirectives.length - ? Array.from( - allowedDirectives - .reduce( - (acc, directive) => acc.add(AUTH_TYPE_TO_DIRECTIVE_MAP[directive]), - allowedAuthModes - ) - .values() - ) + ? Array.from(allowedDirectives.reduce((acc, directive) => acc.add(AUTH_TYPE_TO_DIRECTIVE_MAP[directive]), allowedAuthModes).values()) : [simulator.appSyncConfig.defaultAuthenticationType.authenticationType]; } @@ -110,11 +88,7 @@ function getDirectiveArgumentValues(directives: DirectiveNode, argName: string) .reduce((acc, arg) => [...acc, ...valueFromASTUntyped(arg.value)], []); } -export function protectResolversWithAuthRules( - typeDef, - existingResolvers, - simulator: AmplifyAppSyncSimulator -) { +export function protectResolversWithAuthRules(typeDef, existingResolvers, simulator: AmplifyAppSyncSimulator) { const schema = buildSchemaFromTypeDefinitions(typeDef); const newResolverMap = {}; forEachField(schema, (field, typeName, fieldName) => { @@ -129,8 +103,7 @@ export function protectResolversWithAuthRules( throw err; } if ( - ctx.requestAuthorizationMode === - AmplifyAppSyncSimulatorAuthenticationType.AMAZON_COGNITO_USER_POOLS && + ctx.requestAuthorizationMode === AmplifyAppSyncSimulatorAuthenticationType.AMAZON_COGNITO_USER_POOLS && allowedCognitoGroups.length ) { const groups = getCognitoGroups(ctx.jwt || {}); @@ -145,7 +118,7 @@ export function protectResolversWithAuthRules( if (!newResolverMap[typeName]) { newResolverMap[typeName] = {}; } - newResolverMap[typeName][fieldName] = {...fieldResolver, resolve: newResolver }; + newResolverMap[typeName][fieldName] = { ...fieldResolver, resolve: newResolver }; }); return newResolverMap; } diff --git a/packages/amplify-appsync-simulator/src/schema/directives/aws-subscribe.ts b/packages/amplify-appsync-simulator/src/schema/directives/aws-subscribe.ts index 220bab1b0f..41ab46ca83 100644 --- a/packages/amplify-appsync-simulator/src/schema/directives/aws-subscribe.ts +++ b/packages/amplify-appsync-simulator/src/schema/directives/aws-subscribe.ts @@ -1,8 +1,7 @@ import AppSyncSimulatorDirectiveBase from './directive-base'; export class AwsSubscribe extends AppSyncSimulatorDirectiveBase { - static typeDefinitions: string = - 'directive @aws_subscribe(mutations: [String!]) on FIELD_DEFINITION'; + static typeDefinitions: string = 'directive @aws_subscribe(mutations: [String!]) on FIELD_DEFINITION'; name: string = 'aws_subscribe'; visitFieldDefinition(field) { diff --git a/packages/amplify-appsync-simulator/src/schema/directives/index.ts b/packages/amplify-appsync-simulator/src/schema/directives/index.ts index f724be3e29..dbdf9f8ed7 100644 --- a/packages/amplify-appsync-simulator/src/schema/directives/index.ts +++ b/packages/amplify-appsync-simulator/src/schema/directives/index.ts @@ -1,2 +1,2 @@ export { AwsSubscribe } from './aws-subscribe'; -export { AwsAuth, protectResolversWithAuthRules } from './auth' +export { AwsAuth, protectResolversWithAuthRules } from './auth'; diff --git a/packages/amplify-appsync-simulator/src/schema/index.ts b/packages/amplify-appsync-simulator/src/schema/index.ts index 048cf7f51c..87b1102cc7 100644 --- a/packages/amplify-appsync-simulator/src/schema/index.ts +++ b/packages/amplify-appsync-simulator/src/schema/index.ts @@ -42,10 +42,7 @@ export function generateResolvers( const typeName = resolverConfig.typeName; typeObj[resolverConfig.fieldName] = { resolve: async (source, args, context, info) => { - const resolver = simulatorContext.getResolver( - resolverConfig.typeName, - resolverConfig.fieldName - ); + const resolver = simulatorContext.getResolver(resolverConfig.typeName, resolverConfig.fieldName); try { // Mutation and Query if (typeName !== 'Subscription') { @@ -111,9 +108,7 @@ function generateDefaultSubscriptions( configuredResolvers: AppSyncSimulatorBaseResolverConfig[], simulatorContext: AmplifyAppSyncSimulator ) { - const configuredSubscriptions = configuredResolvers - .filter(cfg => cfg.fieldName === 'Subscription') - .map(cfg => cfg.typeName); + const configuredSubscriptions = configuredResolvers.filter(cfg => cfg.fieldName === 'Subscription').map(cfg => cfg.typeName); const schema = buildASTSchema(doc); const subscriptionType = schema.getSubscriptionType(); if (subscriptionType) { diff --git a/packages/amplify-appsync-simulator/src/server/subscription.ts b/packages/amplify-appsync-simulator/src/server/subscription.ts index de5108ed40..7a24fb2470 100644 --- a/packages/amplify-appsync-simulator/src/server/subscription.ts +++ b/packages/amplify-appsync-simulator/src/server/subscription.ts @@ -32,10 +32,7 @@ export class SubscriptionServer { private port: number; private publishingTopics: Set; - constructor( - private config: AppSyncSimulatorServerConfig, - private appSyncServerContext: AmplifyAppSyncSimulator - ) { + constructor(private config: AppSyncSimulatorServerConfig, private appSyncServerContext: AmplifyAppSyncSimulator) { this.port = config.wsPort; this.webSocketServer = createHTTPServer(); @@ -106,11 +103,7 @@ export class SubscriptionServer { } if (!reg.isRegistered) { - const asyncIterator = await this.subscribeToGraphQL( - reg.documentAST, - reg.variables, - reg.context - ); + const asyncIterator = await this.subscribeToGraphQL(reg.documentAST, reg.variables, reg.context); if ((asyncIterator as ExecutionResult).errors) { log.error('Error(s) subscribing via graphql', (asyncIterator as ExecutionResult).errors); @@ -279,9 +272,7 @@ export class SubscriptionServer { return false; } // every variable key/value pair must match corresponding payload key/value pair - const variableResult = variableEntries.every( - ([variableKey, variableValue]) => payloadData[variableKey] === variableValue - ); + const variableResult = variableEntries.every(([variableKey, variableValue]) => payloadData[variableKey] === variableValue); if (!variableResult) { console.info('subscribe payload did not match variables', inspect(payload)); diff --git a/packages/amplify-appsync-simulator/src/type-definition.ts b/packages/amplify-appsync-simulator/src/type-definition.ts index f9105942a2..b134b42f6e 100644 --- a/packages/amplify-appsync-simulator/src/type-definition.ts +++ b/packages/amplify-appsync-simulator/src/type-definition.ts @@ -134,8 +134,8 @@ export type AppSyncSimulatorServerConfig = { }; export type AmplifyAppSyncSimulatorRequestContext = { - jwt?: object, - requestAuthorizationMode: AmplifyAppSyncSimulatorAuthenticationType, - request: Request, - appsyncErrors: {} -} \ No newline at end of file + jwt?: object; + requestAuthorizationMode: AmplifyAppSyncSimulatorAuthenticationType; + request: Request; + appsyncErrors: {}; +}; diff --git a/packages/amplify-appsync-simulator/src/velocity/index.ts b/packages/amplify-appsync-simulator/src/velocity/index.ts index 16e0db24c0..50aec30ff2 100644 --- a/packages/amplify-appsync-simulator/src/velocity/index.ts +++ b/packages/amplify-appsync-simulator/src/velocity/index.ts @@ -40,9 +40,7 @@ export class VelocityTemplate { } catch (e) { const lineDetails = `${e.hash.line}:${e.hash.loc.first_column}`; const fileName = template.path ? `${template.path}:${lineDetails}` : lineDetails; - const templateError = new VelocityTemplateParseError( - `Error:Parse error on ${fileName} \n${e.message}` - ); + const templateError = new VelocityTemplateParseError(`Error:Parse error on ${fileName} \n${e.message}`); templateError.stack = e.stack; throw templateError; } @@ -65,11 +63,7 @@ export class VelocityTemplate { } } - private buildRenderContext( - ctxValues: AppSyncVTLRenderContext, - requestContext: any, - info: GraphQLResolveInfo - ): any { + private buildRenderContext(ctxValues: AppSyncVTLRenderContext, requestContext: any, info: GraphQLResolveInfo): any { const { source, arguments: argument, result, stash, prevResult, error } = ctxValues; const { @@ -81,19 +75,13 @@ export class VelocityTemplate { const args = convertToJavaTypes(argument); // Identity is null for API Key let identity = null; - if ( - requestContext.requestAuthorizationMode === - AmplifyAppSyncSimulatorAuthenticationType.OPENID_CONNECT - ) { + if (requestContext.requestAuthorizationMode === AmplifyAppSyncSimulatorAuthenticationType.OPENID_CONNECT) { identity = convertToJavaTypes({ sub, issuer, claims: requestContext.jwt, }); - } else if ( - requestContext.requestAuthorizationMode === - AmplifyAppSyncSimulatorAuthenticationType.AMAZON_COGNITO_USER_POOLS - ) { + } else if (requestContext.requestAuthorizationMode === AmplifyAppSyncSimulatorAuthenticationType.AMAZON_COGNITO_USER_POOLS) { identity = convertToJavaTypes({ sub, issuer, diff --git a/packages/amplify-appsync-simulator/src/velocity/util/dynamodb-utils.ts b/packages/amplify-appsync-simulator/src/velocity/util/dynamodb-utils.ts index f1b2cd4efa..18b3802c08 100644 --- a/packages/amplify-appsync-simulator/src/velocity/util/dynamodb-utils.ts +++ b/packages/amplify-appsync-simulator/src/velocity/util/dynamodb-utils.ts @@ -81,7 +81,7 @@ export const dynamodbUtils = { ...sum, [key]: this.toDynamoDB(value), }), - {}, + {} ); }, toMapValuesJson(values) { diff --git a/packages/amplify-appsync-simulator/src/velocity/util/errors.ts b/packages/amplify-appsync-simulator/src/velocity/util/errors.ts index 6c171d9e1d..dd356347b6 100644 --- a/packages/amplify-appsync-simulator/src/velocity/util/errors.ts +++ b/packages/amplify-appsync-simulator/src/velocity/util/errors.ts @@ -2,18 +2,18 @@ import { GraphQLResolveInfo } from 'graphql'; export class TemplateSentError extends Error { extensions: any; - constructor(public message:string, public errorType:string , public data: any, public errorInfo: any, info: GraphQLResolveInfo) { + constructor(public message: string, public errorType: string, public data: any, public errorInfo: any, info: GraphQLResolveInfo) { super(message); const fieldName = info.fieldName; let path = info.path; - const pathArray = [] + const pathArray = []; do { pathArray.splice(0, 0, path.key); - path = path.prev - } while(path); + path = path.prev; + } while (path); const fieldNode = info.fieldNodes.find(f => f.name.value === fieldName); - const filedLocation = fieldNode && fieldNode.loc.startToken || null; + const filedLocation = (fieldNode && fieldNode.loc.startToken) || null; this.extensions = { message: message, errorType, @@ -21,11 +21,13 @@ export class TemplateSentError extends Error { errorInfo, path: pathArray, locations: [ - filedLocation ? { - line: filedLocation.line, - column: filedLocation.column, - sourceName: fieldNode.loc.source.name, - } : [], + filedLocation + ? { + line: filedLocation.line, + column: filedLocation.column, + sourceName: fieldNode.loc.source.name, + } + : [], ], }; } diff --git a/packages/amplify-appsync-simulator/src/velocity/util/map-utils.ts b/packages/amplify-appsync-simulator/src/velocity/util/map-utils.ts index ebe99f8c2b..dc739850f3 100644 --- a/packages/amplify-appsync-simulator/src/velocity/util/map-utils.ts +++ b/packages/amplify-appsync-simulator/src/velocity/util/map-utils.ts @@ -5,27 +5,33 @@ export const mapUtils = { copyAndRetainAllKeys(map: JavaMap, keys: JavaArray): JavaMap { const keyStr = keys.toJSON(); return mapper( - map.keySet().toJSON().reduce((sum, [key, val]) => { - if (keyStr.indexOf(key.toString()) === -1) return sum; - const valJSON = val && val.toJSON ? val.toJSON() : val; - return { - ...sum, - [key]: valJSON, - }; - }, {}), + map + .keySet() + .toJSON() + .reduce((sum, [key, val]) => { + if (keyStr.indexOf(key.toString()) === -1) return sum; + const valJSON = val && val.toJSON ? val.toJSON() : val; + return { + ...sum, + [key]: valJSON, + }; + }, {}) ); }, copyAndRemoveAllKeys(map: JavaMap, keys: JavaArray): JavaMap { const keysStr = keys.toJSON(); - const result = map.keySet().toJSON().reduce((acc, key) => { - key = key && key.toString && key.toString(); - if (!keysStr.includes(key)) { - const val = map.get(key); - const valJSON = val && val.toJSON ? val.toJSON() : val; - return { ...acc, [key]: valJSON }; - } - return acc; - }, {}); + const result = map + .keySet() + .toJSON() + .reduce((acc, key) => { + key = key && key.toString && key.toString(); + if (!keysStr.includes(key)) { + const val = map.get(key); + const valJSON = val && val.toJSON ? val.toJSON() : val; + return { ...acc, [key]: valJSON }; + } + return acc; + }, {}); return mapper(result); }, }; diff --git a/packages/amplify-appsync-simulator/src/velocity/util/time.ts b/packages/amplify-appsync-simulator/src/velocity/util/time.ts index eb14649045..58256efb8b 100644 --- a/packages/amplify-appsync-simulator/src/velocity/util/time.ts +++ b/packages/amplify-appsync-simulator/src/velocity/util/time.ts @@ -16,13 +16,11 @@ const parseTimestamp = (dateTime: string, format?: string, timezone?: string): m try { const momentFormatString = moment().toMomentFormatString(format); - return timezone - ? moment.tz(dateTime, momentFormatString, timezone) - : moment(dateTime, momentFormatString); + return timezone ? moment.tz(dateTime, momentFormatString, timezone) : moment(dateTime, momentFormatString); } catch (e) { return null; } -} +}; export const time = () => ({ nowISO8601(t): string { @@ -47,11 +45,7 @@ export const time = () => ({ return null; } }, - parseFormattedToEpochMilliSeconds( - dateTime: string, - format: string, - timezone?: string - ): number | null { + parseFormattedToEpochMilliSeconds(dateTime: string, format: string, timezone?: string): number | null { const timestamp = parseTimestamp(dateTime, format, timezone); return timestamp ? timestamp.valueOf() : null; }, @@ -73,11 +67,7 @@ export const time = () => ({ return null; } }, - epochMilliSecondsToFormatted( - timestamp: number, - format: string, - timezone: string = 'UTC' - ): string | null { + epochMilliSecondsToFormatted(timestamp: number, format: string, timezone: string = 'UTC'): string | null { try { return moment(timestamp) .tz(timezone) diff --git a/packages/amplify-appsync-simulator/src/velocity/util/transform/dynamodb-filter.ts b/packages/amplify-appsync-simulator/src/velocity/util/transform/dynamodb-filter.ts index 53b1a61467..3c85a33579 100644 --- a/packages/amplify-appsync-simulator/src/velocity/util/transform/dynamodb-filter.ts +++ b/packages/amplify-appsync-simulator/src/velocity/util/transform/dynamodb-filter.ts @@ -20,11 +20,7 @@ const FUNCTION_MAP = { beginsWith: 'begins_with', }; -export function generateFilterExpression( - filter: any, - prefix = null, - parent = null, -): DDBFilterExpression { +export function generateFilterExpression(filter: any, prefix = null, parent = null): DDBFilterExpression { const expr = Object.entries(filter).reduce( (sum, [name, value]) => { let subExpr = { @@ -42,24 +38,16 @@ export function generateFilterExpression( if (Array.isArray(value)) { subExpr = scopeExpression( value.reduce((expr, subFilter, idx) => { - const newExpr = generateFilterExpression( - subFilter, - [prefix, name, idx].filter(i => i !== null).join('_'), - ); + const newExpr = generateFilterExpression(subFilter, [prefix, name, idx].filter(i => i !== null).join('_')); return merge(expr, newExpr, JOINER); - }, subExpr), + }, subExpr) ); } else { - subExpr = generateFilterExpression( - value, - [prefix, name].filter(val => val !== null).join('_'), - ); + subExpr = generateFilterExpression(value, [prefix, name].filter(val => val !== null).join('_')); } break; case 'not': - subExpr = scopeExpression( - generateFilterExpression(value, [prefix, name].filter(val => val !== null).join('_')), - ); + subExpr = scopeExpression(generateFilterExpression(value, [prefix, name].filter(val => val !== null).join('_'))); subExpr.expressions.unshift('NOT'); break; case 'between': @@ -109,27 +97,19 @@ export function generateFilterExpression( expressions: [], expressionNames: {}, expressionValues: {}, - }, + } ); return expr; } -function merge( - expr1: DDBFilterExpression, - expr2: DDBFilterExpression, - joinCondition = 'AND', -): DDBFilterExpression { +function merge(expr1: DDBFilterExpression, expr2: DDBFilterExpression, joinCondition = 'AND'): DDBFilterExpression { if (!expr2.expressions.length) { return expr1; } return { - expressions: [ - ...expr1.expressions, - expr1.expressions.length ? joinCondition : '', - ...expr2.expressions, - ], + expressions: [...expr1.expressions, expr1.expressions.length ? joinCondition : '', ...expr2.expressions], expressionNames: { ...expr1.expressionNames, ...expr2.expressionNames }, expressionValues: { ...expr1.expressionValues, ...expr2.expressionValues }, }; diff --git a/packages/amplify-appsync-simulator/src/velocity/value-mapper/array.ts b/packages/amplify-appsync-simulator/src/velocity/value-mapper/array.ts index e625aca5fd..761d09ac14 100644 --- a/packages/amplify-appsync-simulator/src/velocity/value-mapper/array.ts +++ b/packages/amplify-appsync-simulator/src/velocity/value-mapper/array.ts @@ -3,7 +3,8 @@ import { toJSON } from './to-json'; export class JavaArray extends Array { private mapper: Function; constructor(values = [], mapper: Function) { - if(!Array.isArray(values)) { // splice sends a single object + if (!Array.isArray(values)) { + // splice sends a single object values = [values]; } super(...values); diff --git a/packages/amplify-appsync-simulator/src/velocity/value-mapper/map.ts b/packages/amplify-appsync-simulator/src/velocity/value-mapper/map.ts index 7c0c73540e..c193e0087a 100644 --- a/packages/amplify-appsync-simulator/src/velocity/value-mapper/map.ts +++ b/packages/amplify-appsync-simulator/src/velocity/value-mapper/map.ts @@ -10,7 +10,6 @@ export class JavaMap { Object.entries(obj).forEach(([key, value]) => { this.map.set(key, value); }); - } clear() { @@ -33,9 +32,9 @@ export class JavaMap { key, value, }, - this.mapper, - ), - ), + this.mapper + ) + ) ); return new JavaArray(entries, this.mapper); @@ -66,7 +65,7 @@ export class JavaMap { return saveValue; } - putAll(map: object| JavaMap) { + putAll(map: object | JavaMap) { map = toJSON(map); Object.entries(map).forEach(([key, value]) => { this.put(key, value); @@ -96,7 +95,7 @@ export class JavaMap { ...sum, [key]: toJSON(value), }), - {}, + {} ); } } diff --git a/packages/amplify-appsync-simulator/src/velocity/value-mapper/mapper.ts b/packages/amplify-appsync-simulator/src/velocity/value-mapper/mapper.ts index 896f2da322..58eb117ec9 100644 --- a/packages/amplify-appsync-simulator/src/velocity/value-mapper/mapper.ts +++ b/packages/amplify-appsync-simulator/src/velocity/value-mapper/mapper.ts @@ -5,7 +5,7 @@ import { isPlainObject } from 'lodash'; export function map(value: any) { if (value instanceof JavaMap) return value; - if(value instanceof JavaArray) return value; + if (value instanceof JavaArray) return value; if (Array.isArray(value)) { return new JavaArray(value.map(x => map(x)), map); } @@ -18,8 +18,8 @@ export function map(value: any) { [k]: map(v), }; }, {}), - map, - ), + map + ) ); } diff --git a/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CreateAuthChallenge/captcha-create-challenge.js b/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CreateAuthChallenge/captcha-create-challenge.js index 2bfb61e454..0191287ada 100644 --- a/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CreateAuthChallenge/captcha-create-challenge.js +++ b/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CreateAuthChallenge/captcha-create-challenge.js @@ -4,7 +4,7 @@ exports.handler = (event, context) => { if (event.request.session.length === 2 && event.request.challengeName === 'CUSTOM_CHALLENGE') { event.response.publicChallengeParameters = {}; - event.response.publicChallengeParameters = { 'trigger' : 'true' }; + event.response.publicChallengeParameters = { trigger: 'true' }; event.response.privateChallengeParameters = {}; event.response.privateChallengeParameters.answer = ''; event.response.challengeMetadata = 'CAPTCHA_CHALLENGE'; diff --git a/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/spinner.js b/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/spinner.js index eff9e64cad..b3ffe21ab1 100644 --- a/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/spinner.js +++ b/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/spinner.js @@ -1,15 +1,18 @@ /* eslint-disable */ -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { +var __assign = + (this && this.__assign) || + function() { + __assign = + Object.assign || + function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); }; - return __assign.apply(this, arguments); -}; var defaults = { lines: 12, length: 7, @@ -30,161 +33,158 @@ var defaults = { shadow: '0 0 1px transparent', position: 'absolute', }; -var Spinner = /** @class */ (function () { +var Spinner = /** @class */ (function() { function Spinner(opts) { - if (opts === void 0) { opts = {}; } - this.opts = __assign({}, defaults, opts); + if (opts === void 0) { + opts = {}; + } + this.opts = __assign({}, defaults, opts); } /** * Adds the spinner to the given target element. If this instance is already * spinning, it is automatically removed from its previous target by calling * stop() internally. */ - Spinner.prototype.spin = function (target) { - this.stop(); - this.el = document.createElement('div'); - this.el.className = this.opts.className; - this.el.setAttribute('role', 'progressbar'); - css(this.el, { - position: this.opts.position, - width: 0, - zIndex: this.opts.zIndex, - left: this.opts.left, - top: this.opts.top, - transform: "scale(" + this.opts.scale + ")", - }); - if (target) { - target.insertBefore(this.el, target.firstChild || null); - } - drawLines(this.el, this.opts); - return this; + Spinner.prototype.spin = function(target) { + this.stop(); + this.el = document.createElement('div'); + this.el.className = this.opts.className; + this.el.setAttribute('role', 'progressbar'); + css(this.el, { + position: this.opts.position, + width: 0, + zIndex: this.opts.zIndex, + left: this.opts.left, + top: this.opts.top, + transform: 'scale(' + this.opts.scale + ')', + }); + if (target) { + target.insertBefore(this.el, target.firstChild || null); + } + drawLines(this.el, this.opts); + return this; }; /** * Stops and removes the Spinner. * Stopped spinners may be reused by calling spin() again. */ - Spinner.prototype.stop = function () { - if (this.el) { - if (typeof requestAnimationFrame !== 'undefined') { - cancelAnimationFrame(this.animateId); - } - else { - clearTimeout(this.animateId); - } - if (this.el.parentNode) { - this.el.parentNode.removeChild(this.el); - } - this.el = undefined; + Spinner.prototype.stop = function() { + if (this.el) { + if (typeof requestAnimationFrame !== 'undefined') { + cancelAnimationFrame(this.animateId); + } else { + clearTimeout(this.animateId); } - return this; + if (this.el.parentNode) { + this.el.parentNode.removeChild(this.el); + } + this.el = undefined; + } + return this; }; return Spinner; -}()); +})(); /** -* Sets multiple style properties at once. -*/ + * Sets multiple style properties at once. + */ function css(el, props) { for (var prop in props) { - el.style[prop] = props[prop]; + el.style[prop] = props[prop]; } return el; } /** -* Returns the line color from the given string or array. -*/ + * Returns the line color from the given string or array. + */ function getColor(color, idx) { return typeof color == 'string' ? color : color[idx % color.length]; } /** -* Internal method that draws the individual lines. -*/ + * Internal method that draws the individual lines. + */ function drawLines(el, opts) { - var borderRadius = (Math.round(opts.corners * opts.width * 500) / 1000) + 'px'; + var borderRadius = Math.round(opts.corners * opts.width * 500) / 1000 + 'px'; var shadow = 'none'; if (opts.shadow === true) { - shadow = '0 2px 4px #000'; // default shadow - } - else if (typeof opts.shadow === 'string') { - shadow = opts.shadow; + shadow = '0 2px 4px #000'; // default shadow + } else if (typeof opts.shadow === 'string') { + shadow = opts.shadow; } var shadows = parseBoxShadow(shadow); for (var i = 0; i < opts.lines; i++) { - var degrees = ~~(360 / opts.lines * i + opts.rotate); - var backgroundLine = css(document.createElement('div'), { - position: 'absolute', - top: -opts.width / 2 + "px", - width: (opts.length + opts.width) + 'px', - height: opts.width + 'px', - background: getColor(opts.fadeColor, i), - borderRadius: borderRadius, - transformOrigin: 'left', - transform: "rotate(" + degrees + "deg) translateX(" + opts.radius + "px)", - }); - var delay = i * opts.direction / opts.lines / opts.speed; - delay -= 1 / opts.speed; // so initial animation state will include trail - var line = css(document.createElement('div'), { - width: '100%', - height: '100%', - background: getColor(opts.color, i), - borderRadius: borderRadius, - boxShadow: normalizeShadow(shadows, degrees), - animation: 1 / opts.speed + "s linear " + delay + "s infinite " + opts.animation, - }); - backgroundLine.appendChild(line); - el.appendChild(backgroundLine); + var degrees = ~~((360 / opts.lines) * i + opts.rotate); + var backgroundLine = css(document.createElement('div'), { + position: 'absolute', + top: -opts.width / 2 + 'px', + width: opts.length + opts.width + 'px', + height: opts.width + 'px', + background: getColor(opts.fadeColor, i), + borderRadius: borderRadius, + transformOrigin: 'left', + transform: 'rotate(' + degrees + 'deg) translateX(' + opts.radius + 'px)', + }); + var delay = (i * opts.direction) / opts.lines / opts.speed; + delay -= 1 / opts.speed; // so initial animation state will include trail + var line = css(document.createElement('div'), { + width: '100%', + height: '100%', + background: getColor(opts.color, i), + borderRadius: borderRadius, + boxShadow: normalizeShadow(shadows, degrees), + animation: 1 / opts.speed + 's linear ' + delay + 's infinite ' + opts.animation, + }); + backgroundLine.appendChild(line); + el.appendChild(backgroundLine); } } function parseBoxShadow(boxShadow) { var regex = /^\s*([a-zA-Z]+\s+)?(-?\d+(\.\d+)?)([a-zA-Z]*)\s+(-?\d+(\.\d+)?)([a-zA-Z]*)(.*)$/; var shadows = []; for (var _i = 0, _a = boxShadow.split(','); _i < _a.length; _i++) { - var shadow = _a[_i]; - var matches = shadow.match(regex); - if (matches === null) { - continue; // invalid syntax - } - var x = +matches[2]; - var y = +matches[5]; - var xUnits = matches[4]; - var yUnits = matches[7]; - if (x === 0 && !xUnits) { - xUnits = yUnits; - } - if (y === 0 && !yUnits) { - yUnits = xUnits; - } - if (xUnits !== yUnits) { - continue; // units must match to use as coordinates - } - shadows.push({ - prefix: matches[1] || '', - x: x, - y: y, - xUnits: xUnits, - yUnits: yUnits, - end: matches[8], - }); + var shadow = _a[_i]; + var matches = shadow.match(regex); + if (matches === null) { + continue; // invalid syntax + } + var x = +matches[2]; + var y = +matches[5]; + var xUnits = matches[4]; + var yUnits = matches[7]; + if (x === 0 && !xUnits) { + xUnits = yUnits; + } + if (y === 0 && !yUnits) { + yUnits = xUnits; + } + if (xUnits !== yUnits) { + continue; // units must match to use as coordinates + } + shadows.push({ + prefix: matches[1] || '', + x: x, + y: y, + xUnits: xUnits, + yUnits: yUnits, + end: matches[8], + }); } return shadows; } /** -* Modify box-shadow x/y offsets to counteract rotation -*/ + * Modify box-shadow x/y offsets to counteract rotation + */ function normalizeShadow(shadows, degrees) { var normalized = []; for (var _i = 0, shadows_1 = shadows; _i < shadows_1.length; _i++) { - var shadow = shadows_1[_i]; - var xy = convertOffset(shadow.x, shadow.y, degrees); - normalized.push(shadow.prefix + xy[0] + shadow.xUnits + ' ' + xy[1] + shadow.yUnits + shadow.end); + var shadow = shadows_1[_i]; + var xy = convertOffset(shadow.x, shadow.y, degrees); + normalized.push(shadow.prefix + xy[0] + shadow.xUnits + ' ' + xy[1] + shadow.yUnits + shadow.end); } return normalized.join(', '); } function convertOffset(x, y, degrees) { - var radians = degrees * Math.PI / 180; + var radians = (degrees * Math.PI) / 180; var sin = Math.sin(radians); var cos = Math.cos(radians); - return [ - Math.round((x * cos + y * sin) * 1000) / 1000, - Math.round((-x * sin + y * cos) * 1000) / 1000, - ]; -} \ No newline at end of file + return [Math.round((x * cos + y * sin) * 1000) / 1000, Math.round((-x * sin + y * cos) * 1000) / 1000]; +} diff --git a/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/verify.js b/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/verify.js index 120a768638..8463ff05f4 100644 --- a/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/verify.js +++ b/packages/amplify-category-auth/provider-utils/awscloudformation/triggers/CustomMessage/assets/verify.js @@ -1,6 +1,5 @@ /* eslint-disable */ - var opts = { lines: 13, // The number of lines to draw length: 38, // The length of each line @@ -19,43 +18,36 @@ var opts = { top: '50%', // Top position relative to parent left: '50%', // Left position relative to parent shadow: '0 0 1px transparent', // Box-shadow for the lines - position: 'absolute' // Element positioning + position: 'absolute', // Element positioning }; var target = document.getElementById('myspinner'); var spinner = new Spinner().spin(target); - function confirm() { const urlParams = new URLSearchParams(window.location.search); const encoded = urlParams.get('data'); const code = urlParams.get('code'); - const decoded = JSON.parse(atob(encoded)) - const { - userName, - redirectUrl, - clientId, - region - } = decoded; + const decoded = JSON.parse(atob(encoded)); + const { userName, redirectUrl, clientId, region } = decoded; var params = { - ClientId: clientId, - ConfirmationCode: code, + ClientId: clientId, + ConfirmationCode: code, Username: userName, }; - AWS.config.region = region; + AWS.config.region = region; var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider(); cognitoidentityserviceprovider.confirmSignUp(params, function(err, data) { if (err) { - if (err.message === "User cannot be confirm. Current status is CONFIRMED") { + if (err.message === 'User cannot be confirm. Current status is CONFIRMED') { window.location.replace(redirectUrl); } } else { window.location.replace(redirectUrl); } }); - -} \ No newline at end of file +} diff --git a/packages/amplify-category-hosting/__mocks__/mockAwsProviderModule.js b/packages/amplify-category-hosting/__mocks__/mockAwsProviderModule.js index 73ad439912..614b1de99c 100644 --- a/packages/amplify-category-hosting/__mocks__/mockAwsProviderModule.js +++ b/packages/amplify-category-hosting/__mocks__/mockAwsProviderModule.js @@ -22,16 +22,16 @@ class S3 { class IAM { createPolicy() { return { - promise: () => Promise.resolve({ - Policy: {}, - }), + promise: () => + Promise.resolve({ + Policy: {}, + }), }; } attachRolePolicy() { return { - promise: () => Promise.resolve({ - }), + promise: () => Promise.resolve({}), }; } } @@ -43,27 +43,30 @@ class Pinpoint { getApp() { return { - promise: () => Promise.resolve({ - ApplicationResponse: {}, - }), + promise: () => + Promise.resolve({ + ApplicationResponse: {}, + }), }; } deleteApp(params) { return { - promise: () => Promise.resolve({ - ApplicationResponse: { - Id: params.ApplicationId, - }, - }), + promise: () => + Promise.resolve({ + ApplicationResponse: { + Id: params.ApplicationId, + }, + }), }; } createApp() { return { - promise: () => Promise.resolve({ - ApplicationResponse: {}, - }), + promise: () => + Promise.resolve({ + ApplicationResponse: {}, + }), }; } } diff --git a/packages/amplify-category-hosting/__mocks__/ora.js b/packages/amplify-category-hosting/__mocks__/ora.js index aba0ba74e2..0fd98dbb4f 100644 --- a/packages/amplify-category-hosting/__mocks__/ora.js +++ b/packages/amplify-category-hosting/__mocks__/ora.js @@ -1,13 +1,9 @@ function ora() { return { - start: () => { - }, - stop: () => { - }, - succeed: () => { - }, - fail: () => { - }, + start: () => {}, + stop: () => {}, + succeed: () => {}, + fail: () => {}, }; } diff --git a/packages/amplify-category-hosting/__tests__/index.test.js b/packages/amplify-category-hosting/__tests__/index.test.js index 07c4825413..032d754716 100644 --- a/packages/amplify-category-hosting/__tests__/index.test.js +++ b/packages/amplify-category-hosting/__tests__/index.test.js @@ -8,177 +8,181 @@ const categoryManager = require('../lib/category-manager'); const indexModule = require('../index'); describe('index', () => { - const mockContext = { - print: { - info: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - success: jest.fn() - } - }; - const S3ANDCLOUDFRONT = 'S3AndCloudFront'; - const ANOTHERSERVICE = 'AnotherHostingService'; - - const mockAvailableServices = []; - let mockDisabledServices = []; - let mockEnabledServices = []; - - const mockAnswers = { - selectedServices: [] - }; - - beforeAll(() => { - categoryManager.getCategoryStatus = jest.fn(()=>{ - return { - availableServices: mockAvailableServices, - enabledServices: mockEnabledServices, - disabledServices: mockDisabledServices - } - }); - }); - - beforeEach(() => { - mockAvailableServices.length = 0; - mockDisabledServices.length = 0; - mockEnabledServices.length = 0; - mockAnswers.selectedServices.length = 0; - mockAnswers.selectedService = undefined; - categoryManager.runServiceAction.mockClear(); - }); - - test('add', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockAvailableServices.push(ANOTHERSERVICE); - mockDisabledServices.push(S3ANDCLOUDFRONT); - mockDisabledServices.push(ANOTHERSERVICE); - mockAnswers.selectedServices.push(S3ANDCLOUDFRONT); - mockirer(inquirer, mockAnswers); - await indexModule.add(mockContext); - expect(categoryManager.runServiceAction).toBeCalled(); - }); - - test('add, only one disabled serivce', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockDisabledServices.push(S3ANDCLOUDFRONT); - await indexModule.add(mockContext); - expect(categoryManager.runServiceAction).toBeCalled(); - expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); - expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); - expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('enable'); - }); - - test('add, no disabled service', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockEnabledServices.push(S3ANDCLOUDFRONT); - - await expect(indexModule.add(mockContext)).rejects.toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('add, no available service', async () => { - await expect(indexModule.add(mockContext)).rejects.toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('configure', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockAvailableServices.push(ANOTHERSERVICE); - mockEnabledServices.push(S3ANDCLOUDFRONT); - mockEnabledServices.push(ANOTHERSERVICE); - mockAnswers.selectedServices.push(S3ANDCLOUDFRONT); - mockirer(inquirer, mockAnswers); - await indexModule.configure(mockContext); - expect(categoryManager.runServiceAction).toBeCalled(); - }); - - test('configure, only one enabled serivce', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockEnabledServices.push(S3ANDCLOUDFRONT); - await indexModule.configure(mockContext); - expect(categoryManager.runServiceAction).toBeCalled(); - expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); - expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); - expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('configure'); - }); - - test('configure, no enabled service', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - - await expect(indexModule.configure(mockContext)).rejects.toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('configure, no available service', async () => { - await expect(indexModule.configure(mockContext)).rejects.toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('publish', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockEnabledServices.push(S3ANDCLOUDFRONT); - await indexModule.publish(mockContext, S3ANDCLOUDFRONT); - expect(categoryManager.runServiceAction).toBeCalled(); - expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); - expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); - expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('publish'); - }); - - test('publish, expected service no enabled', () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockAvailableServices.push(ANOTHERSERVICE); - mockEnabledServices.push(ANOTHERSERVICE); - mockDisabledServices.push(S3ANDCLOUDFRONT); - - expect(()=>{indexModule.publish(mockContext, S3ANDCLOUDFRONT)}).toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('publish, no enabled service', () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - - expect(()=>{indexModule.publish(mockContext, S3ANDCLOUDFRONT)}).toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('console', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockAvailableServices.push(ANOTHERSERVICE); - mockEnabledServices.push(S3ANDCLOUDFRONT); - mockEnabledServices.push(ANOTHERSERVICE); - mockAnswers.selectedService = mockEnabledServices[0]; - mockirer(inquirer, mockAnswers); - await indexModule.console(mockContext); - expect(categoryManager.runServiceAction).toBeCalled(); - expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); - expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(mockEnabledServices[0]); - expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('console'); - }); - - test('console, only one enabled serivce', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockEnabledServices.push(S3ANDCLOUDFRONT); - await indexModule.console(mockContext); - expect(categoryManager.runServiceAction).toBeCalled(); - expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); - expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); - expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('console'); - }); - - test('console, no enabled service', async () => { - mockAvailableServices.push(S3ANDCLOUDFRONT); - mockDisabledServices.push(S3ANDCLOUDFRONT); - - await expect(indexModule.console(mockContext)).rejects.toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('console, no available service', async () => { - await expect(indexModule.console(mockContext)).rejects.toThrow(); - expect(categoryManager.runServiceAction).not.toBeCalled(); - }); - - test('migrate', async () => { - await indexModule.migrate(mockContext); - expect(categoryManager.migrate).toBeCalledWith(mockContext); - }); -}) \ No newline at end of file + const mockContext = { + print: { + info: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, + }; + const S3ANDCLOUDFRONT = 'S3AndCloudFront'; + const ANOTHERSERVICE = 'AnotherHostingService'; + + const mockAvailableServices = []; + let mockDisabledServices = []; + let mockEnabledServices = []; + + const mockAnswers = { + selectedServices: [], + }; + + beforeAll(() => { + categoryManager.getCategoryStatus = jest.fn(() => { + return { + availableServices: mockAvailableServices, + enabledServices: mockEnabledServices, + disabledServices: mockDisabledServices, + }; + }); + }); + + beforeEach(() => { + mockAvailableServices.length = 0; + mockDisabledServices.length = 0; + mockEnabledServices.length = 0; + mockAnswers.selectedServices.length = 0; + mockAnswers.selectedService = undefined; + categoryManager.runServiceAction.mockClear(); + }); + + test('add', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockAvailableServices.push(ANOTHERSERVICE); + mockDisabledServices.push(S3ANDCLOUDFRONT); + mockDisabledServices.push(ANOTHERSERVICE); + mockAnswers.selectedServices.push(S3ANDCLOUDFRONT); + mockirer(inquirer, mockAnswers); + await indexModule.add(mockContext); + expect(categoryManager.runServiceAction).toBeCalled(); + }); + + test('add, only one disabled serivce', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockDisabledServices.push(S3ANDCLOUDFRONT); + await indexModule.add(mockContext); + expect(categoryManager.runServiceAction).toBeCalled(); + expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); + expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); + expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('enable'); + }); + + test('add, no disabled service', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockEnabledServices.push(S3ANDCLOUDFRONT); + + await expect(indexModule.add(mockContext)).rejects.toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('add, no available service', async () => { + await expect(indexModule.add(mockContext)).rejects.toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('configure', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockAvailableServices.push(ANOTHERSERVICE); + mockEnabledServices.push(S3ANDCLOUDFRONT); + mockEnabledServices.push(ANOTHERSERVICE); + mockAnswers.selectedServices.push(S3ANDCLOUDFRONT); + mockirer(inquirer, mockAnswers); + await indexModule.configure(mockContext); + expect(categoryManager.runServiceAction).toBeCalled(); + }); + + test('configure, only one enabled serivce', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockEnabledServices.push(S3ANDCLOUDFRONT); + await indexModule.configure(mockContext); + expect(categoryManager.runServiceAction).toBeCalled(); + expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); + expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); + expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('configure'); + }); + + test('configure, no enabled service', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + + await expect(indexModule.configure(mockContext)).rejects.toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('configure, no available service', async () => { + await expect(indexModule.configure(mockContext)).rejects.toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('publish', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockEnabledServices.push(S3ANDCLOUDFRONT); + await indexModule.publish(mockContext, S3ANDCLOUDFRONT); + expect(categoryManager.runServiceAction).toBeCalled(); + expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); + expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); + expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('publish'); + }); + + test('publish, expected service no enabled', () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockAvailableServices.push(ANOTHERSERVICE); + mockEnabledServices.push(ANOTHERSERVICE); + mockDisabledServices.push(S3ANDCLOUDFRONT); + + expect(() => { + indexModule.publish(mockContext, S3ANDCLOUDFRONT); + }).toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('publish, no enabled service', () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + + expect(() => { + indexModule.publish(mockContext, S3ANDCLOUDFRONT); + }).toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('console', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockAvailableServices.push(ANOTHERSERVICE); + mockEnabledServices.push(S3ANDCLOUDFRONT); + mockEnabledServices.push(ANOTHERSERVICE); + mockAnswers.selectedService = mockEnabledServices[0]; + mockirer(inquirer, mockAnswers); + await indexModule.console(mockContext); + expect(categoryManager.runServiceAction).toBeCalled(); + expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); + expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(mockEnabledServices[0]); + expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('console'); + }); + + test('console, only one enabled serivce', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockEnabledServices.push(S3ANDCLOUDFRONT); + await indexModule.console(mockContext); + expect(categoryManager.runServiceAction).toBeCalled(); + expect(categoryManager.runServiceAction.mock.calls[0][0]).toBe(mockContext); + expect(categoryManager.runServiceAction.mock.calls[0][1]).toBe(S3ANDCLOUDFRONT); + expect(categoryManager.runServiceAction.mock.calls[0][2]).toBe('console'); + }); + + test('console, no enabled service', async () => { + mockAvailableServices.push(S3ANDCLOUDFRONT); + mockDisabledServices.push(S3ANDCLOUDFRONT); + + await expect(indexModule.console(mockContext)).rejects.toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('console, no available service', async () => { + await expect(indexModule.console(mockContext)).rejects.toThrow(); + expect(categoryManager.runServiceAction).not.toBeCalled(); + }); + + test('migrate', async () => { + await indexModule.migrate(mockContext); + expect(categoryManager.migrate).toBeCalledWith(mockContext); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/configuration-manager.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/configuration-manager.test.js index 05b57d618a..f8cb6ad718 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/configuration-manager.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/configuration-manager.test.js @@ -2,93 +2,91 @@ jest.mock('inquirer'); const inquirer = require('inquirer'); jest.mock('../../../lib/S3AndCloudFront/helpers/configure-Website'); -const configureWebsite = require('../../../lib/S3AndCloudFront/helpers/configure-Website'); +const configureWebsite = require('../../../lib/S3AndCloudFront/helpers/configure-Website'); jest.mock('../../../lib/S3AndCloudFront/helpers/configure-CloudFront'); -const configureCloudFront = require('../../../lib/S3AndCloudFront/helpers/configure-CloudFront'); +const configureCloudFront = require('../../../lib/S3AndCloudFront/helpers/configure-CloudFront'); jest.mock('../../../lib/S3AndCloudFront/helpers/configure-Publish'); -const configurePublish = require('../../../lib/S3AndCloudFront/helpers/configure-Publish'); +const configurePublish = require('../../../lib/S3AndCloudFront/helpers/configure-Publish'); -const configurationManager = require('../../../lib/S3AndCloudFront/configuration-manager'); +const configurationManager = require('../../../lib/S3AndCloudFront/configuration-manager'); describe('configuration-manager', () => { - const mockAmplifyMeta = { - "providers": { - "awscloudformation": { - "AuthRoleName": "checkhosting-20190226163640-authRole", - "UnauthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole", - "AuthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole", - "Region": "us-west-2", - "DeploymentBucketName": "checkhosting-20190226163640-deployment", - "UnauthRoleName": "checkhosting-20190226163640-unauthRole", - "StackName": "checkhosting-20190226163640", - "StackId": "arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8" - } + const mockAmplifyMeta = { + providers: { + awscloudformation: { + AuthRoleName: 'checkhosting-20190226163640-authRole', + UnauthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole', + AuthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole', + Region: 'us-west-2', + DeploymentBucketName: 'checkhosting-20190226163640-deployment', + UnauthRoleName: 'checkhosting-20190226163640-unauthRole', + StackName: 'checkhosting-20190226163640', + StackId: 'arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8', + }, + }, + hosting: { + S3AndCloudFront: { + service: 'S3AndCloudFront', + providerPlugin: 'awscloudformation', + providerMetadata: { + s3TemplateURL: 'https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json', + logicalId: 'hostingS3AndCloudFront', }, - "hosting": { - "S3AndCloudFront": { - "service": "S3AndCloudFront", - "providerPlugin": "awscloudformation", - "providerMetadata": { - "s3TemplateURL": "https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json", - "logicalId": "hostingS3AndCloudFront" - }, - "lastPushTimeStamp": "2019-02-27T00:39:17.966Z", - "output": { - "S3BucketSecureURL": "https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com", - "WebsiteURL": "http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com", - "Region": "us-west-2", - "HostingBucketName": "checkosting-20190226163802-hostingbucket-dev" - }, - "lastPushDirHash": "83Bhmmec48dILMj3mi2T25B4700=" - } - } - } - - const mockProjectConfig = { - "projectName": "mockProjectName", - "version": "2.0", - "frontend": "javascript", - "javascript": { - "framework": "none", - "config": { - "SourceDir": "src", - "DistributionDir": "dist", - "BuildCommand": "npm run-script build", - "StartCommand": "npm run-script start" - } + lastPushTimeStamp: '2019-02-27T00:39:17.966Z', + output: { + S3BucketSecureURL: 'https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com', + WebsiteURL: 'http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com', + Region: 'us-west-2', + HostingBucketName: 'checkosting-20190226163802-hostingbucket-dev', }, - "providers": [ - "awscloudformation" - ] - }; + lastPushDirHash: '83Bhmmec48dILMj3mi2T25B4700=', + }, + }, + }; + + const mockProjectConfig = { + projectName: 'mockProjectName', + version: '2.0', + frontend: 'javascript', + javascript: { + framework: 'none', + config: { + SourceDir: 'src', + DistributionDir: 'dist', + BuildCommand: 'npm run-script build', + StartCommand: 'npm run-script start', + }, + }, + providers: ['awscloudformation'], + }; - const mockContext = { - exeInfo: { - parameters: {}, - projectConfig: mockProjectConfig, - serviceMeta: mockAmplifyMeta.hosting.S3AndCloudFront - } - }; + const mockContext = { + exeInfo: { + parameters: {}, + projectConfig: mockProjectConfig, + serviceMeta: mockAmplifyMeta.hosting.S3AndCloudFront, + }, + }; - beforeEach(() => { - jest.resetAllMocks(); - }); + beforeEach(() => { + jest.resetAllMocks(); + }); - test('init', async () => { - inquirer.prompt.mockResolvedValueOnce({HostingBucketName: 'mockHostingBucketName'}); - await configurationManager.init(mockContext); - expect(mockContext.exeInfo.parameters.bucketName).toBeDefined(); - expect(configureWebsite.configure).toBeCalledWith(mockContext); - }); + test('init', async () => { + inquirer.prompt.mockResolvedValueOnce({ HostingBucketName: 'mockHostingBucketName' }); + await configurationManager.init(mockContext); + expect(mockContext.exeInfo.parameters.bucketName).toBeDefined(); + expect(configureWebsite.configure).toBeCalledWith(mockContext); + }); - test('configure', async () => { - inquirer.prompt.mockResolvedValueOnce({section: 'Website'}); - inquirer.prompt.mockResolvedValueOnce({section: 'CloudFront'}); - inquirer.prompt.mockResolvedValueOnce({section: 'Publish'}); - inquirer.prompt.mockResolvedValueOnce({section: 'exit'}); - await configurationManager.configure(mockContext); - expect(configureWebsite.configure).toBeCalledWith(mockContext); - expect(configureCloudFront.configure).toBeCalledWith(mockContext); - expect(configurePublish.configure).toBeCalledWith(mockContext); - }); -}) \ No newline at end of file + test('configure', async () => { + inquirer.prompt.mockResolvedValueOnce({ section: 'Website' }); + inquirer.prompt.mockResolvedValueOnce({ section: 'CloudFront' }); + inquirer.prompt.mockResolvedValueOnce({ section: 'Publish' }); + inquirer.prompt.mockResolvedValueOnce({ section: 'exit' }); + await configurationManager.configure(mockContext); + expect(configureWebsite.configure).toBeCalledWith(mockContext); + expect(configureCloudFront.configure).toBeCalledWith(mockContext); + expect(configurePublish.configure).toBeCalledWith(mockContext); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/cloudfront-manager.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/cloudfront-manager.test.js index 42d99eb9f6..75305479b5 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/cloudfront-manager.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/cloudfront-manager.test.js @@ -1,92 +1,91 @@ const mockAwsProviderModule = require('../../../../__mocks__/mockAwsProviderModule'); -const cloudFrontManager = require('../../../../lib/S3AndCloudFront/helpers/cloudfront-manager'); +const cloudFrontManager = require('../../../../lib/S3AndCloudFront/helpers/cloudfront-manager'); describe('cloudfront-manager', () => { - const mockAmplifyMeta = { - "providers": { - "awscloudformation": { - "AuthRoleName": "checkhosting-20190226163640-authRole", - "UnauthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole", - "AuthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole", - "Region": "us-west-2", - "DeploymentBucketName": "checkhosting-20190226163640-deployment", - "UnauthRoleName": "checkhosting-20190226163640-unauthRole", - "StackName": "checkhosting-20190226163640", - "StackId": "arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8" - } + const mockAmplifyMeta = { + providers: { + awscloudformation: { + AuthRoleName: 'checkhosting-20190226163640-authRole', + UnauthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole', + AuthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole', + Region: 'us-west-2', + DeploymentBucketName: 'checkhosting-20190226163640-deployment', + UnauthRoleName: 'checkhosting-20190226163640-unauthRole', + StackName: 'checkhosting-20190226163640', + StackId: 'arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8', + }, + }, + hosting: { + S3AndCloudFront: { + service: 'S3AndCloudFront', + providerPlugin: 'awscloudformation', + providerMetadata: { + s3TemplateURL: 'https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json', + logicalId: 'hostingS3AndCloudFront', }, - "hosting": { - "S3AndCloudFront": { - "service": "S3AndCloudFront", - "providerPlugin": "awscloudformation", - "providerMetadata": { - "s3TemplateURL": "https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json", - "logicalId": "hostingS3AndCloudFront" - }, - "lastPushTimeStamp": "2019-02-27T00:39:17.966Z", - "output": { - "S3BucketSecureURL": "https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com", - "WebsiteURL": "http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com", - "Region": "us-west-2", - "HostingBucketName": "checkosting-20190226163802-hostingbucket-dev", - "CloudFrontDistributionID": "mockCloudFrontDistributionID", - "CloudFrontSecureURL": "mockCloudFrontSecureURL" - - }, - "lastPushDirHash": "83Bhmmec48dILMj3mi2T25B4700=" - } - } - } - - const mockContext = { - amplify: { - getProviderPlugins: jest.fn(()=>{ - return { - "awscloudformation": "mockAwsProviderModule" - }; - }) + lastPushTimeStamp: '2019-02-27T00:39:17.966Z', + output: { + S3BucketSecureURL: 'https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com', + WebsiteURL: 'http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com', + Region: 'us-west-2', + HostingBucketName: 'checkosting-20190226163802-hostingbucket-dev', + CloudFrontDistributionID: 'mockCloudFrontDistributionID', + CloudFrontSecureURL: 'mockCloudFrontSecureURL', }, - print: { - info: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - success: jest.fn() - }, - parameters: { - options: { - invalidateCache: true - } - }, - exeInfo: { - serviceMeta: mockAmplifyMeta.hosting.S3AndCloudFront - } - }; - - const mockcftInvalidationData = {}; + lastPushDirHash: '83Bhmmec48dILMj3mi2T25B4700=', + }, + }, + }; - const mockInvalidateMethod = jest.fn(()=>{ + const mockContext = { + amplify: { + getProviderPlugins: jest.fn(() => { return { - promise: () => Promise.resolve(mockcftInvalidationData), + awscloudformation: 'mockAwsProviderModule', }; - }); - - class mockCloudFront { - constructor(){ - this.createInvalidation = mockInvalidateMethod; - } - } + }), + }, + print: { + info: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, + parameters: { + options: { + invalidateCache: true, + }, + }, + exeInfo: { + serviceMeta: mockAmplifyMeta.hosting.S3AndCloudFront, + }, + }; - mockAwsProviderModule.getConfiguredAWSClient = ()=>{ - return { - CloudFront: mockCloudFront - }; + const mockcftInvalidationData = {}; + + const mockInvalidateMethod = jest.fn(() => { + return { + promise: () => Promise.resolve(mockcftInvalidationData), + }; + }); + + class mockCloudFront { + constructor() { + this.createInvalidation = mockInvalidateMethod; } + } + + mockAwsProviderModule.getConfiguredAWSClient = () => { + return { + CloudFront: mockCloudFront, + }; + }; - test('invalidateCloudFront', async () => { - const result = await cloudFrontManager.invalidateCloudFront(mockContext); - expect(result).toBe(mockContext); - expect(mockInvalidateMethod).toBeCalled(); - expect(mockContext.exeInfo.cftInvalidationData).toEqual(mockcftInvalidationData); - }); -}) \ No newline at end of file + test('invalidateCloudFront', async () => { + const result = await cloudFrontManager.invalidateCloudFront(mockContext); + expect(result).toBe(mockContext); + expect(mockInvalidateMethod).toBeCalled(); + expect(mockContext.exeInfo.cftInvalidationData).toEqual(mockcftInvalidationData); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-CloudFront.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-CloudFront.test.js index 23b101e352..ba86c9193e 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-CloudFront.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-CloudFront.test.js @@ -1,287 +1,279 @@ -const fs = require('fs-extra'); -const path = require('path'); +const fs = require('fs-extra'); +const path = require('path'); -jest.mock('inquirer'); +jest.mock('inquirer'); const inquirer = require('inquirer'); -const configureCloudFront = require('../../../../lib/S3AndCloudFront/helpers/configure-CloudFront'); +const configureCloudFront = require('../../../../lib/S3AndCloudFront/helpers/configure-CloudFront'); -describe('configure-CloudFront', ()=>{ - const configActions = { - list: 'list', - add: 'add', - edit: 'edit', - remove: 'remove', - removeAll: 'remove all', - done: 'exit' - }; - const mockContext = { - exeInfo: { - }, - amplify: { - pathManager: { - searchProjectRootPath: jest.fn(()=>{ - return 'mockProjectRootDirPath'; - }) - }, - readJsonFile: (jsonFilePath)=>{ - let content = fs.readFileSync(jsonFilePath, 'utf8') - if (content.charCodeAt(0) === 0xFEFF) { - content = content.slice(1); - } - return JSON.parse(content); - } - }, - print: { - info: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - success: jest.fn() +describe('configure-CloudFront', () => { + const configActions = { + list: 'list', + add: 'add', + edit: 'edit', + remove: 'remove', + removeAll: 'remove all', + done: 'exit', + }; + const mockContext = { + exeInfo: {}, + amplify: { + pathManager: { + searchProjectRootPath: jest.fn(() => { + return 'mockProjectRootDirPath'; + }), + }, + readJsonFile: jsonFilePath => { + let content = fs.readFileSync(jsonFilePath, 'utf8'); + if (content.charCodeAt(0) === 0xfeff) { + content = content.slice(1); } - }; - - const mockDefaultRootObject = 'mockIndex.html'; - const mockDefaultCacheDefaultTTL = 111222; - const mockDefaultCacheMaxTTL = 11122233; - const mockDefaultCacheMinTTL = 11; - - beforeEach(()=>{ - jest.resetAllMocks(); - }); + return JSON.parse(content); + }, + }, + print: { + info: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, + }; - test('configure: default values', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - inquirer.prompt.mockResolvedValueOnce({ - RemoveCloudFront: false - }); - inquirer.prompt.mockResolvedValueOnce({ - DefaultRootObject: mockDefaultRootObject, - DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, - DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, - DefaultCacheMinTTL: mockDefaultCacheMinTTL, - ConfigCustomError: false - }); - const result = await configureCloudFront.configure(mockContext); - const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; - expect(result).toEqual(mockContext); - expect(mockContext.exeInfo.template.Resources.OriginAccessIdentity).toBeDefined(); - expect(mockContext.exeInfo.template.Resources.CloudFrontDistribution).toBeDefined(); - expect(mockContext.exeInfo.template.Resources.BucketPolicy).not.toBeDefined(); - expect(mockContext.exeInfo.template.Resources.PrivateBucketPolicy).toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontDistributionID).toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontDomainName).toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontSecureURL).toBeDefined(); - expect(mockContext.exeInfo.template.Resources.S3Bucket.Properties.AccessControl).not.toBeDefined(); - expect(DistributionConfig.DefaultRootObject).toEqual(mockDefaultRootObject); - expect(DistributionConfig.DefaultCacheBehavior.DefaultTTL).toEqual(mockDefaultCacheDefaultTTL); - expect(DistributionConfig.DefaultCacheBehavior.MaxTTL).toEqual(mockDefaultCacheMaxTTL); - expect(DistributionConfig.DefaultCacheBehavior.MinTTL).toEqual(mockDefaultCacheMinTTL); - }); + const mockDefaultRootObject = 'mockIndex.html'; + const mockDefaultCacheDefaultTTL = 111222; + const mockDefaultCacheMaxTTL = 11122233; + const mockDefaultCacheMinTTL = 11; - test('configure: remove cloudfront', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - inquirer.prompt.mockResolvedValueOnce({ - RemoveCloudFront: true - }); - inquirer.prompt.mockResolvedValueOnce({ - IndexDocument: 'index.html', - ErrorDocument: 'error.html' - }); - let result = await configureCloudFront.configure(mockContext); - expect(result).toEqual(mockContext); - expect(mockContext.exeInfo.template.Resources.OriginAccessIdentity).not.toBeDefined(); - expect(mockContext.exeInfo.template.Resources.CloudFrontDistribution).not.toBeDefined(); - expect(mockContext.exeInfo.template.Resources.BucketPolicy).not.toBeDefined(); - expect(mockContext.exeInfo.template.Resources.PrivateBucketPolicy).not.toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontDistributionID).not.toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontDomainName).not.toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontSecureURL).not.toBeDefined(); - expect(mockContext.exeInfo.template.Resources.S3Bucket.Properties.AccessControl).toBeDefined(); - }); + beforeEach(() => { + jest.resetAllMocks(); + }); - test('configure: add cloudfront', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate-noCloudFront.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - inquirer.prompt.mockResolvedValueOnce({ - AddCloudFront: true - }); - inquirer.prompt.mockResolvedValueOnce({ - DefaultRootObject: mockDefaultRootObject, - DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, - DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, - DefaultCacheMinTTL: mockDefaultCacheMinTTL, - ConfigCustomError: false - }); - result = await configureCloudFront.configure(mockContext); - const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; - expect(result).toEqual(mockContext); - expect(mockContext.exeInfo.template.Resources.OriginAccessIdentity).toBeDefined(); - expect(mockContext.exeInfo.template.Resources.CloudFrontDistribution).toBeDefined(); - expect(mockContext.exeInfo.template.Resources.PrivateBucketPolicy).toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontDistributionID).toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontDomainName).toBeDefined(); - expect(mockContext.exeInfo.template.Outputs.CloudFrontSecureURL).toBeDefined(); - expect(mockContext.exeInfo.template.Resources.BucketPolicy).not.toBeDefined(); - expect(mockContext.exeInfo.template.Resources.S3Bucket.Properties.AccessControl).not.toBeDefined(); - expect(DistributionConfig.DefaultRootObject).toEqual(mockDefaultRootObject); - expect(DistributionConfig.DefaultCacheBehavior.DefaultTTL).toEqual(mockDefaultCacheDefaultTTL); - expect(DistributionConfig.DefaultCacheBehavior.MaxTTL).toEqual(mockDefaultCacheMaxTTL); - expect(DistributionConfig.DefaultCacheBehavior.MinTTL).toEqual(mockDefaultCacheMinTTL); - }); + test('configure: default values', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + inquirer.prompt.mockResolvedValueOnce({ + RemoveCloudFront: false, + }); + inquirer.prompt.mockResolvedValueOnce({ + DefaultRootObject: mockDefaultRootObject, + DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, + DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, + DefaultCacheMinTTL: mockDefaultCacheMinTTL, + ConfigCustomError: false, + }); + const result = await configureCloudFront.configure(mockContext); + const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; + expect(result).toEqual(mockContext); + expect(mockContext.exeInfo.template.Resources.OriginAccessIdentity).toBeDefined(); + expect(mockContext.exeInfo.template.Resources.CloudFrontDistribution).toBeDefined(); + expect(mockContext.exeInfo.template.Resources.BucketPolicy).not.toBeDefined(); + expect(mockContext.exeInfo.template.Resources.PrivateBucketPolicy).toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontDistributionID).toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontDomainName).toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontSecureURL).toBeDefined(); + expect(mockContext.exeInfo.template.Resources.S3Bucket.Properties.AccessControl).not.toBeDefined(); + expect(DistributionConfig.DefaultRootObject).toEqual(mockDefaultRootObject); + expect(DistributionConfig.DefaultCacheBehavior.DefaultTTL).toEqual(mockDefaultCacheDefaultTTL); + expect(DistributionConfig.DefaultCacheBehavior.MaxTTL).toEqual(mockDefaultCacheMaxTTL); + expect(DistributionConfig.DefaultCacheBehavior.MinTTL).toEqual(mockDefaultCacheMinTTL); + }); - test('configure: list', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - inquirer.prompt.mockResolvedValueOnce({ - RemoveCloudFront: false - }); - inquirer.prompt.mockResolvedValueOnce({ - DefaultRootObject: mockDefaultRootObject, - DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, - DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, - DefaultCacheMinTTL: mockDefaultCacheMinTTL, - ConfigCustomError: true - }); - inquirer.prompt.mockResolvedValueOnce({action: configActions.list}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.done}); - let result = await configureCloudFront.configure(mockContext); - expect(result).toEqual(mockContext); - expect(mockContext.print.info).toBeCalled(); + test('configure: remove cloudfront', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + inquirer.prompt.mockResolvedValueOnce({ + RemoveCloudFront: true, + }); + inquirer.prompt.mockResolvedValueOnce({ + IndexDocument: 'index.html', + ErrorDocument: 'error.html', }); + let result = await configureCloudFront.configure(mockContext); + expect(result).toEqual(mockContext); + expect(mockContext.exeInfo.template.Resources.OriginAccessIdentity).not.toBeDefined(); + expect(mockContext.exeInfo.template.Resources.CloudFrontDistribution).not.toBeDefined(); + expect(mockContext.exeInfo.template.Resources.BucketPolicy).not.toBeDefined(); + expect(mockContext.exeInfo.template.Resources.PrivateBucketPolicy).not.toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontDistributionID).not.toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontDomainName).not.toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontSecureURL).not.toBeDefined(); + expect(mockContext.exeInfo.template.Resources.S3Bucket.Properties.AccessControl).toBeDefined(); + }); - test('configure: add', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - const mockErrorCode = - { - ErrorCode: 500 - }; - const mockCustomErrorResponses = { - ResponseCode: 200, - ResponsePagePath: "/", - ErrorCachingMinTTL: 300 - }; - inquirer.prompt.mockResolvedValueOnce({ - RemoveCloudFront: false - }); - inquirer.prompt.mockResolvedValueOnce({ - DefaultRootObject: mockDefaultRootObject, - DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, - DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, - DefaultCacheMinTTL: mockDefaultCacheMinTTL, - ConfigCustomError: true - }); - inquirer.prompt.mockResolvedValueOnce({action: configActions.add}); - inquirer.prompt.mockResolvedValueOnce(mockErrorCode); - inquirer.prompt.mockResolvedValueOnce(mockCustomErrorResponses); - inquirer.prompt.mockResolvedValueOnce({action: configActions.done}); - const { DistributionConfig } = - mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; - let result = await configureCloudFront.configure(mockContext); - expect(result).toEqual(mockContext); - expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); - expect(DistributionConfig.CustomErrorResponses).toContainEqual({ - ...mockErrorCode, - ...mockCustomErrorResponses - }); + test('configure: add cloudfront', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate-noCloudFront.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + inquirer.prompt.mockResolvedValueOnce({ + AddCloudFront: true, }); + inquirer.prompt.mockResolvedValueOnce({ + DefaultRootObject: mockDefaultRootObject, + DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, + DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, + DefaultCacheMinTTL: mockDefaultCacheMinTTL, + ConfigCustomError: false, + }); + result = await configureCloudFront.configure(mockContext); + const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; + expect(result).toEqual(mockContext); + expect(mockContext.exeInfo.template.Resources.OriginAccessIdentity).toBeDefined(); + expect(mockContext.exeInfo.template.Resources.CloudFrontDistribution).toBeDefined(); + expect(mockContext.exeInfo.template.Resources.PrivateBucketPolicy).toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontDistributionID).toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontDomainName).toBeDefined(); + expect(mockContext.exeInfo.template.Outputs.CloudFrontSecureURL).toBeDefined(); + expect(mockContext.exeInfo.template.Resources.BucketPolicy).not.toBeDefined(); + expect(mockContext.exeInfo.template.Resources.S3Bucket.Properties.AccessControl).not.toBeDefined(); + expect(DistributionConfig.DefaultRootObject).toEqual(mockDefaultRootObject); + expect(DistributionConfig.DefaultCacheBehavior.DefaultTTL).toEqual(mockDefaultCacheDefaultTTL); + expect(DistributionConfig.DefaultCacheBehavior.MaxTTL).toEqual(mockDefaultCacheMaxTTL); + expect(DistributionConfig.DefaultCacheBehavior.MinTTL).toEqual(mockDefaultCacheMinTTL); + }); + test('configure: list', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + inquirer.prompt.mockResolvedValueOnce({ + RemoveCloudFront: false, + }); + inquirer.prompt.mockResolvedValueOnce({ + DefaultRootObject: mockDefaultRootObject, + DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, + DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, + DefaultCacheMinTTL: mockDefaultCacheMinTTL, + ConfigCustomError: true, + }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.list }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.done }); + let result = await configureCloudFront.configure(mockContext); + expect(result).toEqual(mockContext); + expect(mockContext.print.info).toBeCalled(); + }); - test('configure: edit', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - const { DistributionConfig } = - mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; - const mockErrorCode = { - ErrorCode: DistributionConfig.CustomErrorResponses[0].ErrorCode - }; - const mockCustomErrorResponses = { - ResponseCode: 200, - ResponsePagePath: "/mockPack", - ErrorCachingMinTTL: 333 - }; - inquirer.prompt.mockResolvedValueOnce({ - RemoveCloudFront: false - }); - inquirer.prompt.mockResolvedValueOnce({ - DefaultRootObject: mockDefaultRootObject, - DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, - DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, - DefaultCacheMinTTL: mockDefaultCacheMinTTL, - ConfigCustomError: true - }); - inquirer.prompt.mockResolvedValueOnce({action: configActions.edit}); - inquirer.prompt.mockResolvedValueOnce(mockErrorCode); - inquirer.prompt.mockResolvedValueOnce(mockCustomErrorResponses); - inquirer.prompt.mockResolvedValueOnce({action: configActions.done}); - let result = await configureCloudFront.configure(mockContext); - expect(result).toEqual(mockContext); - expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); - expect(DistributionConfig.CustomErrorResponses).toContainEqual({ - ...mockErrorCode, - ...mockCustomErrorResponses - }); + test('configure: add', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + const mockErrorCode = { + ErrorCode: 500, + }; + const mockCustomErrorResponses = { + ResponseCode: 200, + ResponsePagePath: '/', + ErrorCachingMinTTL: 300, + }; + inquirer.prompt.mockResolvedValueOnce({ + RemoveCloudFront: false, }); + inquirer.prompt.mockResolvedValueOnce({ + DefaultRootObject: mockDefaultRootObject, + DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, + DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, + DefaultCacheMinTTL: mockDefaultCacheMinTTL, + ConfigCustomError: true, + }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.add }); + inquirer.prompt.mockResolvedValueOnce(mockErrorCode); + inquirer.prompt.mockResolvedValueOnce(mockCustomErrorResponses); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.done }); + const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; + let result = await configureCloudFront.configure(mockContext); + expect(result).toEqual(mockContext); + expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); + expect(DistributionConfig.CustomErrorResponses).toContainEqual({ + ...mockErrorCode, + ...mockCustomErrorResponses, + }); + }); - test('configure: remove', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - const { DistributionConfig } = - mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; - const mockCustomReponseToRemove = DistributionConfig.CustomErrorResponses[0]; - const mockErrorCode = { - ErrorCode: mockCustomReponseToRemove.ErrorCode - }; - inquirer.prompt.mockResolvedValueOnce({ - RemoveCloudFront: false - }); - inquirer.prompt.mockResolvedValueOnce({ - DefaultRootObject: mockDefaultRootObject, - DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, - DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, - DefaultCacheMinTTL: mockDefaultCacheMinTTL, - ConfigCustomError: true - }); - inquirer.prompt.mockResolvedValueOnce({action: configActions.remove}); - inquirer.prompt.mockResolvedValueOnce(mockErrorCode); - inquirer.prompt.mockResolvedValueOnce({action: configActions.done}); - let result = await configureCloudFront.configure(mockContext); - expect(result).toEqual(mockContext); - expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); - expect(DistributionConfig.CustomErrorResponses).not.toContainEqual(mockCustomReponseToRemove); + test('configure: edit', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; + const mockErrorCode = { + ErrorCode: DistributionConfig.CustomErrorResponses[0].ErrorCode, + }; + const mockCustomErrorResponses = { + ResponseCode: 200, + ResponsePagePath: '/mockPack', + ErrorCachingMinTTL: 333, + }; + inquirer.prompt.mockResolvedValueOnce({ + RemoveCloudFront: false, + }); + inquirer.prompt.mockResolvedValueOnce({ + DefaultRootObject: mockDefaultRootObject, + DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, + DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, + DefaultCacheMinTTL: mockDefaultCacheMinTTL, + ConfigCustomError: true, }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.edit }); + inquirer.prompt.mockResolvedValueOnce(mockErrorCode); + inquirer.prompt.mockResolvedValueOnce(mockCustomErrorResponses); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.done }); + let result = await configureCloudFront.configure(mockContext); + expect(result).toEqual(mockContext); + expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); + expect(DistributionConfig.CustomErrorResponses).toContainEqual({ + ...mockErrorCode, + ...mockCustomErrorResponses, + }); + }); + test('configure: remove', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; + const mockCustomReponseToRemove = DistributionConfig.CustomErrorResponses[0]; + const mockErrorCode = { + ErrorCode: mockCustomReponseToRemove.ErrorCode, + }; + inquirer.prompt.mockResolvedValueOnce({ + RemoveCloudFront: false, + }); + inquirer.prompt.mockResolvedValueOnce({ + DefaultRootObject: mockDefaultRootObject, + DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, + DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, + DefaultCacheMinTTL: mockDefaultCacheMinTTL, + ConfigCustomError: true, + }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.remove }); + inquirer.prompt.mockResolvedValueOnce(mockErrorCode); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.done }); + let result = await configureCloudFront.configure(mockContext); + expect(result).toEqual(mockContext); + expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); + expect(DistributionConfig.CustomErrorResponses).not.toContainEqual(mockCustomReponseToRemove); + }); - test('configure: customError remove all', async ()=>{ - const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); - const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); - mockContext.exeInfo.template = mockTemplate; - inquirer.prompt.mockResolvedValueOnce({ - RemoveCloudFront: false - }); - inquirer.prompt.mockResolvedValueOnce({ - DefaultRootObject: mockDefaultRootObject, - DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, - DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, - DefaultCacheMinTTL: mockDefaultCacheMinTTL, - ConfigCustomError: true - }); - inquirer.prompt.mockResolvedValueOnce({action: configActions.removeAll}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.done}); - const { DistributionConfig } = - mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; - let result = await configureCloudFront.configure(mockContext); - expect(result).toEqual(mockContext); - expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); - expect(DistributionConfig.CustomErrorResponses).toHaveLength(0); + test('configure: customError remove all', async () => { + const mockTemplatePath = path.join(__dirname, '../../../../__mocks__/mockTemplate.json'); + const mockTemplate = mockContext.amplify.readJsonFile(mockTemplatePath); + mockContext.exeInfo.template = mockTemplate; + inquirer.prompt.mockResolvedValueOnce({ + RemoveCloudFront: false, + }); + inquirer.prompt.mockResolvedValueOnce({ + DefaultRootObject: mockDefaultRootObject, + DefaultCacheDefaultTTL: mockDefaultCacheDefaultTTL, + DefaultCacheMaxTTL: mockDefaultCacheMaxTTL, + DefaultCacheMinTTL: mockDefaultCacheMinTTL, + ConfigCustomError: true, }); -}); \ No newline at end of file + inquirer.prompt.mockResolvedValueOnce({ action: configActions.removeAll }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.done }); + const { DistributionConfig } = mockContext.exeInfo.template.Resources.CloudFrontDistribution.Properties; + let result = await configureCloudFront.configure(mockContext); + expect(result).toEqual(mockContext); + expect(Array.isArray(DistributionConfig.CustomErrorResponses)).toBeTruthy(); + expect(DistributionConfig.CustomErrorResponses).toHaveLength(0); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Publish.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Publish.test.js index d15fb2a95b..0fc1e56d58 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Publish.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Publish.test.js @@ -1,96 +1,96 @@ jest.mock('inquirer'); -const fs = require('fs-extra'); -const path = require('path'); +const fs = require('fs-extra'); +const path = require('path'); const inquirer = require('inquirer'); -const configurePublish = require('../../../../lib/S3AndCloudFront/helpers/configure-Publish'); +const configurePublish = require('../../../../lib/S3AndCloudFront/helpers/configure-Publish'); -describe('configure-Publish', ()=>{ - const DONE = 'exit'; - const configActions = { - list: 'list', - add: 'add', - remove: 'remove', - removeAll: 'remove all', - done: DONE - }; - const mockContext = { - amplify: { - pathManager: { - searchProjectRootPath: jest.fn(()=>{ - return path.join(__dirname, '../../../../__mocks__/'); - }) - }, - readJsonFile: (jsonFilePath)=>{ - let content = fs.readFileSync(jsonFilePath, 'utf8') - if (content.charCodeAt(0) === 0xFEFF) { - content = content.slice(1); - } - return JSON.parse(content); - } - }, - print: { - info: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - success: jest.fn() +describe('configure-Publish', () => { + const DONE = 'exit'; + const configActions = { + list: 'list', + add: 'add', + remove: 'remove', + removeAll: 'remove all', + done: DONE, + }; + const mockContext = { + amplify: { + pathManager: { + searchProjectRootPath: jest.fn(() => { + return path.join(__dirname, '../../../../__mocks__/'); + }), + }, + readJsonFile: jsonFilePath => { + let content = fs.readFileSync(jsonFilePath, 'utf8'); + if (content.charCodeAt(0) === 0xfeff) { + content = content.slice(1); } - }; + return JSON.parse(content); + }, + }, + print: { + info: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, + }; - beforeAll(()=>{ - fs.existsSync = jest.fn(()=>{return true;}) - fs.writeFileSync = jest.fn(); - }); + beforeAll(() => { + fs.existsSync = jest.fn(() => { + return true; + }); + fs.writeFileSync = jest.fn(); + }); - beforeEach(()=>{ - inquirer.prompt.mockClear(); - fs.existsSync.mockClear(); - fs.writeFileSync.mockClear(); - }); + beforeEach(() => { + inquirer.prompt.mockClear(); + fs.existsSync.mockClear(); + fs.writeFileSync.mockClear(); + }); - test('configure, flow1', async ()=>{ - inquirer.prompt.mockResolvedValueOnce({action: configActions.list}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.add}); - inquirer.prompt.mockResolvedValueOnce({patternToAdd: 'mockPattern1'}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.add}); - inquirer.prompt.mockResolvedValueOnce({patternToAdd: 'mockPattern2'}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.remove}); - inquirer.prompt.mockResolvedValueOnce({patternToRemove: 'mockPattern1'}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.done}); - const result = await configurePublish.configure(mockContext); - expect(mockContext.print.info).toBeCalled(); - expect(fs.writeFileSync).toBeCalled(); - expect(Array.isArray(result)).toBeTruthy(); - expect(result).not.toContain('mockPattern1'); - expect(result).toContain('mockPattern2'); - }); + test('configure, flow1', async () => { + inquirer.prompt.mockResolvedValueOnce({ action: configActions.list }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.add }); + inquirer.prompt.mockResolvedValueOnce({ patternToAdd: 'mockPattern1' }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.add }); + inquirer.prompt.mockResolvedValueOnce({ patternToAdd: 'mockPattern2' }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.remove }); + inquirer.prompt.mockResolvedValueOnce({ patternToRemove: 'mockPattern1' }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.done }); + const result = await configurePublish.configure(mockContext); + expect(mockContext.print.info).toBeCalled(); + expect(fs.writeFileSync).toBeCalled(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).not.toContain('mockPattern1'); + expect(result).toContain('mockPattern2'); + }); - test('configure, flow2', async ()=>{ - inquirer.prompt.mockResolvedValueOnce({action: configActions.add}); - inquirer.prompt.mockResolvedValueOnce({patternToAdd: 'mockPattern1'}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.add}); - inquirer.prompt.mockResolvedValueOnce({patternToAdd: 'mockPattern2'}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.removeAll}); - inquirer.prompt.mockResolvedValueOnce({action: configActions.done}); - const result = await configurePublish.configure(mockContext); - expect(mockContext.print.info).toBeCalled(); - expect(fs.writeFileSync).toBeCalled(); - expect(Array.isArray(result)).toBeTruthy(); - expect(result).toHaveLength(0); - }); + test('configure, flow2', async () => { + inquirer.prompt.mockResolvedValueOnce({ action: configActions.add }); + inquirer.prompt.mockResolvedValueOnce({ patternToAdd: 'mockPattern1' }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.add }); + inquirer.prompt.mockResolvedValueOnce({ patternToAdd: 'mockPattern2' }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.removeAll }); + inquirer.prompt.mockResolvedValueOnce({ action: configActions.done }); + const result = await configurePublish.configure(mockContext); + expect(mockContext.print.info).toBeCalled(); + expect(fs.writeFileSync).toBeCalled(); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toHaveLength(0); + }); - test('getIgnore', async ()=>{ - const actual = mockContext.amplify.readJsonFile( - path.join(__dirname, '../../../../__mocks__/amplifyPublishIgnore.json') - ); - const result = await configurePublish.getIgnore(mockContext); - expect(Array.isArray(result)).toBeTruthy(); - expect(result).toEqual(actual); - }); + test('getIgnore', async () => { + const actual = mockContext.amplify.readJsonFile(path.join(__dirname, '../../../../__mocks__/amplifyPublishIgnore.json')); + const result = await configurePublish.getIgnore(mockContext); + expect(Array.isArray(result)).toBeTruthy(); + expect(result).toEqual(actual); + }); - test('isIgnored', async ()=>{ - const result = configurePublish.isIgnored('dist/ignoredFile', ['ignoredFile'], 'dist'); - expect(typeof(result)).toEqual('boolean'); - expect(result).toEqual(true); - }); -}); \ No newline at end of file + test('isIgnored', async () => { + const result = configurePublish.isIgnored('dist/ignoredFile', ['ignoredFile'], 'dist'); + expect(typeof result).toEqual('boolean'); + expect(result).toEqual(true); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Website.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Website.test.js index c2083f0d1f..0f3803adf7 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Website.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/configure-Website.test.js @@ -1,34 +1,34 @@ const inquirer = require('inquirer'); -const mockirer = require('mockirer'); +const mockirer = require('mockirer'); const mockTemplate = require('../../../../__mocks__/mockTemplate-noCloudFront'); -const configureWebsite = require('../../../../lib/S3AndCloudFront/helpers/configure-Website'); +const configureWebsite = require('../../../../lib/S3AndCloudFront/helpers/configure-Website'); -describe('configure-Website', ()=>{ - const mockContext = { - exeInfo: { - template: mockTemplate - }, - print: { - warning: jest.fn() - } - }; +describe('configure-Website', () => { + const mockContext = { + exeInfo: { + template: mockTemplate, + }, + print: { + warning: jest.fn(), + }, + }; - const indexDoc = 'index.html'; - const errorDoc = 'error.html'; - beforeAll(()=>{ - mockirer(inquirer, { - IndexDocument: indexDoc, - ErrorDocument: errorDoc - }) - }); + const indexDoc = 'index.html'; + const errorDoc = 'error.html'; + beforeAll(() => { + mockirer(inquirer, { + IndexDocument: indexDoc, + ErrorDocument: errorDoc, + }); + }); - test('configure', async ()=>{ - const { WebsiteConfiguration } = mockContext.exeInfo.template.Resources.S3Bucket.Properties; - const result = await configureWebsite.configure(mockContext); - expect(result).toEqual(mockContext); - expect(WebsiteConfiguration.IndexDocument).toEqual(indexDoc.trim()); - expect(WebsiteConfiguration.ErrorDocument).toEqual(errorDoc.trim()); - }); -}); \ No newline at end of file + test('configure', async () => { + const { WebsiteConfiguration } = mockContext.exeInfo.template.Resources.S3Bucket.Properties; + const result = await configureWebsite.configure(mockContext); + expect(result).toEqual(mockContext); + expect(WebsiteConfiguration.IndexDocument).toEqual(indexDoc.trim()); + expect(WebsiteConfiguration.ErrorDocument).toEqual(errorDoc.trim()); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-scanner.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-scanner.test.js index 4b2e9434f9..73297f261f 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-scanner.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-scanner.test.js @@ -2,155 +2,175 @@ const fs = require('fs-extra'); const path = require('path'); const publishConfig = require('../../../../lib/S3AndCloudFront/helpers/configure-Publish'); -const fileScanner = require('../../../../lib/S3AndCloudFront/helpers/file-scanner'); +const fileScanner = require('../../../../lib/S3AndCloudFront/helpers/file-scanner'); describe('file-scanner', () => { - const mockDistDirPath = 'dist'; - const mockIndexDocName = 'index.html'; - const mockIndexDocPath = path.join(mockDistDirPath, mockIndexDocName); - const otherFileName = 'otherfile'; - - const mockContext = { - print: { - info: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - success: jest.fn() - } - }; - - beforeAll(() => { - publishConfig.isIgnored = jest.fn(()=>{return false;}); - publishConfig.getIgnore = jest.fn(()=>{return [];}); - fs.existsSync = jest.fn(); - fs.readdirSync = jest.fn(); - fs.statSync = jest.fn(); - }); - - beforeEach(() => { - fs.existsSync.mockClear(); - fs.readdirSync.mockClear(); - fs.statSync.mockClear(); + const mockDistDirPath = 'dist'; + const mockIndexDocName = 'index.html'; + const mockIndexDocPath = path.join(mockDistDirPath, mockIndexDocName); + const otherFileName = 'otherfile'; + + const mockContext = { + print: { + info: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, + }; + + beforeAll(() => { + publishConfig.isIgnored = jest.fn(() => { + return false; }); - - test('scan, happy route', async () => { - fs.existsSync = jest.fn((itemPath)=>{ - let result = false; - if(itemPath === mockDistDirPath){ - result = true; - }else if(itemPath === mockIndexDocPath){ - result = true; - } - return result; - }); - fs.readdirSync = jest.fn(()=>{ return [mockIndexDocName]}); - fs.statSync = jest.fn(()=>{ - return { - isDirectory: ()=>{return false;} - } - }); - - let result; - let err; - - try{ - result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); - }catch(e){ - err = e; - } - - expect(result).toBeDefined(); - expect(Array.isArray(result)).toBeTruthy(); - expect(err).not.toBeDefined(); + publishConfig.getIgnore = jest.fn(() => { + return []; + }); + fs.existsSync = jest.fn(); + fs.readdirSync = jest.fn(); + fs.statSync = jest.fn(); + }); + + beforeEach(() => { + fs.existsSync.mockClear(); + fs.readdirSync.mockClear(); + fs.statSync.mockClear(); + }); + + test('scan, happy route', async () => { + fs.existsSync = jest.fn(itemPath => { + let result = false; + if (itemPath === mockDistDirPath) { + result = true; + } else if (itemPath === mockIndexDocPath) { + result = true; + } + return result; + }); + fs.readdirSync = jest.fn(() => { + return [mockIndexDocName]; + }); + fs.statSync = jest.fn(() => { + return { + isDirectory: () => { + return false; + }, + }; }); - test('scan, dist dir empty', async () => { - fs.existsSync = jest.fn((itemPath)=>{ - let result = false; - if(itemPath === mockDistDirPath){ - result = true; - }else if(itemPath === mockIndexDocPath){ - result = false; - } - return result; - }); - fs.readdirSync = jest.fn(()=>{ return []}); - fs.statSync = jest.fn(()=>{ - return { - isDirectory: ()=>{return false;} - } - }); - - let result; - let err; - - try{ - result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); - }catch(e){ - err = e; - } - - expect(result).not.toBeDefined(); - expect(err).toBeDefined(); + let result; + let err; + + try { + result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); + } catch (e) { + err = e; + } + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBeTruthy(); + expect(err).not.toBeDefined(); + }); + + test('scan, dist dir empty', async () => { + fs.existsSync = jest.fn(itemPath => { + let result = false; + if (itemPath === mockDistDirPath) { + result = true; + } else if (itemPath === mockIndexDocPath) { + result = false; + } + return result; + }); + fs.readdirSync = jest.fn(() => { + return []; + }); + fs.statSync = jest.fn(() => { + return { + isDirectory: () => { + return false; + }, + }; }); - test('scan, dist dir does not exist', async () => { - fs.existsSync = jest.fn((itemPath)=>{ - let result = false; - if(itemPath === mockDistDirPath){ - result = false; - }else if(itemPath === mockIndexDocPath){ - result = false; - } - return result; - }); - fs.readdirSync = jest.fn(()=>{ return []}); - fs.statSync = jest.fn(()=>{ - return { - isDirectory: ()=>{return false;} - } - }); - - let result; - let err; - - try{ - result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); - }catch(e){ - err = e; - } - - expect(result).not.toBeDefined(); - expect(err).toBeDefined(); + let result; + let err; + + try { + result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); + } catch (e) { + err = e; + } + + expect(result).not.toBeDefined(); + expect(err).toBeDefined(); + }); + + test('scan, dist dir does not exist', async () => { + fs.existsSync = jest.fn(itemPath => { + let result = false; + if (itemPath === mockDistDirPath) { + result = false; + } else if (itemPath === mockIndexDocPath) { + result = false; + } + return result; + }); + fs.readdirSync = jest.fn(() => { + return []; + }); + fs.statSync = jest.fn(() => { + return { + isDirectory: () => { + return false; + }, + }; }); - test('scan, index doc does not exist', async () => { - fs.existsSync = jest.fn((itemPath)=>{ - let result = false; - if(itemPath === mockDistDirPath){ - result = false; - }else if(itemPath === mockIndexDocPath){ - result = false; - } - return result; - }); - fs.readdirSync = jest.fn(()=>{ return [otherFileName]}); - fs.statSync = jest.fn(()=>{ - return { - isDirectory: ()=>{return false;} - } - }); - - let result; - let err; - - try{ - result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); - }catch(e){ - err = e; - } - - expect(result).not.toBeDefined(); - expect(err).toBeDefined(); + let result; + let err; + + try { + result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); + } catch (e) { + err = e; + } + + expect(result).not.toBeDefined(); + expect(err).toBeDefined(); + }); + + test('scan, index doc does not exist', async () => { + fs.existsSync = jest.fn(itemPath => { + let result = false; + if (itemPath === mockDistDirPath) { + result = false; + } else if (itemPath === mockIndexDocPath) { + result = false; + } + return result; + }); + fs.readdirSync = jest.fn(() => { + return [otherFileName]; + }); + fs.statSync = jest.fn(() => { + return { + isDirectory: () => { + return false; + }, + }; }); -}) \ No newline at end of file + + let result; + let err; + + try { + result = fileScanner.scan(mockContext, mockDistDirPath, mockIndexDocName); + } catch (e) { + err = e; + } + + expect(result).not.toBeDefined(); + expect(err).toBeDefined(); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-uploader.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-uploader.test.js index 6c92f846a1..d60d89cbb4 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-uploader.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/file-uploader.test.js @@ -1,13 +1,10 @@ jest.mock('mime-types'); -jest.mock('../../../../lib/S3AndCloudFront/helpers/file-scanner', ()=>{ - return { - scan: jest.fn(()=>{ - return [ - 'filePath1', - 'filePath2' - ] - }) - } +jest.mock('../../../../lib/S3AndCloudFront/helpers/file-scanner', () => { + return { + scan: jest.fn(() => { + return ['filePath1', 'filePath2']; + }), + }; }); const fs = require('fs-extra'); @@ -18,85 +15,84 @@ const fileScanner = require('../../../../lib/S3AndCloudFront/helpers/file-scanne const mockTemplate = require('../../../../__mocks__/mockTemplate'); const mockParameters = require('../../../../__mocks__/mockParameters'); -const fileUploader = require('../../../../lib/S3AndCloudFront/helpers/file-uploader'); +const fileUploader = require('../../../../lib/S3AndCloudFront/helpers/file-uploader'); describe('cloudfront-manager', () => { - const mockAmplifyMeta = { - "providers": { - "awscloudformation": { - "AuthRoleName": "checkhosting-20190228131446-authRole", - "UnauthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190228131446-unauthRole", - "AuthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190228131446-authRole", - "Region": "us-west-2", - "DeploymentBucketName": "checkhosting-20190228131446-deployment", - "UnauthRoleName": "checkhosting-20190228131446-unauthRole", - "StackName": "checkhosting-20190228131446", - "StackId": "arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190228131446/52623470-3b9e-11e9-a03a-0ad6ed005066" - } + const mockAmplifyMeta = { + providers: { + awscloudformation: { + AuthRoleName: 'checkhosting-20190228131446-authRole', + UnauthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190228131446-unauthRole', + AuthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190228131446-authRole', + Region: 'us-west-2', + DeploymentBucketName: 'checkhosting-20190228131446-deployment', + UnauthRoleName: 'checkhosting-20190228131446-unauthRole', + StackName: 'checkhosting-20190228131446', + StackId: 'arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190228131446/52623470-3b9e-11e9-a03a-0ad6ed005066', + }, + }, + hosting: { + S3AndCloudFront: { + service: 'S3AndCloudFront', + providerPlugin: 'awscloudformation', + providerMetadata: { + s3TemplateURL: 'https://s3.amazonaws.com/checkhosting-20190228131446-deployment/amplify-cfn-templates/hosting/template.json', + logicalId: 'hostingS3AndCloudFront', }, - "hosting": { - "S3AndCloudFront": { - "service": "S3AndCloudFront", - "providerPlugin": "awscloudformation", - "providerMetadata": { - "s3TemplateURL": "https://s3.amazonaws.com/checkhosting-20190228131446-deployment/amplify-cfn-templates/hosting/template.json", - "logicalId": "hostingS3AndCloudFront" - }, - "lastPushTimeStamp": "2019-02-28T22:35:50.043Z", - "output": { - "S3BucketSecureURL": "https://checkhosting-20190228131606-hostingbucket-dev.s3.amazonaws.com", - "WebsiteURL": "http://checkhosting-20190228131606-hostingbucket-dev.s3-website-us-west-2.amazonaws.com", - "Region": "us-west-2", - "HostingBucketName": "checkhosting-20190228131606-hostingbucket-dev", - "CloudFrontSecureURL": "https://mockdomain.cloudfront.net", - "CloudFrontDistributionID": "MOCKDISID01V", - "CloudFrontDomainName": "mockdomain.cloudfront.net" - }, - "lastPushDirHash": "MockHashehogmJPW8UwL5Q1JZlI=" - } - } - } - - const mockContext = { - amplify: { - getProviderPlugins: jest.fn(()=>{ - return { - "awscloudformation": "mockAwsProviderModule" - }; - }) - }, - print: { - info: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - success: jest.fn() + lastPushTimeStamp: '2019-02-28T22:35:50.043Z', + output: { + S3BucketSecureURL: 'https://checkhosting-20190228131606-hostingbucket-dev.s3.amazonaws.com', + WebsiteURL: 'http://checkhosting-20190228131606-hostingbucket-dev.s3-website-us-west-2.amazonaws.com', + Region: 'us-west-2', + HostingBucketName: 'checkhosting-20190228131606-hostingbucket-dev', + CloudFrontSecureURL: 'https://mockdomain.cloudfront.net', + CloudFrontDistributionID: 'MOCKDISID01V', + CloudFrontDomainName: 'mockdomain.cloudfront.net', }, - parameters: { - options: { - invalidateCache: true - } - }, - exeInfo: { - template: mockTemplate, - parameters: mockParameters, - amplifyMeta: mockAmplifyMeta, - } - }; + lastPushDirHash: 'MockHashehogmJPW8UwL5Q1JZlI=', + }, + }, + }; - beforeAll(() => { - fs.createReadStream = jest.fn(()=>{ - return {}; - }); - mime.lookup = jest.fn(()=>{ - return 'text/plain'; - }); - }); + const mockContext = { + amplify: { + getProviderPlugins: jest.fn(() => { + return { + awscloudformation: 'mockAwsProviderModule', + }; + }), + }, + print: { + info: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, + parameters: { + options: { + invalidateCache: true, + }, + }, + exeInfo: { + template: mockTemplate, + parameters: mockParameters, + amplifyMeta: mockAmplifyMeta, + }, + }; - beforeEach(() => { + beforeAll(() => { + fs.createReadStream = jest.fn(() => { + return {}; }); - - test('run', async () => { - await fileUploader.run(mockContext, 'mockDistributionFolder'); - expect(fileScanner.scan).toBeCalled(); + mime.lookup = jest.fn(() => { + return 'text/plain'; }); -}) \ No newline at end of file + }); + + beforeEach(() => {}); + + test('run', async () => { + await fileUploader.run(mockContext, 'mockDistributionFolder'); + expect(fileScanner.scan).toBeCalled(); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-bucket-name.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-bucket-name.test.js index 4efb11c9dd..5e555812bc 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-bucket-name.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-bucket-name.test.js @@ -1,52 +1,52 @@ -const validateBucketName = require('../../../../lib/S3AndCloudFront/helpers/validate-bucket-name'); +const validateBucketName = require('../../../../lib/S3AndCloudFront/helpers/validate-bucket-name'); describe('validate-bucket-name', () => { - test('validate, lentgh', () => { - const bucketName = 'bn'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); - - test('validate, case', () => { - const bucketName = 'BucketName'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); - - test('validate, start', () => { - const bucketName = '-bucketname'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); - - test('validate, end', () => { - const bucketName = 'bucketname-'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); - - test('validate, consecutive periods', () => { - const bucketName = 'bucket..name'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); - - test('validate, dash adjacent to period', () => { - const bucketName = 'bucket-.name'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); - - test('validate, ip address', () => { - const bucketName = '192.168.1.1'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); - - test('validate, good bucket name', () => { - const bucketName = 'my.bucket-name10'; - const result = validateBucketName(bucketName); - expect(typeof(result) === 'boolean').toBeTruthy(); - expect(result).toEqual(true); - }); -}) \ No newline at end of file + test('validate, lentgh', () => { + const bucketName = 'bn'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeFalsy(); + }); + + test('validate, case', () => { + const bucketName = 'BucketName'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeFalsy(); + }); + + test('validate, start', () => { + const bucketName = '-bucketname'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeFalsy(); + }); + + test('validate, end', () => { + const bucketName = 'bucketname-'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeFalsy(); + }); + + test('validate, consecutive periods', () => { + const bucketName = 'bucket..name'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeFalsy(); + }); + + test('validate, dash adjacent to period', () => { + const bucketName = 'bucket-.name'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeFalsy(); + }); + + test('validate, ip address', () => { + const bucketName = '192.168.1.1'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeFalsy(); + }); + + test('validate, good bucket name', () => { + const bucketName = 'my.bucket-name10'; + const result = validateBucketName(bucketName); + expect(typeof result === 'boolean').toBeTruthy(); + expect(result).toEqual(true); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-website-doc-name.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-website-doc-name.test.js index 6a88d5c654..da0ab7aac4 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-website-doc-name.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/helpers/validate-website-doc-name.test.js @@ -1,22 +1,22 @@ -const validateDocName = require('../../../../lib/S3AndCloudFront/helpers/validate-website-doc-name'); +const validateDocName = require('../../../../lib/S3AndCloudFront/helpers/validate-website-doc-name'); -describe('validate-website-doc-name', ()=>{ - test('validate, not empty', ()=>{ - const docName = ' '; - const result = validateDocName(docName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); +describe('validate-website-doc-name', () => { + test('validate, not empty', () => { + const docName = ' '; + const result = validateDocName(docName); + expect(typeof result === 'boolean').toBeFalsy(); + }); - test('validate, no slash', ()=>{ - const docName = 'doc/name/w/slash'; - const result = validateDocName(docName); - expect(typeof(result) === 'boolean').toBeFalsy(); - }); + test('validate, no slash', () => { + const docName = 'doc/name/w/slash'; + const result = validateDocName(docName); + expect(typeof result === 'boolean').toBeFalsy(); + }); - test('validate, good doc name', ()=>{ - const docName = 'good.html'; - const result = validateDocName(docName); - expect(typeof(result) === 'boolean').toBeTruthy(); - expect(result).toEqual(true); - }); -}); \ No newline at end of file + test('validate, good doc name', () => { + const docName = 'good.html'; + const result = validateDocName(docName); + expect(typeof result === 'boolean').toBeTruthy(); + expect(result).toEqual(true); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/s3Index.test.js b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/s3Index.test.js index 9bf0736a8c..6b0095044d 100644 --- a/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/s3Index.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/S3AndCloudFront/s3Index.test.js @@ -25,144 +25,144 @@ const providerPlugin = 'awscloudformation'; const templateFileName = 'template.json'; const parametersFileName = 'parameters.json'; - const DEV = 'DEV (S3 only with HTTP)'; const PROD = 'PROD (S3 with CloudFront using HTTPS)'; -const Environments = [ - DEV, - PROD, -]; - +const Environments = [DEV, PROD]; const s3IndexModule = require('../../../lib/S3AndCloudFront/index'); describe('s3IndexModule', () => { - const INTERNAL_TEMPLATE_FILE_PATH = path.normalize(path.join(__dirname, '../../../lib/', templateFileName)); - const INTERNAL_PARAMETERS_FILE_PATH = path.normalize(path.join(__dirname, '../../../lib/', parametersFileName)); - - const mockAmplifyMeta = { - "providers": { - "awscloudformation": { - "AuthRoleName": "checkhosting-20190226163640-authRole", - "UnauthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole", - "AuthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole", - "Region": "us-west-2", - "DeploymentBucketName": "checkhosting-20190226163640-deployment", - "UnauthRoleName": "checkhosting-20190226163640-unauthRole", - "StackName": "checkhosting-20190226163640", - "StackId": "arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8" - } - }, - "hosting": { - "S3AndCloudFront": { - "service": "S3AndCloudFront", - "providerPlugin": "awscloudformation", - "providerMetadata": { - "s3TemplateURL": "https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json", - "logicalId": "hostingS3AndCloudFront" - }, - "lastPushTimeStamp": "2019-02-27T00:39:17.966Z", - "output": { - "S3BucketSecureURL": "https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com", - "WebsiteURL": "http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com", - "Region": "us-west-2", - "HostingBucketName": "checkosting-20190226163802-hostingbucket-dev" - }, - "lastPushDirHash": "83Bhmmec48dILMj3mi2T25B4700=" - } - } - } - const mockAnswers = { - environment: DEV - }; - let mockContext = { - amplify: { - pathManager: { - getBackendDirPath: jest.fn(()=>{ - return 'mockBackendDirPath'; - }) - }, - updateamplifyMetaAfterResourceAdd: jest.fn(), - getProjectMeta: jest.fn(()=>{ - return mockAmplifyMeta; - }), - readJsonFile: jest.fn(path => JSON.parse(fs.readFileSync(path))), + const INTERNAL_TEMPLATE_FILE_PATH = path.normalize(path.join(__dirname, '../../../lib/', templateFileName)); + const INTERNAL_PARAMETERS_FILE_PATH = path.normalize(path.join(__dirname, '../../../lib/', parametersFileName)); + + const mockAmplifyMeta = { + providers: { + awscloudformation: { + AuthRoleName: 'checkhosting-20190226163640-authRole', + UnauthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole', + AuthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole', + Region: 'us-west-2', + DeploymentBucketName: 'checkhosting-20190226163640-deployment', + UnauthRoleName: 'checkhosting-20190226163640-unauthRole', + StackName: 'checkhosting-20190226163640', + StackId: 'arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8', + }, + }, + hosting: { + S3AndCloudFront: { + service: 'S3AndCloudFront', + providerPlugin: 'awscloudformation', + providerMetadata: { + s3TemplateURL: 'https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json', + logicalId: 'hostingS3AndCloudFront', }, - print: { - info: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - success: jest.fn() + lastPushTimeStamp: '2019-02-27T00:39:17.966Z', + output: { + S3BucketSecureURL: 'https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com', + WebsiteURL: 'http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com', + Region: 'us-west-2', + HostingBucketName: 'checkosting-20190226163802-hostingbucket-dev', }, - exeInfo: { - parameters: {}, - serviceMeta: { - output: { - WebsiteURL: "mockWebsiteURL" - } - } + lastPushDirHash: '83Bhmmec48dILMj3mi2T25B4700=', + }, + }, + }; + const mockAnswers = { + environment: DEV, + }; + let mockContext = { + amplify: { + pathManager: { + getBackendDirPath: jest.fn(() => { + return 'mockBackendDirPath'; + }), + }, + updateamplifyMetaAfterResourceAdd: jest.fn(), + getProjectMeta: jest.fn(() => { + return mockAmplifyMeta; + }), + readJsonFile: jest.fn(path => JSON.parse(fs.readFileSync(path))), + }, + print: { + info: jest.fn(), + warning: jest.fn(), + error: jest.fn(), + success: jest.fn(), + }, + exeInfo: { + parameters: {}, + serviceMeta: { + output: { + WebsiteURL: 'mockWebsiteURL', }, - parameters: { - options: {} - } - }; - beforeAll(() => { - mockirer(inquirer, mockAnswers); - fs.ensureDirSync = jest.fn(); - fs.existsSync = jest.fn(()=>{return true;}) - fs.writeFileSync = jest.fn(); - fs.readFileSync = jest.fn((filePath)=>{ - let result; - filePath = path.normalize(filePath); - if(filePath.indexOf(templateFileName) > -1){ - if(filePath === INTERNAL_TEMPLATE_FILE_PATH){ - result = JSON.stringify(internalTemplateContents); - }else{ - result = JSON.stringify(mockTemplate); - } - }else if(filePath.indexOf(parametersFileName)>-1){ - if(filePath === INTERNAL_PARAMETERS_FILE_PATH){ - result = JSON.stringify(internalParametersContents); - }else{ - result = JSON.stringify(mockParameters); - } - } - return result; - }); - fileUPloader.run = jest.fn(()=>{return Promise.resolve(); }); - cloudFrontManager.invalidateCloudFront = jest.fn(()=>{return Promise.resolve(); }); - - }); - - beforeEach(() => { - fs.ensureDirSync.mockClear(); - fs.writeFileSync.mockClear(); + }, + }, + parameters: { + options: {}, + }, + }; + beforeAll(() => { + mockirer(inquirer, mockAnswers); + fs.ensureDirSync = jest.fn(); + fs.existsSync = jest.fn(() => { + return true; }); - - test('enable', async () => { - await s3IndexModule.enable(mockContext); - expect(configManager.init).toBeCalled(); - expect(mockContext.amplify.updateamplifyMetaAfterResourceAdd).toBeCalled(); - }); - - test('configure', async () => { - await s3IndexModule.configure(mockContext); - expect(configManager.configure).toBeCalled(); - }); - - test('publish', async () => { - await s3IndexModule.publish(mockContext, {distributionDirPath: 'dist'}); - expect(fileUPloader.run).toBeCalled(); - expect(cloudFrontManager.invalidateCloudFront).toBeCalled(); - expect(open).toBeCalled(); + fs.writeFileSync = jest.fn(); + fs.readFileSync = jest.fn(filePath => { + let result; + filePath = path.normalize(filePath); + if (filePath.indexOf(templateFileName) > -1) { + if (filePath === INTERNAL_TEMPLATE_FILE_PATH) { + result = JSON.stringify(internalTemplateContents); + } else { + result = JSON.stringify(mockTemplate); + } + } else if (filePath.indexOf(parametersFileName) > -1) { + if (filePath === INTERNAL_PARAMETERS_FILE_PATH) { + result = JSON.stringify(internalParametersContents); + } else { + result = JSON.stringify(mockParameters); + } + } + return result; }); - - test('console', async () => { - await s3IndexModule.console(mockContext); - expect(open).toBeCalled(); + fileUPloader.run = jest.fn(() => { + return Promise.resolve(); }); - - test('migrate', async () => { - await s3IndexModule.migrate(mockContext); + cloudFrontManager.invalidateCloudFront = jest.fn(() => { + return Promise.resolve(); }); -}) + }); + + beforeEach(() => { + fs.ensureDirSync.mockClear(); + fs.writeFileSync.mockClear(); + }); + + test('enable', async () => { + await s3IndexModule.enable(mockContext); + expect(configManager.init).toBeCalled(); + expect(mockContext.amplify.updateamplifyMetaAfterResourceAdd).toBeCalled(); + }); + + test('configure', async () => { + await s3IndexModule.configure(mockContext); + expect(configManager.configure).toBeCalled(); + }); + + test('publish', async () => { + await s3IndexModule.publish(mockContext, { distributionDirPath: 'dist' }); + expect(fileUPloader.run).toBeCalled(); + expect(cloudFrontManager.invalidateCloudFront).toBeCalled(); + expect(open).toBeCalled(); + }); + + test('console', async () => { + await s3IndexModule.console(mockContext); + expect(open).toBeCalled(); + }); + + test('migrate', async () => { + await s3IndexModule.migrate(mockContext); + }); +}); diff --git a/packages/amplify-category-hosting/__tests__/lib/category-manager.test.js b/packages/amplify-category-hosting/__tests__/lib/category-manager.test.js index 2fcb17a93b..62fde008aa 100644 --- a/packages/amplify-category-hosting/__tests__/lib/category-manager.test.js +++ b/packages/amplify-category-hosting/__tests__/lib/category-manager.test.js @@ -1,7 +1,7 @@ jest.mock('promise-sequential'); -jest.mock('fs-extra'); -jest.mock('../../lib/S3AndCloudFront/index'); -const s3AndCFIndexModule = require('../../lib/S3AndCloudFront/index'); +jest.mock('fs-extra'); +jest.mock('../../lib/S3AndCloudFront/index'); +const s3AndCFIndexModule = require('../../lib/S3AndCloudFront/index'); const fs = require('fs-extra'); const sequential = require('promise-sequential'); @@ -9,123 +9,124 @@ const sequential = require('promise-sequential'); const categoryManager = require('../../lib/category-manager'); describe('category-manager', () => { - const S3AndCloudFront = 'S3AndCloudFront'; + const S3AndCloudFront = 'S3AndCloudFront'; - const mockBackendDirPath = 'mockBackendDirPath'; + const mockBackendDirPath = 'mockBackendDirPath'; - const mockProjectConfig = { - "projectName": "mockProjectName", - "version": "2.0", - "frontend": "javascript", - "javascript": { - "framework": "none", - "config": { - "SourceDir": "src", - "DistributionDir": "dist", - "BuildCommand": "npm run-script build", - "StartCommand": "npm run-script start" - } - }, - "providers": [ - "awscloudformation" - ] - }; + const mockProjectConfig = { + projectName: 'mockProjectName', + version: '2.0', + frontend: 'javascript', + javascript: { + framework: 'none', + config: { + SourceDir: 'src', + DistributionDir: 'dist', + BuildCommand: 'npm run-script build', + StartCommand: 'npm run-script start', + }, + }, + providers: ['awscloudformation'], + }; - const mockAmplifyMeta = { - "providers": { - "awscloudformation": { - "AuthRoleName": "checkhosting-20190226163640-authRole", - "UnauthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole", - "AuthRoleArn": "arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole", - "Region": "us-west-2", - "DeploymentBucketName": "checkhosting-20190226163640-deployment", - "UnauthRoleName": "checkhosting-20190226163640-unauthRole", - "StackName": "checkhosting-20190226163640", - "StackId": "arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8" - } + const mockAmplifyMeta = { + providers: { + awscloudformation: { + AuthRoleName: 'checkhosting-20190226163640-authRole', + UnauthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-unauthRole', + AuthRoleArn: 'arn:aws:iam::mockAccountId:role/checkhosting-20190226163640-authRole', + Region: 'us-west-2', + DeploymentBucketName: 'checkhosting-20190226163640-deployment', + UnauthRoleName: 'checkhosting-20190226163640-unauthRole', + StackName: 'checkhosting-20190226163640', + StackId: 'arn:aws:cloudformation:us-west-2:mockAccountId:stack/checkhosting-20190226163640/2c061610-3a28-11e9-acf3-02ee71065ed8', + }, + }, + hosting: { + S3AndCloudFront: { + service: 'S3AndCloudFront', + providerPlugin: 'awscloudformation', + providerMetadata: { + s3TemplateURL: 'https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json', + logicalId: 'hostingS3AndCloudFront', }, - "hosting": { - "S3AndCloudFront": { - "service": "S3AndCloudFront", - "providerPlugin": "awscloudformation", - "providerMetadata": { - "s3TemplateURL": "https://s3.amazonaws.com/checkhosting-20190226163640-deployment/amplify-cfn-templates/hosting/template.json", - "logicalId": "hostingS3AndCloudFront" - }, - "lastPushTimeStamp": "2019-02-27T00:39:17.966Z", - "output": { - "S3BucketSecureURL": "https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com", - "WebsiteURL": "http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com", - "Region": "us-west-2", - "HostingBucketName": "checkosting-20190226163802-hostingbucket-dev" - }, - "lastPushDirHash": "83Bhmmec48dILMj3mi2T25B4700=" - } - } - } - - const mockContext = { - amplify: { - pathManager: { - getBackendDirPath: jest.fn(()=>{ - return mockBackendDirPath; - }) - }, - getProjectConfig: jest.fn(()=>{ - return mockProjectConfig; - }), - getProjectDetails: jest.fn(()=>{ - return { - amplifyMeta: mockAmplifyMeta - }; - }) + lastPushTimeStamp: '2019-02-27T00:39:17.966Z', + output: { + S3BucketSecureURL: 'https://checkosting-20190226163802-hostingbucket-dev.s3.amazonaws.com', + WebsiteURL: 'http://checkosting-20190226163802-hostingbucket-dev.s3-website-us-west-2.amazonaws.com', + Region: 'us-west-2', + HostingBucketName: 'checkosting-20190226163802-hostingbucket-dev', }, - migrationInfo: { - amplifyMeta: mockAmplifyMeta - } - }; + lastPushDirHash: '83Bhmmec48dILMj3mi2T25B4700=', + }, + }, + }; - beforeAll(() => { - }); + const mockContext = { + amplify: { + pathManager: { + getBackendDirPath: jest.fn(() => { + return mockBackendDirPath; + }), + }, + getProjectConfig: jest.fn(() => { + return mockProjectConfig; + }), + getProjectDetails: jest.fn(() => { + return { + amplifyMeta: mockAmplifyMeta, + }; + }), + }, + migrationInfo: { + amplifyMeta: mockAmplifyMeta, + }, + }; - beforeEach(() => { - fs.existsSync.mockClear(); - fs.readdirSync.mockClear(); - fs.lstatSync.mockClear(); - }); + beforeAll(() => {}); - test('getCategoryStatus', () => { - fs.existsSync = jest.fn(()=>{ return true; }); - fs.readdirSync = jest.fn(()=>{ - const result = []; - result.push(S3AndCloudFront); - return result; - }); - fs.lstatSync = jest.fn(()=>{ - const result = { - isDirectory: jest.fn(()=>{return true;}) - } - return result; - }); - const result = categoryManager.getCategoryStatus(mockContext); - expect(result.availableServices).toBeDefined(); - expect(result.enabledServices).toBeDefined(); - expect(result.disabledServices).toBeDefined(); - }); + beforeEach(() => { + fs.existsSync.mockClear(); + fs.readdirSync.mockClear(); + fs.lstatSync.mockClear(); + }); - test('runServiceAction', async () => { - const mockAction = 'publish'; - const mockArgs = {}; - s3AndCFIndexModule.publish = jest.fn(); - await categoryManager.runServiceAction(mockContext, S3AndCloudFront, mockAction, mockArgs); - expect(s3AndCFIndexModule.publish).toBeCalled(); - expect(s3AndCFIndexModule.publish.mock.calls[0][0]).toBe(mockContext); - expect(s3AndCFIndexModule.publish.mock.calls[0][1]).toBe(mockArgs); + test('getCategoryStatus', () => { + fs.existsSync = jest.fn(() => { + return true; }); - - test('migrate', async () => { - s3AndCFIndexModule.migrate = jest.fn(); - await categoryManager.migrate(mockContext); - expect(sequential).toBeCalled(); + fs.readdirSync = jest.fn(() => { + const result = []; + result.push(S3AndCloudFront); + return result; + }); + fs.lstatSync = jest.fn(() => { + const result = { + isDirectory: jest.fn(() => { + return true; + }), + }; + return result; }); -}) \ No newline at end of file + const result = categoryManager.getCategoryStatus(mockContext); + expect(result.availableServices).toBeDefined(); + expect(result.enabledServices).toBeDefined(); + expect(result.disabledServices).toBeDefined(); + }); + + test('runServiceAction', async () => { + const mockAction = 'publish'; + const mockArgs = {}; + s3AndCFIndexModule.publish = jest.fn(); + await categoryManager.runServiceAction(mockContext, S3AndCloudFront, mockAction, mockArgs); + expect(s3AndCFIndexModule.publish).toBeCalled(); + expect(s3AndCFIndexModule.publish.mock.calls[0][0]).toBe(mockContext); + expect(s3AndCFIndexModule.publish.mock.calls[0][1]).toBe(mockArgs); + }); + + test('migrate', async () => { + s3AndCFIndexModule.migrate = jest.fn(); + await categoryManager.migrate(mockContext); + expect(sequential).toBeCalled(); + }); +}); diff --git a/packages/amplify-category-interactions/provider-utils/awscloudformation/function-template-dir/cfn-response.js b/packages/amplify-category-interactions/provider-utils/awscloudformation/function-template-dir/cfn-response.js index 3d063a81d9..bd710df751 100644 --- a/packages/amplify-category-interactions/provider-utils/awscloudformation/function-template-dir/cfn-response.js +++ b/packages/amplify-category-interactions/provider-utils/awscloudformation/function-template-dir/cfn-response.js @@ -8,7 +8,7 @@ See the License for the specific language governing permissions and limitations exports.SUCCESS = 'SUCCESS'; exports.FAILED = 'FAILED'; -exports.send = function (event, context, responseStatus, responseData, physicalResourceId, noEcho) { +exports.send = function(event, context, responseStatus, responseData, physicalResourceId, noEcho) { const responseBody = JSON.stringify({ Status: responseStatus, Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`, @@ -37,13 +37,13 @@ exports.send = function (event, context, responseStatus, responseData, physicalR }, }; - const request = https.request(options, (response) => { + const request = https.request(options, response => { console.log(`Status code: ${response.statusCode}`); console.log(`Status message: ${response.statusMessage}`); context.done(); }); - request.on('error', (error) => { + request.on('error', error => { console.log(`send(..) failed executing https.request(..): ${error}`); context.done(); }); diff --git a/packages/amplify-category-notifications/__tests__/lib/apns-cert-config.test.js b/packages/amplify-category-notifications/__tests__/lib/apns-cert-config.test.js index 4ef9812408..02fc319f82 100644 --- a/packages/amplify-category-notifications/__tests__/lib/apns-cert-config.test.js +++ b/packages/amplify-category-notifications/__tests__/lib/apns-cert-config.test.js @@ -2,30 +2,29 @@ const inquirer = require('inquirer'); const mockirer = require('mockirer'); const p12decoder = require('../../lib/p12decoder'); -const apnsCertConfig = require('../../lib/apns-cert-config'); +const apnsCertConfig = require('../../lib/apns-cert-config'); describe('apns-cert-config', () => { - const mockFielPath = 'mock_p12_file_path'; - const mockPassword = 'mock_password'; + const mockFielPath = 'mock_p12_file_path'; + const mockPassword = 'mock_password'; - const mockAnswers = { - P12FilePath: mockFielPath, - P12FilePassword: mockPassword - }; - const mockP12DecoderReturn = {}; + const mockAnswers = { + P12FilePath: mockFielPath, + P12FilePassword: mockPassword, + }; + const mockP12DecoderReturn = {}; - beforeAll(() => { - mockirer(inquirer, mockAnswers); - p12decoder.run = jest.fn(); - p12decoder.run.mockReturnValue(mockP12DecoderReturn); - }); + beforeAll(() => { + mockirer(inquirer, mockAnswers); + p12decoder.run = jest.fn(); + p12decoder.run.mockReturnValue(mockP12DecoderReturn); + }); - beforeEach(() => { - }); + beforeEach(() => {}); - test('p12decoder invoked', async () => { - const result = await apnsCertConfig.run(); - expect(p12decoder.run).toBeCalledWith(mockAnswers); - expect(result).toBe(mockP12DecoderReturn); - }); -}); \ No newline at end of file + test('p12decoder invoked', async () => { + const result = await apnsCertConfig.run(); + expect(p12decoder.run).toBeCalledWith(mockAnswers); + expect(result).toBe(mockP12DecoderReturn); + }); +}); diff --git a/packages/amplify-category-notifications/__tests__/lib/apns-key-config.test.js b/packages/amplify-category-notifications/__tests__/lib/apns-key-config.test.js index dd2296da66..ad3419ced0 100644 --- a/packages/amplify-category-notifications/__tests__/lib/apns-key-config.test.js +++ b/packages/amplify-category-notifications/__tests__/lib/apns-key-config.test.js @@ -2,34 +2,33 @@ const inquirer = require('inquirer'); const mockirer = require('mockirer'); const p8decoder = require('../../lib/p8decoder'); -const apnsKeyConfig = require('../../lib/apns-key-config'); +const apnsKeyConfig = require('../../lib/apns-key-config'); describe('apns-key-config', () => { - const mockBundleId = 'mockBundleId'; - const mockTeamId = 'mockTeamId'; - const mockTokenKeyId = 'mockTokenKeyId'; - const mockFielPath = 'mock_p8_file_path'; + const mockBundleId = 'mockBundleId'; + const mockTeamId = 'mockTeamId'; + const mockTokenKeyId = 'mockTokenKeyId'; + const mockFielPath = 'mock_p8_file_path'; - const mockKeyConfig = { - BundleId: mockBundleId, - TeamId: mockTeamId, - TokenKeyId: mockTokenKeyId, - P8FilePath: mockFielPath - }; - const mockP8DecoderReturn = 'mockP8DecoderReturn'; + const mockKeyConfig = { + BundleId: mockBundleId, + TeamId: mockTeamId, + TokenKeyId: mockTokenKeyId, + P8FilePath: mockFielPath, + }; + const mockP8DecoderReturn = 'mockP8DecoderReturn'; - beforeAll(() => { - mockirer(inquirer, mockKeyConfig); - p8decoder.run = jest.fn(); - p8decoder.run.mockReturnValue(mockP8DecoderReturn); - }); + beforeAll(() => { + mockirer(inquirer, mockKeyConfig); + p8decoder.run = jest.fn(); + p8decoder.run.mockReturnValue(mockP8DecoderReturn); + }); - beforeEach(() => { - }); + beforeEach(() => {}); - test('p8decoder invoked', async () => { - const result = await apnsKeyConfig.run(); - expect(p8decoder.run).toBeCalledWith(mockFielPath); - expect(result).toBe(mockKeyConfig); - }); -}); \ No newline at end of file + test('p8decoder invoked', async () => { + const result = await apnsKeyConfig.run(); + expect(p8decoder.run).toBeCalledWith(mockFielPath); + expect(result).toBe(mockKeyConfig); + }); +}); diff --git a/packages/amplify-category-notifications/__tests__/lib/channel-APNS.test.js b/packages/amplify-category-notifications/__tests__/lib/channel-APNS.test.js index f7a7365226..60499b252a 100644 --- a/packages/amplify-category-notifications/__tests__/lib/channel-APNS.test.js +++ b/packages/amplify-category-notifications/__tests__/lib/channel-APNS.test.js @@ -5,119 +5,118 @@ const configureCertificate = require('../../lib/apns-cert-config'); const channelName = 'APNS'; -const channelAPNS = require('../../lib/channel-APNS'); +const channelAPNS = require('../../lib/channel-APNS'); describe('channel-APNS', () => { - const mockServiceOutput = {}; - const mockChannelOutput = { Enabled: true}; - const mockPinpointResponseErr = {}; - const mockPinpointResponseData = { - APNSChannelResponse: {} - }; - const mockKeyConfig = {}; - const mockCertificateConfig = {}; - const mockPinpointClient = { - updateApnsChannel: jest.fn((_, callback)=>{ - callback(null, mockPinpointResponseData); - }) - } - mockServiceOutput[channelName] = mockChannelOutput; - - let mockContext = { - exeInfo: { - serviceMeta: { - output: mockServiceOutput - }, - pinpointClient: mockPinpointClient - }, - print: { - info: jest.fn(), - error: jest.fn() - } - }; - - beforeAll(() => { - global.console = {log: jest.fn()}; - configureKey.run = jest.fn(()=>{ - return mockKeyConfig; - }); - configureCertificate.run = jest.fn(()=>{ - return mockCertificateConfig; - }); - }); - - beforeEach(() => { - }); - - test('configure', () => { - mockPinpointClient.updateApnsChannel = jest.fn((_, callback)=>{ - callback(null, mockPinpointResponseData); - }); - - mockChannelOutput.Enabled = true; - mockirer(inquirer, {disableChannel: true}); - channelAPNS.configure(mockContext).then(()=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }); - - mockChannelOutput.Enabled = true; - mockirer(inquirer, {disableChannel: false}); - channelAPNS.configure(mockContext).then(()=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }); - - mockChannelOutput.Enabled = false; - mockirer(inquirer, {enableChannel: true}); - channelAPNS.configure(mockContext).then(()=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }); - }); - - test('enable', async () => { - mockPinpointClient.updateApnsChannel = jest.fn((_, callback)=>{ - callback(null, mockPinpointResponseData); - }); - - mockirer(inquirer, {DefaultAuthenticationMethod: 'Certificate'}); - channelAPNS.enable(mockContext, 'successMessage').then(()=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }); - - mockirer(inquirer, {DefaultAuthenticationMethod: 'Key'}); - channelAPNS.enable(mockContext, 'successMessage').then(()=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }); - }); - - test('enable unsccessful', async () => { - mockPinpointClient.updateApnsChannel = jest.fn((_, callback)=>{ - callback(mockPinpointResponseErr, mockPinpointResponseData); - }); - - mockirer(inquirer, {DefaultAuthenticationMethod: 'Certificate'}); - channelAPNS.enable(mockContext, 'successMessage').catch((err)=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }) - - mockirer(inquirer, {DefaultAuthenticationMethod: 'Key'}); - channelAPNS.enable(mockContext, 'successMessage').catch((err)=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }) - }); - - test('disable', () => { - mockPinpointClient.updateApnsChannel = jest.fn((_, callback)=>{ - callback(null, mockPinpointResponseData); - }); - channelAPNS.disable(mockContext).then(()=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }); - - mockPinpointClient.updateApnsChannel = jest.fn((_, callback)=>{ - callback(mockPinpointResponseErr, mockPinpointResponseData); - }); - channelAPNS.disable(mockContext).catch(()=>{ - expect(mockPinpointClient.updateApnsChannel).toBeCalled(); - }) - }); -}); \ No newline at end of file + const mockServiceOutput = {}; + const mockChannelOutput = { Enabled: true }; + const mockPinpointResponseErr = {}; + const mockPinpointResponseData = { + APNSChannelResponse: {}, + }; + const mockKeyConfig = {}; + const mockCertificateConfig = {}; + const mockPinpointClient = { + updateApnsChannel: jest.fn((_, callback) => { + callback(null, mockPinpointResponseData); + }), + }; + mockServiceOutput[channelName] = mockChannelOutput; + + let mockContext = { + exeInfo: { + serviceMeta: { + output: mockServiceOutput, + }, + pinpointClient: mockPinpointClient, + }, + print: { + info: jest.fn(), + error: jest.fn(), + }, + }; + + beforeAll(() => { + global.console = { log: jest.fn() }; + configureKey.run = jest.fn(() => { + return mockKeyConfig; + }); + configureCertificate.run = jest.fn(() => { + return mockCertificateConfig; + }); + }); + + beforeEach(() => {}); + + test('configure', () => { + mockPinpointClient.updateApnsChannel = jest.fn((_, callback) => { + callback(null, mockPinpointResponseData); + }); + + mockChannelOutput.Enabled = true; + mockirer(inquirer, { disableChannel: true }); + channelAPNS.configure(mockContext).then(() => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + + mockChannelOutput.Enabled = true; + mockirer(inquirer, { disableChannel: false }); + channelAPNS.configure(mockContext).then(() => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + + mockChannelOutput.Enabled = false; + mockirer(inquirer, { enableChannel: true }); + channelAPNS.configure(mockContext).then(() => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + }); + + test('enable', async () => { + mockPinpointClient.updateApnsChannel = jest.fn((_, callback) => { + callback(null, mockPinpointResponseData); + }); + + mockirer(inquirer, { DefaultAuthenticationMethod: 'Certificate' }); + channelAPNS.enable(mockContext, 'successMessage').then(() => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + + mockirer(inquirer, { DefaultAuthenticationMethod: 'Key' }); + channelAPNS.enable(mockContext, 'successMessage').then(() => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + }); + + test('enable unsccessful', async () => { + mockPinpointClient.updateApnsChannel = jest.fn((_, callback) => { + callback(mockPinpointResponseErr, mockPinpointResponseData); + }); + + mockirer(inquirer, { DefaultAuthenticationMethod: 'Certificate' }); + channelAPNS.enable(mockContext, 'successMessage').catch(err => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + + mockirer(inquirer, { DefaultAuthenticationMethod: 'Key' }); + channelAPNS.enable(mockContext, 'successMessage').catch(err => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + }); + + test('disable', () => { + mockPinpointClient.updateApnsChannel = jest.fn((_, callback) => { + callback(null, mockPinpointResponseData); + }); + channelAPNS.disable(mockContext).then(() => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + + mockPinpointClient.updateApnsChannel = jest.fn((_, callback) => { + callback(mockPinpointResponseErr, mockPinpointResponseData); + }); + channelAPNS.disable(mockContext).catch(() => { + expect(mockPinpointClient.updateApnsChannel).toBeCalled(); + }); + }); +}); diff --git a/packages/amplify-category-storage/provider-utils/awscloudformation/triggers/s3/index.js b/packages/amplify-category-storage/provider-utils/awscloudformation/triggers/s3/index.js index a084a84523..2be4f1b3c0 100644 --- a/packages/amplify-category-storage/provider-utils/awscloudformation/triggers/s3/index.js +++ b/packages/amplify-category-storage/provider-utils/awscloudformation/triggers/s3/index.js @@ -1,4 +1,5 @@ -exports.handler = function (event, context) { //eslint-disable-line +exports.handler = function(event, context) { + //eslint-disable-line console.log('Received S3 event:', JSON.stringify(event, null, 2)); // Get the object from the event and show its content type const bucket = event.Records[0].s3.bucket.name; //eslint-disable-line diff --git a/packages/amplify-category-xr/__tests__/commands/add.test.js b/packages/amplify-category-xr/__tests__/commands/add.test.js index e057668d68..12b59a6d9f 100644 --- a/packages/amplify-category-xr/__tests__/commands/add.test.js +++ b/packages/amplify-category-xr/__tests__/commands/add.test.js @@ -1,7 +1,7 @@ const add = require('../../commands/xr/add'); describe('XR add', () => { - it('should have a run method', () => { - expect(add.run).toBeDefined(); - }); + it('should have a run method', () => { + expect(add.run).toBeDefined(); + }); }); diff --git a/packages/amplify-category-xr/__tests__/commands/push.test.js b/packages/amplify-category-xr/__tests__/commands/push.test.js index ec5a78e16c..b82f8ef166 100644 --- a/packages/amplify-category-xr/__tests__/commands/push.test.js +++ b/packages/amplify-category-xr/__tests__/commands/push.test.js @@ -1,7 +1,7 @@ const push = require('../../commands/xr/push'); describe('XR push', () => { - it('should have a run method', () => { - expect(push.run).toBeDefined(); - }); -}); \ No newline at end of file + it('should have a run method', () => { + expect(push.run).toBeDefined(); + }); +}); diff --git a/packages/amplify-category-xr/__tests__/commands/remove.test.js b/packages/amplify-category-xr/__tests__/commands/remove.test.js index 985501e94e..5c6e178d74 100644 --- a/packages/amplify-category-xr/__tests__/commands/remove.test.js +++ b/packages/amplify-category-xr/__tests__/commands/remove.test.js @@ -1,7 +1,7 @@ const remove = require('../../commands/xr/remove'); describe('XR remove', () => { - it('should have a run method', () => { - expect(remove.run).toBeDefined(); - }); + it('should have a run method', () => { + expect(remove.run).toBeDefined(); + }); }); diff --git a/packages/amplify-category-xr/__tests__/commands/update.test.js b/packages/amplify-category-xr/__tests__/commands/update.test.js index 39f8407727..ef345d9e3a 100644 --- a/packages/amplify-category-xr/__tests__/commands/update.test.js +++ b/packages/amplify-category-xr/__tests__/commands/update.test.js @@ -1,7 +1,7 @@ const update = require('../../commands/xr/update'); describe('XR update', () => { - it('should have a run method', () => { - expect(update.run).toBeDefined(); - }); + it('should have a run method', () => { + expect(update.run).toBeDefined(); + }); }); diff --git a/packages/amplify-category-xr/__tests__/lib/xr-manager.test.js b/packages/amplify-category-xr/__tests__/lib/xr-manager.test.js index 7b34d8b29d..74a206a89b 100644 --- a/packages/amplify-category-xr/__tests__/lib/xr-manager.test.js +++ b/packages/amplify-category-xr/__tests__/lib/xr-manager.test.js @@ -1,48 +1,48 @@ const xrManager = require('../../lib/xr-manager'); describe('xrManager', () => { - const mockContextWithXR = { - amplify: { - removeResource: jest.fn() + const mockContextWithXR = { + amplify: { + removeResource: jest.fn(), + }, + exeInfo: { + amplifyMeta: { + xr: { + scene1: {}, + scene2: {}, }, - exeInfo: { - amplifyMeta: { - xr: { - scene1: {}, - scene2: {} - } - } - } - }; + }, + }, + }; - const mockContextWithoutXR = { - exeInfo: { - amplifyMeta: {} - } - }; + const mockContextWithoutXR = { + exeInfo: { + amplifyMeta: {}, + }, + }; - describe('isXRSetup', () => { - it('should return true when xr is configured in amplifyMeta', () => { - expect(xrManager.isXRSetup(mockContextWithXR)).toBeTruthy(); - }); + describe('isXRSetup', () => { + it('should return true when xr is configured in amplifyMeta', () => { + expect(xrManager.isXRSetup(mockContextWithXR)).toBeTruthy(); + }); - it('should return false when xr is configured in amplifyMeta', () => { - expect(xrManager.isXRSetup(mockContextWithoutXR)).toBeFalsy(); - }); - }) + it('should return false when xr is configured in amplifyMeta', () => { + expect(xrManager.isXRSetup(mockContextWithoutXR)).toBeFalsy(); + }); + }); - describe('getExistingScenes', () => { - it('should return a list of scene resoureces', () => { - const existingScenes = xrManager.getExistingScenes(mockContextWithXR); - expect(existingScenes).toHaveLength(2); - expect(existingScenes).toContain('scene1'); - expect(existingScenes).toContain('scene2'); - }); + describe('getExistingScenes', () => { + it('should return a list of scene resoureces', () => { + const existingScenes = xrManager.getExistingScenes(mockContextWithXR); + expect(existingScenes).toHaveLength(2); + expect(existingScenes).toContain('scene1'); + expect(existingScenes).toContain('scene2'); + }); - it('should return an empty list if xr is not configured', () => { - const existingScenes = xrManager.getExistingScenes(mockContextWithoutXR); - expect(existingScenes).toHaveLength(0); - expect(existingScenes).toEqual([]); - }); + it('should return an empty list if xr is not configured', () => { + const existingScenes = xrManager.getExistingScenes(mockContextWithoutXR); + expect(existingScenes).toHaveLength(0); + expect(existingScenes).toEqual([]); }); -}); \ No newline at end of file + }); +}); diff --git a/packages/amplify-cli/__mocks__/fs-extra.js b/packages/amplify-cli/__mocks__/fs-extra.js index 0590a9f0cc..8ee2fe937d 100644 --- a/packages/amplify-cli/__mocks__/fs-extra.js +++ b/packages/amplify-cli/__mocks__/fs-extra.js @@ -1,6 +1,5 @@ const fsExtra = jest.genMockFromModule('fs-extra'); - function copySync() { return {}; } diff --git a/packages/amplify-cli/__mocks__/fs.js b/packages/amplify-cli/__mocks__/fs.js index 9f82065b93..500a564637 100644 --- a/packages/amplify-cli/__mocks__/fs.js +++ b/packages/amplify-cli/__mocks__/fs.js @@ -1,6 +1,5 @@ const fs = jest.genMockFromModule('fs'); - function readdirSync() { return ['file1', 'file2']; } @@ -17,7 +16,6 @@ function statSync() { return { isDirectory: () => {} }; } - fs.readdirSync = readdirSync; fs.readFileSync = readFileSync; fs.statSync = statSync; diff --git a/packages/amplify-cli/__mocks__/inquirer.js b/packages/amplify-cli/__mocks__/inquirer.js index 6bcaeae5af..2fa28ab6ff 100644 --- a/packages/amplify-cli/__mocks__/inquirer.js +++ b/packages/amplify-cli/__mocks__/inquirer.js @@ -1,6 +1,5 @@ const inquirer = jest.genMockFromModule('inquirer'); - function prompt(obj) { return obj; } diff --git a/packages/amplify-cli/src/__test__/context-manager.test.ts b/packages/amplify-cli/src/__test__/context-manager.test.ts index 4f9ff6f378..673724817b 100644 --- a/packages/amplify-cli/src/__test__/context-manager.test.ts +++ b/packages/amplify-cli/src/__test__/context-manager.test.ts @@ -16,4 +16,4 @@ test('constructContext', () => { expect(context.amplify).toBeDefined(); expect(context.pluginPlatform).toEqual(mockPluginPlatform); expect(context.input).toEqual(mockInput); -}) \ No newline at end of file +}); diff --git a/packages/amplify-cli/src/commands/plugin.ts b/packages/amplify-cli/src/commands/plugin.ts index 499e15b751..1a7883a203 100644 --- a/packages/amplify-cli/src/commands/plugin.ts +++ b/packages/amplify-cli/src/commands/plugin.ts @@ -11,12 +11,12 @@ export async function run(context: Context) { const subCommandPath = path.normalize(path.join(__dirname, 'plugin', subCommand)); import(subCommandPath) - .then(async (subCommandModule) => { + .then(async subCommandModule => { await subCommandModule.run(context); }) .catch(() => { context.print.error(`Cannot load command amplify plugin ${subCommand}`); - }) + }); } function mapSubcommandAlias(subcommand: string): string { @@ -24,4 +24,4 @@ function mapSubcommandAlias(subcommand: string): string { return 'new'; } return subcommand; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/add.ts b/packages/amplify-cli/src/commands/plugin/add.ts index 72ef8e5de7..5354c56a71 100644 --- a/packages/amplify-cli/src/commands/plugin/add.ts +++ b/packages/amplify-cli/src/commands/plugin/add.ts @@ -4,11 +4,7 @@ import path from 'path'; import Context from '../../domain/context'; import PluginInfo from '../../domain/plugin-info'; import constants from '../../domain/constants'; -import { - addUserPluginPackage, - addExcludedPluginPackage as addFromExcluded, - confirmAndScan, -} from '../../plugin-manager'; +import { addUserPluginPackage, addExcludedPluginPackage as addFromExcluded, confirmAndScan } from '../../plugin-manager'; import inquirer, { InquirerOption, EXPAND } from '../../domain/inquirer-helper'; import { AddPluginError } from '../../domain/add-plugin-result'; import { normalizePluginDirectory } from '../../plugin-helpers/scan-plugin-platform'; @@ -47,8 +43,7 @@ async function resolvePluginPathAndAdd(context: Context, inputPath: string) { } } -async function resolvePluginPackagePath(context: Context, inputPath: string): -Promise { +async function resolvePluginPackagePath(context: Context, inputPath: string): Promise { if (path.isAbsolute(inputPath)) { return inputPath; } @@ -56,21 +51,13 @@ Promise { let result; const { pluginPlatform } = context; - let searchDirPaths = [ - constants.ParentDirectory, - constants.LocalNodeModules, - constants.GlobalNodeModules, - process.cwd(), - ]; - searchDirPaths = searchDirPaths.filter(dirPath => - !pluginPlatform.pluginDirectories.includes(dirPath.toString())); + let searchDirPaths = [constants.ParentDirectory, constants.LocalNodeModules, constants.GlobalNodeModules, process.cwd()]; + searchDirPaths = searchDirPaths.filter(dirPath => !pluginPlatform.pluginDirectories.includes(dirPath.toString())); searchDirPaths = searchDirPaths.concat(pluginPlatform.pluginDirectories); - const candicatePluginDirPaths = searchDirPaths.map(dirPath => - path.normalize(path.join(normalizePluginDirectory(dirPath), inputPath)), - ).filter(pluginDirPath => - fs.existsSync(pluginDirPath) && fs.statSync(pluginDirPath).isDirectory(), - ); + const candicatePluginDirPaths = searchDirPaths + .map(dirPath => path.normalize(path.join(normalizePluginDirectory(dirPath), inputPath))) + .filter(pluginDirPath => fs.existsSync(pluginDirPath) && fs.statSync(pluginDirPath).isDirectory()); if (candicatePluginDirPaths.length === 0) { context.print.error('Can not locate the plugin package.'); @@ -90,9 +77,7 @@ Promise { } else if (candicatePluginDirPaths.length > 1) { context.print.warning('Multiple plugins with the package name are found.'); - const options = candicatePluginDirPaths.concat([ - CANCEL, - ]); + const options = candicatePluginDirPaths.concat([CANCEL]); const answer = await inquirer.prompt({ type: 'list', name: 'selection', @@ -107,12 +92,11 @@ Promise { return result; } - async function promptAndAdd(context: Context) { const options = new Array(); const { excluded } = context.pluginPlatform; if (excluded && Object.keys(excluded).length > 0) { - Object.keys(excluded).forEach((key) => { + Object.keys(excluded).forEach(key => { if (excluded[key].length > 0) { const option = { name: key + EXPAND, @@ -126,7 +110,7 @@ async function promptAndAdd(context: Context) { } options.push(option); } - }) + }); } if (options.length > 0) { @@ -153,7 +137,6 @@ async function promptAndAdd(context: Context) { } } - async function promptForPluginPath(): Promise { const answer = await inquirer.prompt({ type: 'input', @@ -173,22 +156,19 @@ async function promptForPluginPath(): Promise { async function addNewPluginPackage(context: Context, pluginDirPath: string) { try { - const addUserPluginResult = addUserPluginPackage( - context.pluginPlatform, - pluginDirPath.trim(), - ); + const addUserPluginResult = addUserPluginPackage(context.pluginPlatform, pluginDirPath.trim()); if (addUserPluginResult.isAdded) { context.print.success('Successfully added plugin package.'); await confirmAndScan(context.pluginPlatform); } else { context.print.error('Failed to add the plugin package.'); context.print.info(`Error code: ${addUserPluginResult.error}`); - if (addUserPluginResult.error === AddPluginError.FailedVerification && - addUserPluginResult.pluginVerificationResult && - addUserPluginResult.pluginVerificationResult.error) { - context.print.info( - `Plugin verification error code: ${addUserPluginResult.pluginVerificationResult.error}`, - ); + if ( + addUserPluginResult.error === AddPluginError.FailedVerification && + addUserPluginResult.pluginVerificationResult && + addUserPluginResult.pluginVerificationResult.error + ) { + context.print.info(`Plugin verification error code: ${addUserPluginResult.pluginVerificationResult.error}`); } } } catch (e) { @@ -203,12 +183,12 @@ async function addExcludedPluginPackage(context: Context, userSelection: PluginI addFromExcluded(context.pluginPlatform, userSelection[0]); } else { const options = new Array(); - userSelection.forEach((pluginInfo) => { + userSelection.forEach(pluginInfo => { options.push({ name: pluginInfo.packageName + '@' + pluginInfo.packageVersion, value: pluginInfo, short: pluginInfo.packageName + '@' + pluginInfo.packageVersion, - }) + }); }); const answer = await inquirer.prompt({ @@ -222,4 +202,4 @@ async function addExcludedPluginPackage(context: Context, userSelection: PluginI } await confirmAndScan(context.pluginPlatform); } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/configure.ts b/packages/amplify-cli/src/commands/plugin/configure.ts index 9d2374d989..aa3df7373e 100644 --- a/packages/amplify-cli/src/commands/plugin/configure.ts +++ b/packages/amplify-cli/src/commands/plugin/configure.ts @@ -7,8 +7,12 @@ import Constants from '../../domain/constants'; import { writePluginsJsonFileSync } from '../../plugin-helpers/access-plugins-file'; import { normalizePluginDirectory } from '../../plugin-helpers/scan-plugin-platform'; import { scan } from '../../plugin-manager'; -import { displayPluginDirectories, displayPrefixes, displayScanInterval, - displayConfiguration } from '../../plugin-helpers/display-plugin-platform'; +import { + displayPluginDirectories, + displayPrefixes, + displayScanInterval, + displayConfiguration, +} from '../../plugin-helpers/display-plugin-platform'; const MINPREFIXLENGTH = 2; const MAXPREFIXLENGTH = 20; @@ -20,12 +24,7 @@ export async function run(context: Context): Promise { const maxScanIntervalInSeconds = 'max CLI scan interval in seconds'; const exit = 'save & exit'; - const options = [ - pluginDirectories, - pluginPrefixes, - maxScanIntervalInSeconds, - exit, - ]; + const options = [pluginDirectories, pluginPrefixes, maxScanIntervalInSeconds, exit]; let answer: any; @@ -64,7 +63,7 @@ async function configurePluginDirectories(context: Context, pluginPlatform: Plug const ADD = 'add'; const REMOVE = 'remove'; const EXIT = 'exit'; - const LEARNMORE = 'Learn more' + const LEARNMORE = 'Learn more'; const actionAnswer = await inquirer.prompt({ type: 'list', @@ -91,7 +90,6 @@ Only explicitly added plugins are active.'); displayPluginDirectories(context, pluginPlatform); } - function displayPluginDirectoriesLearnMore(context: Context) { context.print.info(''); context.print.green('The directories contained this list are searched for \ @@ -116,12 +114,8 @@ is the global node_modules directory.`); } async function addPluginDirectory(pluginPlatform: PluginPlatform) { - const ADDCUSTOMDIRECTORY = 'Add custom directory >' - let options = [ - Constants.ParentDirectory, - Constants.LocalNodeModules, - Constants.GlobalNodeModules, - ]; + const ADDCUSTOMDIRECTORY = 'Add custom directory >'; + let options = [Constants.ParentDirectory, Constants.LocalNodeModules, Constants.GlobalNodeModules]; options = options.filter(item => !pluginPlatform.pluginDirectories.includes(item.toString())); @@ -148,7 +142,7 @@ async function addPluginDirectory(pluginPlatform: PluginPlatform) { type: 'input', name: 'newScanDirectory', message: `Enter the full path of the plugin scan directory you want to add${os.EOL}`, - validate: (input : string) => { + validate: (input: string) => { if (!fs.existsSync(input) || !fs.statSync(input).isDirectory()) { return 'Must enter a valid full path of a directory'; } @@ -166,9 +160,7 @@ async function removePluginDirectory(pluginPlatform: PluginPlatform) { message: 'Select the directories that Amplify CLI should NOT scan for plugins', choices: pluginPlatform.pluginDirectories, }); - pluginPlatform.pluginDirectories = pluginPlatform.pluginDirectories.filter( - dir => !answer.directoriesToRemove.includes(dir), - ); + pluginPlatform.pluginDirectories = pluginPlatform.pluginDirectories.filter(dir => !answer.directoriesToRemove.includes(dir)); } async function configurePrefixes(context: Context, pluginPlatform: PluginPlatform) { @@ -177,7 +169,7 @@ async function configurePrefixes(context: Context, pluginPlatform: PluginPlatfor const ADD = 'add'; const REMOVE = 'remove'; const EXIT = 'exit'; - const LEARNMORE = 'Learn more' + const LEARNMORE = 'Learn more'; const actionAnswer = await inquirer.prompt({ type: 'list', @@ -192,8 +184,10 @@ async function configurePrefixes(context: Context, pluginPlatform: PluginPlatfor await removePrefixes(pluginPlatform); if (pluginPlatform.pluginPrefixes.length === 0) { context.print.warning('You have removed all prefixes for plugin dir name matching!'); - context.print.info('All the packages inside the plugin directories will be checked \ -during a plugin scan, this can significantly increase the scan time.') + context.print.info( + 'All the packages inside the plugin directories will be checked \ +during a plugin scan, this can significantly increase the scan time.' + ); } } else if (actionAnswer.action === LEARNMORE) { displayPluginPrefixesLearnMore(context); @@ -207,8 +201,10 @@ function displayPluginPrefixesLearnMore(context: Context) { context.print.info(''); context.print.green('The package name prefixes contained this list are used for \ plugin name matching in plugin scans.'); - context.print.green('Only packages with matching name are considered plugin candidates, \ -they are verified and then added to the Amplify CLI.'); + context.print.green( + 'Only packages with matching name are considered plugin candidates, \ +they are verified and then added to the Amplify CLI.' + ); context.print.green('If this list is empty, all packages inside the scanned directories \ are checked in plugin scans.'); context.print.green('You can add or remove from this list to change the plugin \ @@ -219,10 +215,8 @@ scan behavior, and consequently its outcome.'); } async function addPrefix(pluginPlatform: PluginPlatform) { - const ADDCUSTOMPREFIX = 'Add custom prefix >' - let options = [ - Constants.AmplifyPrefix, - ]; + const ADDCUSTOMPREFIX = 'Add custom prefix >'; + let options = [Constants.AmplifyPrefix]; options = options.filter(item => !pluginPlatform.pluginPrefixes.includes(item.toString())); @@ -249,11 +243,12 @@ async function addPrefix(pluginPlatform: PluginPlatform) { type: 'input', name: 'newPrefix', message: 'Enter the new prefix', - validate: (input : string) => { + validate: (input: string) => { input = input.trim(); if (input.length < MINPREFIXLENGTH || input.length > MAXPREFIXLENGTH) { - return 'The Length of prefix must be between 2 and 20.' - }if (!/^[a-zA-Z][a-zA-Z0-9-]+$/.test(input)) { + return 'The Length of prefix must be between 2 and 20.'; + } + if (!/^[a-zA-Z][a-zA-Z0-9-]+$/.test(input)) { return 'Prefix must start with letter, and contain only alphanumerics and dashes(-)'; } return true; @@ -270,14 +265,14 @@ async function removePrefixes(pluginPlatform: PluginPlatform) { message: 'Select the prefixes to remove', choices: pluginPlatform.pluginPrefixes, }); - pluginPlatform.pluginPrefixes = pluginPlatform.pluginPrefixes.filter( - prefix => !answer.prefixesToRemove.includes(prefix), - ) + pluginPlatform.pluginPrefixes = pluginPlatform.pluginPrefixes.filter(prefix => !answer.prefixesToRemove.includes(prefix)); } async function configureScanInterval(context: Context, pluginPlatform: PluginPlatform) { - context.print.green('The Amplify CLI plugin platform regularly scans the local \ -system to update its internal metadata on the locally installed plugins.'); + context.print.green( + 'The Amplify CLI plugin platform regularly scans the local \ +system to update its internal metadata on the locally installed plugins.' + ); context.print.green('This automatic scan will happen if the last scan \ time has passed for longer than max-scan-interval-in-seconds.'); context.print.info(''); @@ -304,12 +299,7 @@ export async function listConfiguration(context: Context, pluginPlatform: Plugin const maxScanIntervalInSeconds = 'max scan interval in seconds'; const all = 'all'; - const options = [ - pluginDirectories, - pluginPrefixes, - maxScanIntervalInSeconds, - all, - ]; + const options = [pluginDirectories, pluginPrefixes, maxScanIntervalInSeconds, all]; const answer = await inquirer.prompt({ type: 'list', @@ -335,4 +325,4 @@ export async function listConfiguration(context: Context, pluginPlatform: Plugin displayConfiguration(context, pluginPlatform); break; } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/help.ts b/packages/amplify-cli/src/commands/plugin/help.ts index b492ad0081..c97d3c29db 100644 --- a/packages/amplify-cli/src/commands/plugin/help.ts +++ b/packages/amplify-cli/src/commands/plugin/help.ts @@ -45,4 +45,4 @@ export function run(context: Context) { context.print.info(''); context.print.table(tableOptions, { format: 'default' }); context.print.info(''); -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/list.ts b/packages/amplify-cli/src/commands/plugin/list.ts index 7ae040b833..3dd14f00df 100644 --- a/packages/amplify-cli/src/commands/plugin/list.ts +++ b/packages/amplify-cli/src/commands/plugin/list.ts @@ -1,8 +1,7 @@ import Context from '../../domain/context'; import inquirer from '../../domain/inquirer-helper'; import PluginCollection from '../../domain/plugin-collection'; -import { displayGeneralInfo, - displayPluginCollection, displayPluginInfoArray } from '../../plugin-helpers/display-plugin-platform'; +import { displayGeneralInfo, displayPluginCollection, displayPluginInfoArray } from '../../plugin-helpers/display-plugin-platform'; export async function run(context: Context) { const { pluginPlatform } = context; @@ -11,12 +10,7 @@ export async function run(context: Context) { const excluded = 'excluded plugins'; const generalInfo = 'general information'; - const options = - [ - plugins, - excluded, - generalInfo, - ] + const options = [plugins, excluded, generalInfo]; const answer = await inquirer.prompt({ type: 'list', @@ -65,4 +59,4 @@ async function listPluginCollection(context: Context, collection: PluginCollecti } else { context.print.info('The collection is empty'); } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/new.ts b/packages/amplify-cli/src/commands/plugin/new.ts index eb81a717c5..f4e1e0bf53 100644 --- a/packages/amplify-cli/src/commands/plugin/new.ts +++ b/packages/amplify-cli/src/commands/plugin/new.ts @@ -22,9 +22,11 @@ async function plugIntoLocalAmplifyCli(context: Context, pluginDirPath: string): } else { context.print.error('Failed to add the plugin package to the local Amplify CLI.'); context.print.info(`Error code: ${addPluginResult.error}`); - if (addPluginResult.error === AddPluginError.FailedVerification && - addPluginResult.pluginVerificationResult && - addPluginResult.pluginVerificationResult.error) { + if ( + addPluginResult.error === AddPluginError.FailedVerification && + addPluginResult.pluginVerificationResult && + addPluginResult.pluginVerificationResult.error + ) { const { error } = addPluginResult.pluginVerificationResult; context.print.info(`Plugin verification error code: \ ${error}`); } @@ -83,8 +85,7 @@ function printInfo(context: Context, pluginDirPath: string, isPluggedInLocalAmpl context.print.info(`$ amplify plugin add: add the plugin into the local Amplify CLI for testing.`); } - const amplifyPluginJsonFilePath = - path.normalize(path.join(pluginDirPath, constants.MANIFEST_FILE_NAME)); + const amplifyPluginJsonFilePath = path.normalize(path.join(pluginDirPath, constants.MANIFEST_FILE_NAME)); const commandsDirPath = path.normalize(path.join(pluginDirPath, 'commands')); const eventHandlerDirPath = path.normalize(path.join(pluginDirPath, 'event-handlers')); @@ -102,4 +103,4 @@ function printInfo(context: Context, pluginDirPath: string, isPluggedInLocalAmpl context.print.info('2. Add/remove the event handler code file into the event-handler folder.'); context.print.green(eventHandlerDirPath); context.print.info(''); -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/remove.ts b/packages/amplify-cli/src/commands/plugin/remove.ts index 1401cce2d1..eb3a751237 100644 --- a/packages/amplify-cli/src/commands/plugin/remove.ts +++ b/packages/amplify-cli/src/commands/plugin/remove.ts @@ -7,12 +7,10 @@ import PluginInfo from '../../domain/plugin-info'; export async function run(context: Context) { const options = new Array(); - const { - plugins, - } = context.pluginPlatform; + const { plugins } = context.pluginPlatform; if (plugins && Object.keys(plugins).length > 0) { - Object.keys(plugins).forEach((key) => { + Object.keys(plugins).forEach(key => { if (key === Constant.CORE) { return; } @@ -30,7 +28,7 @@ export async function run(context: Context) { } options.push(option); } - }) + }); } if (options.length > 0) { @@ -63,7 +61,7 @@ async function removeNamedPlugins(pluginPlatform: PluginPlatform, pluginInfos: A name: pluginInfo.packageName + '@' + pluginInfo.packageVersion, value: pluginInfo, short: pluginInfo.packageName + '@' + pluginInfo.packageVersion, - } + }; return optionObject; }); const { selections } = await inquirer.prompt({ @@ -81,4 +79,4 @@ async function removeNamedPlugins(pluginPlatform: PluginPlatform, pluginInfos: A await sequential(removeTasks); } } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/scan.ts b/packages/amplify-cli/src/commands/plugin/scan.ts index 5fc19718b3..cdf9460e60 100644 --- a/packages/amplify-cli/src/commands/plugin/scan.ts +++ b/packages/amplify-cli/src/commands/plugin/scan.ts @@ -3,4 +3,4 @@ import { scan as pluginManagerScan } from '../../plugin-manager'; export async function run(context: Context) { await pluginManagerScan(); -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/plugin/verify.ts b/packages/amplify-cli/src/commands/plugin/verify.ts index f94fdfcad1..e3836ae031 100644 --- a/packages/amplify-cli/src/commands/plugin/verify.ts +++ b/packages/amplify-cli/src/commands/plugin/verify.ts @@ -2,7 +2,7 @@ import Context from '../../domain/context'; import { verifyPlugin } from '../../plugin-manager'; export async function run(context: Context) { - context.print.warning('Run this command at the root directory of the plugin package.') + context.print.warning('Run this command at the root directory of the plugin package.'); const verificatonResult = await verifyPlugin(process.cwd()); if (verificatonResult.verified) { context.print.success('The current directory is verified to be a valid Amplify CLI plugin package.'); @@ -12,4 +12,4 @@ export async function run(context: Context) { context.print.info(`Error code: ${verificatonResult.error}`); context.print.info(''); } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/commands/version.ts b/packages/amplify-cli/src/commands/version.ts index 766ccfa7c4..1c6f4207b0 100644 --- a/packages/amplify-cli/src/commands/version.ts +++ b/packages/amplify-cli/src/commands/version.ts @@ -5,4 +5,4 @@ import path from 'path'; export function run(context: Context) { const packageJsonFilePath = path.join(__dirname, '../../package.json'); context.print.info(readJsonFileSync(packageJsonFilePath).version); -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/context-extensions.ts b/packages/amplify-cli/src/context-extensions.ts index e7460606f2..43cdbadfd3 100644 --- a/packages/amplify-cli/src/context-extensions.ts +++ b/packages/amplify-cli/src/context-extensions.ts @@ -16,21 +16,21 @@ importedColors.setTheme({ yellow: 'yellow', red: 'red', blue: 'blue', -}) +}); type CliPrintColors = typeof importedColors & { - highlight: (t: string) => string - info: (t: string) => string - warning: (t: string) => string - success: (t: string) => string - error: (t: string) => string - line: (t: string) => string - muted: (t: string) => string - green: (t: string) => string - yellow: (t: string) => string - red: (t: string) => string - blue: (t: string) => string, -} + highlight: (t: string) => string; + info: (t: string) => string; + warning: (t: string) => string; + success: (t: string) => string; + error: (t: string) => string; + line: (t: string) => string; + muted: (t: string) => string; + green: (t: string) => string; + yellow: (t: string) => string; + red: (t: string) => string; + blue: (t: string) => string; +}; const colors = importedColors as CliPrintColors; @@ -52,33 +52,37 @@ function attachPrompt(context: Context) { name: 'yesno', type: 'confirm', message, - }) + }); return yesno; }, ask: async (questions: any) => { if (Array.isArray(questions)) { - questions = questions.map((q) => { - if (q.type === 'rawlist' || q.type === 'list') { q.type = 'select' } - if (q.type === 'expand') { q.type = 'autocomplete' } - if (q.type === 'checkbox') { q.type = 'multiselect' } - if (q.type === 'radio') { q.type = 'select' } - if (q.type === 'question') { q.type = 'input' } - return q - }) + questions = questions.map(q => { + if (q.type === 'rawlist' || q.type === 'list') { + q.type = 'select'; + } + if (q.type === 'expand') { + q.type = 'autocomplete'; + } + if (q.type === 'checkbox') { + q.type = 'multiselect'; + } + if (q.type === 'radio') { + q.type = 'select'; + } + if (q.type === 'question') { + q.type = 'input'; + } + return q; + }); } - return inquirer.prompt(questions) + return inquirer.prompt(questions); }, - } + }; } function attachParameters(context: Context) { - const { - argv, - plugin, - command, - subCommands, - options, - } = context.input; + const { argv, plugin, command, subCommands, options } = context.input; context.parameters = { argv, @@ -108,9 +112,9 @@ function attachRuntime(context: Context) { context.runtime = { plugins: [], }; - Object.keys(context.pluginPlatform.plugins).forEach((pluginShortName) => { + Object.keys(context.pluginPlatform.plugins).forEach(pluginShortName => { const pluginInfos = context.pluginPlatform.plugins[pluginShortName]; - pluginInfos.forEach((pluginInfo) => { + pluginInfos.forEach(pluginInfo => { const name = path.basename(pluginInfo.packageLocation); const directory = pluginInfo.packageLocation; const pluginName = pluginInfo.manifest.name; @@ -122,9 +126,9 @@ function attachRuntime(context: Context) { pluginName, pluginType, commands, - }) - }) - }) + }); + }); + }); } function attachFilesystem(context: Context) { @@ -155,18 +159,17 @@ const contextFileSystem = { const result = path.normalize(path.join(...pathParts)); return result; }, -} +}; function attachPatching(context: Context) { context.patching = { - replace: async (filePath: string, oldContent: string, newContent: string): - Promise => { + replace: async (filePath: string, oldContent: string, newContent: string): Promise => { const fileContent = fs.readFileSync(filePath, 'utf-8'); const updatedFileContent = fileContent.replace(oldContent, newContent); fs.writeFileSync(filePath, updatedFileContent, 'utf-8'); return Promise.resolve(updatedFileContent); }, - } + }; } function attachPrint(context: Context) { @@ -185,7 +188,7 @@ const print = { yellow, red, blue, -} +}; export { print }; @@ -226,49 +229,47 @@ function fancy(message: string): void { } function debug(message: string, title: string = 'DEBUG'): void { - const topLine = `vvv -----[ ${title} ]----- vvv` - const botLine = `^^^ -----[ ${title} ]----- ^^^` + const topLine = `vvv -----[ ${title} ]----- vvv`; + const botLine = `^^^ -----[ ${title} ]----- ^^^`; - console.log(colors.rainbow(topLine)) - console.log(message) - console.log(colors.rainbow(botLine)) + console.log(colors.rainbow(topLine)); + console.log(message); + console.log(colors.rainbow(botLine)); } function table(data: string[][], options: any = {}): void { - let t + let t; switch (options.format) { case 'markdown': - const header = data.shift() + const header = data.shift(); t = new CLITable({ head: header, chars: CLI_TABLE_MARKDOWN, }) as CLITable.HorizontalTable; t.push(...data); t.unshift(columnHeaderDivider(t)); - break + break; case 'lean': t = new CLITable() as CLITable.HorizontalTable; t.push(...data); - break + break; default: t = new CLITable({ chars: CLI_TABLE_COMPACT, }) as CLITable.HorizontalTable; - t.push(...data) + t.push(...data); } - console.log(t.toString()) + console.log(t.toString()); } function columnHeaderDivider(cliTable: CLITable.Table): string[] { - return findWidths(cliTable).map(w => Array(w).join('-')) + return findWidths(cliTable).map(w => Array(w).join('-')); } function findWidths(cliTable: CLITable.Table): number[] { return [(cliTable as any).options.head] - .concat(getRows(cliTable)) - .reduce((colWidths, row) => row.map( - (str: string, i: number) => Math.max(`${str}`.length + 1, colWidths[i] || 1), - ), []); + .concat(getRows(cliTable)) + .reduce((colWidths, row) => row.map((str: string, i: number) => Math.max(`${str}`.length + 1, colWidths[i] || 1)), []); } function getRows(cliTable: CLITable.Table) { @@ -295,43 +296,43 @@ const CLI_TABLE_COMPACT = { right: '', 'right-mid': '', middle: ' ', -} +}; const CLI_TABLE_MARKDOWN = { ...CLI_TABLE_COMPACT, left: '|', right: '|', middle: '|', -} +}; function attachTemplate(context: Context) { context.template = { - async generate (opts: any): Promise { + async generate(opts: any): Promise { const ejs = require('ejs'); - const template = opts.template - const target = opts.target - const props = opts.props || {} + const template = opts.template; + const target = opts.target; + const props = opts.props || {}; const data = { props, - } + }; const directory = opts.directory; - const pathToTemplate = `${directory}/${template}` + const pathToTemplate = `${directory}/${template}`; if (!contextFileSystem.isFile(pathToTemplate)) { - throw new Error(`template not found ${pathToTemplate}`) + throw new Error(`template not found ${pathToTemplate}`); } const templateContent = contextFileSystem.read(pathToTemplate); - const content = ejs.render(templateContent, data) + const content = ejs.render(templateContent, data); if (target.length > 0) { - const dir = target.replace(/$(\/)*/g, '') - const dest = contextFileSystem.path(dir) - contextFileSystem.write(dest, content) + const dir = target.replace(/$(\/)*/g, ''); + const dest = contextFileSystem.path(dir); + contextFileSystem.write(dest, content); } - return content + return content; }, - } -} \ No newline at end of file + }; +} diff --git a/packages/amplify-cli/src/context-manager.ts b/packages/amplify-cli/src/context-manager.ts index 13fc5306a3..e87bd5ea28 100644 --- a/packages/amplify-cli/src/context-manager.ts +++ b/packages/amplify-cli/src/context-manager.ts @@ -12,7 +12,6 @@ export function constructContext(pluginPlatform: PluginPlatform, input: Input): } export function persistContext(context: Context): void { - // write to the backend and current backend - // and get the frontend plugin to write to the config files. + // write to the backend and current backend + // and get the frontend plugin to write to the config files. } - diff --git a/packages/amplify-cli/src/domain/add-plugin-result.ts b/packages/amplify-cli/src/domain/add-plugin-result.ts index 60c54cd953..ae8885d80e 100644 --- a/packages/amplify-cli/src/domain/add-plugin-result.ts +++ b/packages/amplify-cli/src/domain/add-plugin-result.ts @@ -1,14 +1,10 @@ import PluginVerificationResult from './plugin-verification-result'; export default class AddPluginResult { - constructor( - public isAdded: boolean = false, - public pluginVerificationResult?: PluginVerificationResult, - public error?: AddPluginError, - ) {} + constructor(public isAdded: boolean = false, public pluginVerificationResult?: PluginVerificationResult, public error?: AddPluginError) {} } export enum AddPluginError { - FailedVerification = 'FailedVerification', - Other = 'Other', -} \ No newline at end of file + FailedVerification = 'FailedVerification', + Other = 'Other', +} diff --git a/packages/amplify-cli/src/domain/amplify-event.ts b/packages/amplify-cli/src/domain/amplify-event.ts index ba190481d1..ba325e0db5 100644 --- a/packages/amplify-cli/src/domain/amplify-event.ts +++ b/packages/amplify-cli/src/domain/amplify-event.ts @@ -1,28 +1,20 @@ export enum AmplifyEvent { - PreInit = 'PreInit', - PostInit = 'PostInit', - PrePush = 'PrePush', - PostPush = 'PostPush', + PreInit = 'PreInit', + PostInit = 'PostInit', + PrePush = 'PrePush', + PostPush = 'PostPush', } -export class AmplifyEventData { -} +export class AmplifyEventData {} -export class AmplifyPreInitEventData extends AmplifyEventData { -} +export class AmplifyPreInitEventData extends AmplifyEventData {} -export class AmplifyPostInitEventData extends AmplifyEventData { -} +export class AmplifyPostInitEventData extends AmplifyEventData {} -export class AmplifyPrePushEventData extends AmplifyEventData { -} +export class AmplifyPrePushEventData extends AmplifyEventData {} -export class AmplifyPostPushEventData extends AmplifyEventData { -} +export class AmplifyPostPushEventData extends AmplifyEventData {} export class AmplifyEventArgs { - constructor( - public event: AmplifyEvent, - public data?: AmplifyEventData, - ) {} -} \ No newline at end of file + constructor(public event: AmplifyEvent, public data?: AmplifyEventData) {} +} diff --git a/packages/amplify-cli/src/domain/amplify-plugin-type.ts b/packages/amplify-cli/src/domain/amplify-plugin-type.ts index d88115ab3b..9f1ad9f506 100644 --- a/packages/amplify-cli/src/domain/amplify-plugin-type.ts +++ b/packages/amplify-cli/src/domain/amplify-plugin-type.ts @@ -1,6 +1,6 @@ export enum AmplifyPluginType { - category = 'category', - frontend = 'frontend', - provider = 'provider', - util = 'util', + category = 'category', + frontend = 'frontend', + provider = 'provider', + util = 'util', } diff --git a/packages/amplify-cli/src/domain/amplify-toolkit.ts b/packages/amplify-cli/src/domain/amplify-toolkit.ts index 5eb0fcf14e..efed3bd136 100644 --- a/packages/amplify-cli/src/domain/amplify-toolkit.ts +++ b/packages/amplify-cli/src/domain/amplify-toolkit.ts @@ -66,328 +66,305 @@ export default class AmplifyToolkit { private _getTriggerEnvVariables: any; private _getTriggerEnvInputs: any; - private _amplifyHelpersDirPath: string = - path.normalize(path.join(__dirname, '../extensions/amplify-helpers')); + private _amplifyHelpersDirPath: string = path.normalize(path.join(__dirname, '../extensions/amplify-helpers')); get buildResources(): any { - this._buildResources = this._buildResources || - require(path.join(this._amplifyHelpersDirPath, 'build-resources')).buildResources; + this._buildResources = this._buildResources || require(path.join(this._amplifyHelpersDirPath, 'build-resources')).buildResources; return this._buildResources; } get confirmPrompt(): any { - this._confirmPrompt = this._confirmPrompt || - require(path.join(this._amplifyHelpersDirPath, 'confirm-prompt')); + this._confirmPrompt = this._confirmPrompt || require(path.join(this._amplifyHelpersDirPath, 'confirm-prompt')); return this._confirmPrompt; } get constants(): any { - this._constants = this._constants || - require(path.join(this._amplifyHelpersDirPath, 'constants')); + this._constants = this._constants || require(path.join(this._amplifyHelpersDirPath, 'constants')); return this._constants; } get constructExeInfo(): any { - this._constructExeInfo = this._constructExeInfo || - require(path.join(this._amplifyHelpersDirPath, 'construct-exeInfo')).constructExeInfo; + this._constructExeInfo = + this._constructExeInfo || require(path.join(this._amplifyHelpersDirPath, 'construct-exeInfo')).constructExeInfo; return this._constructExeInfo; } get copyBatch(): any { - this._copyBatch = this._copyBatch || - require(path.join(this._amplifyHelpersDirPath, 'copy-batch')).copyBatch; + this._copyBatch = this._copyBatch || require(path.join(this._amplifyHelpersDirPath, 'copy-batch')).copyBatch; return this._copyBatch; } get crudFlow(): any { - this._crudFlow = this._crudFlow || - require(path.join(this._amplifyHelpersDirPath, 'permission-flow')).crudFlow; + this._crudFlow = this._crudFlow || require(path.join(this._amplifyHelpersDirPath, 'permission-flow')).crudFlow; return this._crudFlow; } get deleteProject(): any { - this._deleteProject = this._deleteProject || - require(path.join(this._amplifyHelpersDirPath, 'delete-project')).deleteProject; + this._deleteProject = this._deleteProject || require(path.join(this._amplifyHelpersDirPath, 'delete-project')).deleteProject; return this._deleteProject; } get executeProviderUtils(): any { - this._executeProviderUtils = this._executeProviderUtils || - require(path.join(this._amplifyHelpersDirPath, 'execute-provider-utils')).executeProviderUtils; + this._executeProviderUtils = + this._executeProviderUtils || require(path.join(this._amplifyHelpersDirPath, 'execute-provider-utils')).executeProviderUtils; return this._executeProviderUtils; } get getAllEnvs(): any { - this._getAllEnvs = this._getAllEnvs || - require(path.join(this._amplifyHelpersDirPath, 'get-all-envs')).getAllEnvs; + this._getAllEnvs = this._getAllEnvs || require(path.join(this._amplifyHelpersDirPath, 'get-all-envs')).getAllEnvs; return this._getAllEnvs; } get getPlugin(): any { - this._getPlugin = this._getPlugin || - require(path.join(this._amplifyHelpersDirPath, 'get-plugin')).getPlugin; + this._getPlugin = this._getPlugin || require(path.join(this._amplifyHelpersDirPath, 'get-plugin')).getPlugin; return this._getPlugin; } get getCategoryPlugins(): any { - this._getCategoryPlugins = this._getCategoryPlugins || - require(path.join(this._amplifyHelpersDirPath, 'get-category-plugins')).getCategoryPlugins; + this._getCategoryPlugins = + this._getCategoryPlugins || require(path.join(this._amplifyHelpersDirPath, 'get-category-plugins')).getCategoryPlugins; return this._getCategoryPlugins; } get getFrontendPlugins(): any { - this._getFrontendPlugins = this._getFrontendPlugins || - require(path.join(this._amplifyHelpersDirPath, 'get-frontend-plugins')).getFrontendPlugins; + this._getFrontendPlugins = + this._getFrontendPlugins || require(path.join(this._amplifyHelpersDirPath, 'get-frontend-plugins')).getFrontendPlugins; return this._getFrontendPlugins; } get getProviderPlugins(): any { - this._getProviderPlugins = this._getProviderPlugins || - require(path.join(this._amplifyHelpersDirPath, 'get-provider-plugins')).getProviderPlugins; + this._getProviderPlugins = + this._getProviderPlugins || require(path.join(this._amplifyHelpersDirPath, 'get-provider-plugins')).getProviderPlugins; return this._getProviderPlugins; } get getEnvDetails(): any { - this._getEnvDetails = this._getEnvDetails || - require(path.join(this._amplifyHelpersDirPath, 'get-env-details')).getEnvDetails; + this._getEnvDetails = this._getEnvDetails || require(path.join(this._amplifyHelpersDirPath, 'get-env-details')).getEnvDetails; return this._getEnvDetails; } get getEnvInfo(): any { - this._getEnvInfo = this._getEnvInfo || - require(path.join(this._amplifyHelpersDirPath, 'get-env-info')).getEnvInfo; + this._getEnvInfo = this._getEnvInfo || require(path.join(this._amplifyHelpersDirPath, 'get-env-info')).getEnvInfo; return this._getEnvInfo; } get getPluginInstance(): any { - this._getPluginInstance = this._getPluginInstance || - require(path.join(this._amplifyHelpersDirPath, 'get-plugin-instance')).getPluginInstance; + this._getPluginInstance = + this._getPluginInstance || require(path.join(this._amplifyHelpersDirPath, 'get-plugin-instance')).getPluginInstance; return this._getPluginInstance; } get getProjectConfig(): any { - this._getProjectConfig = this._getProjectConfig || - require(path.join(this._amplifyHelpersDirPath, 'get-project-config')).getProjectConfig; + this._getProjectConfig = + this._getProjectConfig || require(path.join(this._amplifyHelpersDirPath, 'get-project-config')).getProjectConfig; return this._getProjectConfig; } get getProjectDetails(): any { - this._getProjectDetails = this._getProjectDetails || - require(path.join(this._amplifyHelpersDirPath, 'get-project-details')).getProjectDetails; + this._getProjectDetails = + this._getProjectDetails || require(path.join(this._amplifyHelpersDirPath, 'get-project-details')).getProjectDetails; return this._getProjectDetails; } get getProjectMeta(): any { - this._getProjectMeta = this._getProjectMeta || - require(path.join(this._amplifyHelpersDirPath, 'get-project-meta')).getProjectMeta; + this._getProjectMeta = this._getProjectMeta || require(path.join(this._amplifyHelpersDirPath, 'get-project-meta')).getProjectMeta; return this._getProjectMeta; } get getResourceStatus(): any { - this._getResourceStatus = this._getResourceStatus || - require(path.join(this._amplifyHelpersDirPath, 'resource-status')).getResourceStatus; + this._getResourceStatus = + this._getResourceStatus || require(path.join(this._amplifyHelpersDirPath, 'resource-status')).getResourceStatus; return this._getResourceStatus; } get getResourceOutputs(): any { - this._getResourceOutputs = this._getResourceOutputs || - require(path.join(this._amplifyHelpersDirPath, 'get-resource-outputs')).getResourceOutputs; + this._getResourceOutputs = + this._getResourceOutputs || require(path.join(this._amplifyHelpersDirPath, 'get-resource-outputs')).getResourceOutputs; return this._getResourceOutputs; } get getWhen(): any { - this._getWhen = this._getWhen || - require(path.join(this._amplifyHelpersDirPath, 'get-when-function')).getWhen; + this._getWhen = this._getWhen || require(path.join(this._amplifyHelpersDirPath, 'get-when-function')).getWhen; return this._getWhen; } get inputValidation(): any { - this._inputValidation = this._inputValidation || - require(path.join(this._amplifyHelpersDirPath, 'input-validation')).inputValidation; + this._inputValidation = this._inputValidation || require(path.join(this._amplifyHelpersDirPath, 'input-validation')).inputValidation; return this._inputValidation; } get isRunningOnEC2(): any { - this._isRunningOnEC2 = this._isRunningOnEC2 || - require(path.join(this._amplifyHelpersDirPath, 'is-running-on-EC2')).isRunningOnEC2; + this._isRunningOnEC2 = this._isRunningOnEC2 || require(path.join(this._amplifyHelpersDirPath, 'is-running-on-EC2')).isRunningOnEC2; return this._isRunningOnEC2; } get listCategories(): any { - this._listCategories = this._listCategories || - require(path.join(this._amplifyHelpersDirPath, 'list-categories')).listCategories; + this._listCategories = this._listCategories || require(path.join(this._amplifyHelpersDirPath, 'list-categories')).listCategories; return this._listCategories; } get makeId(): any { - this._makeId = this._makeId || - require(path.join(this._amplifyHelpersDirPath, 'make-id')).makeId; + this._makeId = this._makeId || require(path.join(this._amplifyHelpersDirPath, 'make-id')).makeId; return this._makeId; } get openEditor(): any { - this._openEditor = this._openEditor || - require(path.join(this._amplifyHelpersDirPath, 'open-editor')).openEditor; + this._openEditor = this._openEditor || require(path.join(this._amplifyHelpersDirPath, 'open-editor')).openEditor; return this._openEditor; } get onCategoryOutputsChange(): any { - this._onCategoryOutputsChange = this._onCategoryOutputsChange || - require(path.join(this._amplifyHelpersDirPath, 'on-category-outputs-change')).onCategoryOutputsChange; + this._onCategoryOutputsChange = + this._onCategoryOutputsChange || + require(path.join(this._amplifyHelpersDirPath, 'on-category-outputs-change')).onCategoryOutputsChange; return this._onCategoryOutputsChange; } get pathManager(): any { - this._pathManager = this._pathManager || - require(path.join(this._amplifyHelpersDirPath, 'path-manager')); + this._pathManager = this._pathManager || require(path.join(this._amplifyHelpersDirPath, 'path-manager')); return this._pathManager; } get pressEnterToContinue(): any { - this._pressEnterToContinue = this._pressEnterToContinue || - require(path.join(this._amplifyHelpersDirPath, 'press-enter-to-continue')); + this._pressEnterToContinue = this._pressEnterToContinue || require(path.join(this._amplifyHelpersDirPath, 'press-enter-to-continue')); return this._pressEnterToContinue; } get pushResources(): any { - this._pushResources = this._pushResources || - require(path.join(this._amplifyHelpersDirPath, 'push-resources')).pushResources; + this._pushResources = this._pushResources || require(path.join(this._amplifyHelpersDirPath, 'push-resources')).pushResources; return this._pushResources; } get readJsonFile(): any { - this._readJsonFile = this._readJsonFile || - require(path.join(this._amplifyHelpersDirPath, 'read-json-file')).readJsonFile; + this._readJsonFile = this._readJsonFile || require(path.join(this._amplifyHelpersDirPath, 'read-json-file')).readJsonFile; return this._readJsonFile; } get removeEnvFromCloud(): any { - this._removeEnvFromCloud = this._removeEnvFromCloud || - require(path.join(this._amplifyHelpersDirPath, 'remove-env-from-cloud')).removeEnvFromCloud; + this._removeEnvFromCloud = + this._removeEnvFromCloud || require(path.join(this._amplifyHelpersDirPath, 'remove-env-from-cloud')).removeEnvFromCloud; return this._removeEnvFromCloud; } get removeResource(): any { - this._removeResource = this._removeResource || - require(path.join(this._amplifyHelpersDirPath, 'remove-resource')).removeResource; + this._removeResource = this._removeResource || require(path.join(this._amplifyHelpersDirPath, 'remove-resource')).removeResource; return this._removeResource; } get sharedQuestions(): any { - this._sharedQuestions = this._sharedQuestions || - require(path.join(this._amplifyHelpersDirPath, 'shared-questions')).sharedQuestions; + this._sharedQuestions = this._sharedQuestions || require(path.join(this._amplifyHelpersDirPath, 'shared-questions')).sharedQuestions; return this._sharedQuestions; } get showHelp(): any { - this._showHelp = this._showHelp || - require(path.join(this._amplifyHelpersDirPath, 'show-help')).showHelp; + this._showHelp = this._showHelp || require(path.join(this._amplifyHelpersDirPath, 'show-help')).showHelp; return this._showHelp; } get showAllHelp(): any { - this._showAllHelp = this._showAllHelp || - require(path.join(this._amplifyHelpersDirPath, 'show-all-help')).showAllHelp; + this._showAllHelp = this._showAllHelp || require(path.join(this._amplifyHelpersDirPath, 'show-all-help')).showAllHelp; return this._showAllHelp; } get showHelpfulProviderLinks(): any { - this._showHelpfulProviderLinks = this._showHelpfulProviderLinks || - require(path.join(this._amplifyHelpersDirPath, 'show-helpful-provider-links')).showHelpfulProviderLinks; + this._showHelpfulProviderLinks = + this._showHelpfulProviderLinks || + require(path.join(this._amplifyHelpersDirPath, 'show-helpful-provider-links')).showHelpfulProviderLinks; return this._showHelpfulProviderLinks; } get showResourceTable(): any { - this._showResourceTable = this._showResourceTable || - require(path.join(this._amplifyHelpersDirPath, 'resource-status')).showResourceTable; + this._showResourceTable = + this._showResourceTable || require(path.join(this._amplifyHelpersDirPath, 'resource-status')).showResourceTable; return this._showResourceTable; } get serviceSelectionPrompt(): any { - this._serviceSelectionPrompt = this._serviceSelectionPrompt || - require(path.join(this._amplifyHelpersDirPath, 'service-select-prompt')).serviceSelectionPrompt; + this._serviceSelectionPrompt = + this._serviceSelectionPrompt || require(path.join(this._amplifyHelpersDirPath, 'service-select-prompt')).serviceSelectionPrompt; return this._serviceSelectionPrompt; } get updateProjectConfig(): any { - this._updateProjectConfig = this._updateProjectConfig || - require(path.join(this._amplifyHelpersDirPath, 'update-project-config')).updateProjectConfig; + this._updateProjectConfig = + this._updateProjectConfig || require(path.join(this._amplifyHelpersDirPath, 'update-project-config')).updateProjectConfig; return this._updateProjectConfig; } get updateamplifyMetaAfterResourceUpdate(): any { - this._updateamplifyMetaAfterResourceUpdate = this._updateamplifyMetaAfterResourceUpdate || - require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterResourceUpdate; + this._updateamplifyMetaAfterResourceUpdate = + this._updateamplifyMetaAfterResourceUpdate || + require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterResourceUpdate; return this._updateamplifyMetaAfterResourceUpdate; } get updateamplifyMetaAfterResourceAdd(): any { - this._updateamplifyMetaAfterResourceAdd = this._updateamplifyMetaAfterResourceAdd || - require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterResourceAdd; + this._updateamplifyMetaAfterResourceAdd = + this._updateamplifyMetaAfterResourceAdd || + require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterResourceAdd; return this._updateamplifyMetaAfterResourceAdd; } get updateamplifyMetaAfterResourceDelete(): any { - this._updateamplifyMetaAfterResourceDelete = this._updateamplifyMetaAfterResourceDelete || - require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterResourceDelete; + this._updateamplifyMetaAfterResourceDelete = + this._updateamplifyMetaAfterResourceDelete || + require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterResourceDelete; return this._updateamplifyMetaAfterResourceDelete; } get updateProvideramplifyMeta(): any { - this._updateProvideramplifyMeta = this._updateProvideramplifyMeta || - require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateProvideramplifyMeta; + this._updateProvideramplifyMeta = + this._updateProvideramplifyMeta || require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateProvideramplifyMeta; return this._updateProvideramplifyMeta; } get updateamplifyMetaAfterPush(): any { - this._updateamplifyMetaAfterPush = this._updateamplifyMetaAfterPush || - require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterPush; + this._updateamplifyMetaAfterPush = + this._updateamplifyMetaAfterPush || require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterPush; return this._updateamplifyMetaAfterPush; } get updateamplifyMetaAfterBuild(): any { - this._updateamplifyMetaAfterBuild = this._updateamplifyMetaAfterBuild || - require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterBuild; + this._updateamplifyMetaAfterBuild = + this._updateamplifyMetaAfterBuild || + require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateamplifyMetaAfterBuild; return this._updateamplifyMetaAfterBuild; } get updateAmplifyMetaAfterPackage(): any { - this._updateAmplifyMetaAfterPackage = this._updateAmplifyMetaAfterPackage || - require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateAmplifyMetaAfterPackage; + this._updateAmplifyMetaAfterPackage = + this._updateAmplifyMetaAfterPackage || + require(path.join(this._amplifyHelpersDirPath, 'update-amplify-meta')).updateAmplifyMetaAfterPackage; return this._updateAmplifyMetaAfterPackage; } get updateBackendConfigAfterResourceAdd(): any { - this._updateBackendConfigAfterResourceAdd = this._updateBackendConfigAfterResourceAdd || - require(path.join(this._amplifyHelpersDirPath, 'update-backend-config')).updateBackendConfigAfterResourceAdd; + this._updateBackendConfigAfterResourceAdd = + this._updateBackendConfigAfterResourceAdd || + require(path.join(this._amplifyHelpersDirPath, 'update-backend-config')).updateBackendConfigAfterResourceAdd; return this._updateBackendConfigAfterResourceAdd; } get updateBackendConfigAfterResourceRemove(): any { - this._updateBackendConfigAfterResourceRemove = this._updateBackendConfigAfterResourceRemove || - require(path.join(this._amplifyHelpersDirPath, 'update-backend-config')).updateBackendConfigAfterResourceRemove; + this._updateBackendConfigAfterResourceRemove = + this._updateBackendConfigAfterResourceRemove || + require(path.join(this._amplifyHelpersDirPath, 'update-backend-config')).updateBackendConfigAfterResourceRemove; return this._updateBackendConfigAfterResourceRemove; } get loadEnvResourceParameters(): any { - this._loadEnvResourceParameters = this._loadEnvResourceParameters || - require(path.join(this._amplifyHelpersDirPath, 'envResourceParams')).loadEnvResourceParameters; + this._loadEnvResourceParameters = + this._loadEnvResourceParameters || require(path.join(this._amplifyHelpersDirPath, 'envResourceParams')).loadEnvResourceParameters; return this._loadEnvResourceParameters; } get saveEnvResourceParameters(): any { - this._saveEnvResourceParameters = this._saveEnvResourceParameters || - require(path.join(this._amplifyHelpersDirPath, 'envResourceParams')).saveEnvResourceParameters; + this._saveEnvResourceParameters = + this._saveEnvResourceParameters || require(path.join(this._amplifyHelpersDirPath, 'envResourceParams')).saveEnvResourceParameters; return this._saveEnvResourceParameters; } get removeResourceParameters(): any { - this._removeResourceParameters = this._removeResourceParameters || - require(path.join(this._amplifyHelpersDirPath, 'envResourceParams')).removeResourceParameters; + this._removeResourceParameters = + this._removeResourceParameters || require(path.join(this._amplifyHelpersDirPath, 'envResourceParams')).removeResourceParameters; return this._removeResourceParameters; } get triggerFlow(): any { - this._triggerFlow = this._triggerFlow || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).triggerFlow; + this._triggerFlow = this._triggerFlow || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).triggerFlow; return this._triggerFlow; } get addTrigger(): any { - this._addTrigger = this._addTrigger || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).addTrigger; + this._addTrigger = this._addTrigger || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).addTrigger; return this._addTrigger; } get updateTrigger(): any { - this._updateTrigger = this._updateTrigger || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).updateTrigger; + this._updateTrigger = this._updateTrigger || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).updateTrigger; return this._updateTrigger; } get deleteTrigger(): any { - this._deleteTrigger = this._deleteTrigger || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).deleteTrigger; + this._deleteTrigger = this._deleteTrigger || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).deleteTrigger; return this._deleteTrigger; } get deleteAllTriggers(): any { - this._deleteAllTriggers = this._deleteAllTriggers || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).deleteAllTriggers; + this._deleteAllTriggers = this._deleteAllTriggers || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).deleteAllTriggers; return this._deleteAllTriggers; } get deleteDeselectedTriggers(): any { - this._deleteDeselectedTriggers = this._deleteDeselectedTriggers || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).deleteDeselectedTriggers; + this._deleteDeselectedTriggers = + this._deleteDeselectedTriggers || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).deleteDeselectedTriggers; return this._deleteDeselectedTriggers; } get dependsOnBlock(): any { - this._dependsOnBlock = this._dependsOnBlock || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).dependsOnBlock; + this._dependsOnBlock = this._dependsOnBlock || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).dependsOnBlock; return this._dependsOnBlock; } get getTriggerMetadata(): any { - this._getTriggerMetadata = this._getTriggerMetadata || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerMetadata; + this._getTriggerMetadata = + this._getTriggerMetadata || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerMetadata; return this._getTriggerMetadata; } get getTriggerPermissions(): any { - this._getTriggerPermissions = this._getTriggerPermissions || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerPermissions; + this._getTriggerPermissions = + this._getTriggerPermissions || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerPermissions; return this._getTriggerPermissions; } get getTriggerEnvVariables(): any { - this._getTriggerEnvVariables = this._getTriggerEnvVariables || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerEnvVariables; + this._getTriggerEnvVariables = + this._getTriggerEnvVariables || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerEnvVariables; return this._getTriggerEnvVariables; } get getTriggerEnvInputs(): any { - this._getTriggerEnvInputs = this._getTriggerEnvInputs || - require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerEnvInputs; + this._getTriggerEnvInputs = + this._getTriggerEnvInputs || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerEnvInputs; return this._getTriggerEnvInputs; } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/domain/constants.ts b/packages/amplify-cli/src/domain/constants.ts index 90e241a48f..07f884d37e 100644 --- a/packages/amplify-cli/src/domain/constants.ts +++ b/packages/amplify-cli/src/domain/constants.ts @@ -18,4 +18,4 @@ export default { GlobalNodeModules: 'global-node-modules', ExecuteAmplifyCommand: 'executeAmplifyCommand', HandleAmplifyEvent: 'handleAmplifyEvent', -} \ No newline at end of file +}; diff --git a/packages/amplify-cli/src/domain/context.ts b/packages/amplify-cli/src/domain/context.ts index 6ece204d87..de8590d50c 100644 --- a/packages/amplify-cli/src/domain/context.ts +++ b/packages/amplify-cli/src/domain/context.ts @@ -12,4 +12,4 @@ export default class Context { // After the new platform is stablized, we probably should disallow arbituary // properties to be attached to the context object. [key: string]: any; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/domain/input-verification-result.ts b/packages/amplify-cli/src/domain/input-verification-result.ts index 49956f7fef..3cdf7f4bcc 100644 --- a/packages/amplify-cli/src/domain/input-verification-result.ts +++ b/packages/amplify-cli/src/domain/input-verification-result.ts @@ -2,6 +2,6 @@ export default class InputVerificationResult { constructor( public verified: boolean = false, public helpCommandAvailable: boolean = false, - public message: string | undefined = undefined, + public message: string | undefined = undefined ) {} -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/domain/input.ts b/packages/amplify-cli/src/domain/input.ts index 56167a8f33..4142bd7af9 100644 --- a/packages/amplify-cli/src/domain/input.ts +++ b/packages/amplify-cli/src/domain/input.ts @@ -4,9 +4,9 @@ export default class Input { command?: string; subCommands?: string[]; options?: { - [key: string]: string | boolean, + [key: string]: string | boolean; }; constructor(argv: Array) { - this.argv = (new Array()).concat(argv); + this.argv = new Array().concat(argv); } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/domain/inquirer-helper.ts b/packages/amplify-cli/src/domain/inquirer-helper.ts index bf141af09f..cae366be25 100644 --- a/packages/amplify-cli/src/domain/inquirer-helper.ts +++ b/packages/amplify-cli/src/domain/inquirer-helper.ts @@ -3,9 +3,9 @@ const inquirer = require('inquirer'); export const EXPAND = ' >'; export type InquirerOption = { - name: string, - value: any, - short: string, + name: string; + value: any; + short: string; }; -export default inquirer; \ No newline at end of file +export default inquirer; diff --git a/packages/amplify-cli/src/domain/plugin-collection.ts b/packages/amplify-cli/src/domain/plugin-collection.ts index 160ebd7f0b..6a8d291453 100644 --- a/packages/amplify-cli/src/domain/plugin-collection.ts +++ b/packages/amplify-cli/src/domain/plugin-collection.ts @@ -1,5 +1,5 @@ import PluginInfo from './plugin-info'; export default class PluginCollection { - [key: string]: Array -} \ No newline at end of file + [key: string]: Array; +} diff --git a/packages/amplify-cli/src/domain/plugin-info.ts b/packages/amplify-cli/src/domain/plugin-info.ts index 34ad127005..160a624ef8 100644 --- a/packages/amplify-cli/src/domain/plugin-info.ts +++ b/packages/amplify-cli/src/domain/plugin-info.ts @@ -1,10 +1,5 @@ import PluginManifest from './plugin-manifest'; export default class PluginInfo { - constructor( - public packageName: string, - public packageVersion: string, - public packageLocation: string, - public manifest: PluginManifest, - ) {} -} \ No newline at end of file + constructor(public packageName: string, public packageVersion: string, public packageLocation: string, public manifest: PluginManifest) {} +} diff --git a/packages/amplify-cli/src/domain/plugin-manifest.ts b/packages/amplify-cli/src/domain/plugin-manifest.ts index 716953c453..0fe54fa11b 100644 --- a/packages/amplify-cli/src/domain/plugin-manifest.ts +++ b/packages/amplify-cli/src/domain/plugin-manifest.ts @@ -7,8 +7,8 @@ export default class PluginManifest { public aliases?: string[], public commands?: string[], public commandAliases?: { - [key: string]: string, + [key: string]: string; }, - public eventHandlers?: AmplifyEvent[]) { - } -} \ No newline at end of file + public eventHandlers?: AmplifyEvent[] + ) {} +} diff --git a/packages/amplify-cli/src/domain/plugin-platform.ts b/packages/amplify-cli/src/domain/plugin-platform.ts index b138cfe619..b932009b01 100644 --- a/packages/amplify-cli/src/domain/plugin-platform.ts +++ b/packages/amplify-cli/src/domain/plugin-platform.ts @@ -5,14 +5,8 @@ const SECONDSINADAY = 86400; export default class PluginPlatform { constructor() { - this.pluginDirectories = [ - constants.LocalNodeModules, - constants.ParentDirectory, - constants.GlobalNodeModules, - ]; - this.pluginPrefixes = [ - constants.AmplifyPrefix, - ]; + this.pluginDirectories = [constants.LocalNodeModules, constants.ParentDirectory, constants.GlobalNodeModules]; + this.pluginPrefixes = [constants.AmplifyPrefix]; this.userAddedLocations = []; this.lastScanTime = new Date(); this.maxScanIntervalInSeconds = SECONDSINADAY; @@ -27,4 +21,4 @@ export default class PluginPlatform { maxScanIntervalInSeconds: number; plugins: PluginCollection; excluded: PluginCollection; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/domain/plugin-verification-result.ts b/packages/amplify-cli/src/domain/plugin-verification-result.ts index 3c1ce5363a..fe0b2891ae 100644 --- a/packages/amplify-cli/src/domain/plugin-verification-result.ts +++ b/packages/amplify-cli/src/domain/plugin-verification-result.ts @@ -6,15 +6,15 @@ export default class PluginVerificationResult { public error?: PluginVerificationError, public errorInfo?: any, public packageJson?: any, - public manifest?: PluginManifest, + public manifest?: PluginManifest ) {} } export enum PluginVerificationError { - PluginDirPathNotExist = 'PluginDirPathNotExist', - InvalidNodePackage = 'InvalidNodePackage', - MissingManifest = 'MissingManifest', - InvalidManifest = 'InvalidManifest', - MissingExecuteAmplifyCommandMethod = 'MissingExecuteAmplifyCommandMethod', - MissingHandleAmplifyEventMethod = 'MissingHandleAmplifyEventMethod', -} \ No newline at end of file + PluginDirPathNotExist = 'PluginDirPathNotExist', + InvalidNodePackage = 'InvalidNodePackage', + MissingManifest = 'MissingManifest', + InvalidManifest = 'InvalidManifest', + MissingExecuteAmplifyCommandMethod = 'MissingExecuteAmplifyCommandMethod', + MissingHandleAmplifyEventMethod = 'MissingHandleAmplifyEventMethod', +} diff --git a/packages/amplify-cli/src/execution-manager.ts b/packages/amplify-cli/src/execution-manager.ts index c5dafe48bf..576d086288 100644 --- a/packages/amplify-cli/src/execution-manager.ts +++ b/packages/amplify-cli/src/execution-manager.ts @@ -7,17 +7,16 @@ import PluginInfo from './domain/plugin-info'; import inquirer from './domain/inquirer-helper'; import constants from './domain/constants'; import { - AmplifyEvent, - AmplifyEventArgs, - AmplifyPreInitEventData, - AmplifyPostInitEventData, - AmplifyPrePushEventData, - AmplifyPostPushEventData, + AmplifyEvent, + AmplifyEventArgs, + AmplifyPreInitEventData, + AmplifyPostInitEventData, + AmplifyPrePushEventData, + AmplifyPostPushEventData, } from './domain/amplify-event'; export async function executeCommand(context: Context) { - const pluginCandidates = getPluginsWithNameAndCommand(context.pluginPlatform, - context.input.plugin!, context.input.command!); + const pluginCandidates = getPluginsWithNameAndCommand(context.pluginPlatform, context.input.plugin!, context.input.command!); if (pluginCandidates.length === 1) { await executePluginModuleCommand(context, pluginCandidates[0]); @@ -26,7 +25,7 @@ export async function executeCommand(context: Context) { type: 'list', name: 'section', message: 'Select the module to execute', - choices: pluginCandidates.map((plugin) => { + choices: pluginCandidates.map(plugin => { const pluginOptions = { name: plugin.packageName + '@' + plugin.packageVersion, value: plugin, @@ -50,16 +49,16 @@ async function executePluginModuleCommand(context: Context, plugin: PluginInfo) await raisePreEvent(context); const pluginModule = require(plugin.packageLocation); - if (pluginModule.hasOwnProperty(constants.ExecuteAmplifyCommand) && - typeof pluginModule[constants.ExecuteAmplifyCommand] === 'function') { + if ( + pluginModule.hasOwnProperty(constants.ExecuteAmplifyCommand) && + typeof pluginModule[constants.ExecuteAmplifyCommand] === 'function' + ) { attachContextExtensions(context, plugin); await pluginModule.executeAmplifyCommand(context); } else { // if the module does not have the executeAmplifyCommand method, // fall back to the old approach by scanning the command folder and locate the command file - let commandFilepath = path.normalize( - path.join(plugin.packageLocation, 'commands', plugin.manifest.name, context.input.command!), - ); + let commandFilepath = path.normalize(path.join(plugin.packageLocation, 'commands', plugin.manifest.name, context.input.command!)); if (context.input.subCommands && context.input.subCommands.length > 0) { commandFilepath = path.join(commandFilepath, ...context.input.subCommands!); } @@ -73,9 +72,7 @@ async function executePluginModuleCommand(context: Context, plugin: PluginInfo) } if (!commandModule) { - commandFilepath = path.normalize( - path.join(plugin.packageLocation, 'commands', plugin.manifest.name), - ); + commandFilepath = path.normalize(path.join(plugin.packageLocation, 'commands', plugin.manifest.name)); try { commandModule = require(commandFilepath); } catch (e) { @@ -112,17 +109,11 @@ async function raisePreEvent(context: Context) { } async function raisePreInitEvent(context: Context) { - await raiseEvent(context, new AmplifyEventArgs( - AmplifyEvent.PreInit, - new AmplifyPreInitEventData(), - )); + await raiseEvent(context, new AmplifyEventArgs(AmplifyEvent.PreInit, new AmplifyPreInitEventData())); } async function raisePrePushEvent(context: Context) { - await raiseEvent(context, new AmplifyEventArgs( - AmplifyEvent.PrePush, - new AmplifyPrePushEventData(), - )); + await raiseEvent(context, new AmplifyEventArgs(AmplifyEvent.PrePush, new AmplifyPrePushEventData())); } async function raisePostEvent(context: Context) { @@ -136,38 +127,34 @@ async function raisePostEvent(context: Context) { } async function raisePostInitEvent(context: Context) { - await raiseEvent(context, new AmplifyEventArgs( - AmplifyEvent.PostInit, - new AmplifyPostPushEventData(), - )); + await raiseEvent(context, new AmplifyEventArgs(AmplifyEvent.PostInit, new AmplifyPostPushEventData())); } async function raisePostPushEvent(context: Context) { - await raiseEvent(context, new AmplifyEventArgs( - AmplifyEvent.PostPush, - new AmplifyPostInitEventData(), - )); + await raiseEvent(context, new AmplifyEventArgs(AmplifyEvent.PostPush, new AmplifyPostInitEventData())); } export async function raiseEvent(context: Context, args: AmplifyEventArgs) { const plugins = getPluginsWithEventHandler(context.pluginPlatform, args.event); if (plugins.length > 0) { const sequential = require('promise-sequential'); - const eventHandlers = plugins.filter((plugin) => { - const exists = fs.existsSync(plugin.packageLocation); - return exists; - }).map((plugin) => { - const eventHandler = async () => { - try { - attachContextExtensions(context, plugin); - const pluginModule = require(plugin.packageLocation); - await pluginModule.handleAmplifyEvent(context, args); - } catch { - // no need to need anything - } - }; - return eventHandler; - }); + const eventHandlers = plugins + .filter(plugin => { + const exists = fs.existsSync(plugin.packageLocation); + return exists; + }) + .map(plugin => { + const eventHandler = async () => { + try { + attachContextExtensions(context, plugin); + const pluginModule = require(plugin.packageLocation); + await pluginModule.handleAmplifyEvent(context, args); + } catch { + // no need to need anything + } + }; + return eventHandler; + }); await sequential(eventHandlers); } } @@ -179,7 +166,7 @@ function attachContextExtensions(context: Context, plugin: PluginInfo) { const stats = fs.statSync(extensionsDirPath); if (stats.isDirectory()) { const itemNames = fs.readdirSync(extensionsDirPath); - itemNames.forEach((itemName) => { + itemNames.forEach(itemName => { const itemPath = path.join(extensionsDirPath, itemName); let itemModule; try { @@ -188,7 +175,7 @@ function attachContextExtensions(context: Context, plugin: PluginInfo) { } catch (e) { // do nothing } - }) + }); } } -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/index.ts b/packages/amplify-cli/src/index.ts index 7cae6ce590..513f67bfea 100644 --- a/packages/amplify-cli/src/index.ts +++ b/packages/amplify-cli/src/index.ts @@ -9,14 +9,14 @@ import constants from './domain/constants'; import * as path from 'path'; // entry from commandline -export async function run() : Promise { +export async function run(): Promise { try { let pluginPlatform = await getPluginPlatform(); let input = getCommandLineInput(pluginPlatform); let verificationResult = verifyInput(pluginPlatform, input); - // invalid input might be because plugin platform might have been updated, - // scan and try again + // invalid input might be because plugin platform might have been updated, + // scan and try again if (!verificationResult.verified) { if (verificationResult.message) { print.warning(verificationResult.message); @@ -39,7 +39,7 @@ export async function run() : Promise { persistContext(context); return 0; } catch (e) { - // ToDo: add logging to the core, and log execution errors using the unified core logging. + // ToDo: add logging to the core, and log execution errors using the unified core logging. if (e.message) { print.error(e.message); } @@ -77,7 +77,7 @@ export async function execute(input: Input) { persistContext(context); return 0; } catch (e) { - // ToDo: add logging to the core, and log execution errors using the unified core logging. + // ToDo: add logging to the core, and log execution errors using the unified core logging. if (e.message) { print.error(e.message); } @@ -93,4 +93,3 @@ export async function executeAmplifyCommand(context: Context) { const commandModule = require(commandPath); await commandModule.run(context); } - diff --git a/packages/amplify-cli/src/input-manager.ts b/packages/amplify-cli/src/input-manager.ts index 081bf73660..5533ab72cd 100644 --- a/packages/amplify-cli/src/input-manager.ts +++ b/packages/amplify-cli/src/input-manager.ts @@ -101,12 +101,11 @@ export function verifyInput(pluginPlatform: PluginPlatform, input: Input): Input for (let i = 0; i < pluginCandidates.length; i++) { const { name, commands, commandAliases } = pluginCandidates[i].manifest; - if ((commands && commands!.includes(Constant.HELP)) || - (commandAliases && Object.keys(commandAliases).includes(Constant.HELP))) { + if ((commands && commands!.includes(Constant.HELP)) || (commandAliases && Object.keys(commandAliases).includes(Constant.HELP))) { result.helpCommandAvailable = true; } - if ((commands && commands!.includes(input.command!))) { + if (commands && commands!.includes(input.command!)) { result.verified = true; break; } @@ -123,14 +122,12 @@ export function verifyInput(pluginPlatform: PluginPlatform, input: Input): Input result.verified = true; break; } - if (input.options && input.options[Constant.VERSION] && - commands && commands!.includes(Constant.VERSION)) { + if (input.options && input.options[Constant.VERSION] && commands && commands!.includes(Constant.VERSION)) { input.command = Constant.VERSION; result.verified = true; break; } - if (input.options && input.options[Constant.HELP] && - commands && commands!.includes(Constant.HELP)) { + if (input.options && input.options[Constant.HELP] && commands && commands!.includes(Constant.HELP)) { input.command = Constant.HELP; result.verified = true; break; @@ -164,4 +161,4 @@ export function verifyInput(pluginPlatform: PluginPlatform, input: Input): Input } return result; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/plugin-helpers/access-plugins-file.ts b/packages/amplify-cli/src/plugin-helpers/access-plugins-file.ts index aa9f20ff8d..4ea6a91b2e 100644 --- a/packages/amplify-cli/src/plugin-helpers/access-plugins-file.ts +++ b/packages/amplify-cli/src/plugin-helpers/access-plugins-file.ts @@ -12,7 +12,7 @@ export function readPluginsJsonFileSync(): PluginPlatform | undefined { const pluginsFilePath = getPluginsJsonFilePath(); if (fs.existsSync(pluginsFilePath)) { - result = readJsonFileSync(pluginsFilePath) + result = readJsonFileSync(pluginsFilePath); } return result; @@ -25,7 +25,7 @@ export async function readPluginsJsonFile(): Promise const exists = await fs.pathExists(pluginsFilePath); if (exists) { - result = await readJsonFile(pluginsFilePath) + result = await readJsonFile(pluginsFilePath); } return result; @@ -51,7 +51,6 @@ export async function writePluginsJsonFile(pluginsJson: PluginPlatform): Promise await fs.writeFile(pluginsJsonFilePath, jsonString, 'utf8'); } - function getPluginsJsonFilePath(): string { return path.join(getSystemDotAmplifyDirPath(), getPluginsJsonFileName()); } @@ -69,4 +68,4 @@ function getPluginsJsonFileName(): string { } return result; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/plugin-helpers/compare-plugins.ts b/packages/amplify-cli/src/plugin-helpers/compare-plugins.ts index 20293ed75b..5e751d87a0 100644 --- a/packages/amplify-cli/src/plugin-helpers/compare-plugins.ts +++ b/packages/amplify-cli/src/plugin-helpers/compare-plugins.ts @@ -5,10 +5,9 @@ export function twoPluginsAreTheSame(plugin0: PluginInfo, plugin1: PluginInfo) { return true; } - if (plugin0.packageName === plugin1.packageName && - plugin0.packageVersion === plugin1.packageVersion) { + if (plugin0.packageName === plugin1.packageName && plugin0.packageVersion === plugin1.packageVersion) { return true; } return false; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/plugin-helpers/create-new-plugin.ts b/packages/amplify-cli/src/plugin-helpers/create-new-plugin.ts index dada33df3a..ec98a1082a 100644 --- a/packages/amplify-cli/src/plugin-helpers/create-new-plugin.ts +++ b/packages/amplify-cli/src/plugin-helpers/create-new-plugin.ts @@ -2,7 +2,7 @@ import path from 'path'; import fs from 'fs-extra'; import inquirer from '../domain/inquirer-helper'; import Context from '../domain/context'; -import Constant from '../domain/constants' +import Constant from '../domain/constants'; import { AmplifyEvent } from '../domain/amplify-event'; import { AmplifyPluginType } from '../domain/amplify-plugin-type'; import { readJsonFileSync } from '../utils/readJsonFile'; @@ -12,8 +12,7 @@ import { createIndentation } from './display-plugin-platform'; const INDENTATIONSPACE = 4; -export default async function createNewPlugin(context: Context, pluginParentDirPath: string): -Promise { +export default async function createNewPlugin(context: Context, pluginParentDirPath: string): Promise { const pluginName = await getPluginName(context, pluginParentDirPath); if (pluginName) { return await copyAndUpdateTemplateFiles(context, pluginParentDirPath, pluginName!); @@ -21,12 +20,12 @@ Promise { return undefined; } -async function getPluginName(context: Context, pluginParentDirPath: string): -Promise { +async function getPluginName(context: Context, pluginParentDirPath: string): Promise { let pluginName = 'my-amplify-plugin'; const yesFlag = context.input.options && context.input.options[Constant.YES]; - if (context.input.subCommands!.length > 1) { // subcommands: ['new', 'name'] + if (context.input.subCommands!.length > 1) { + // subcommands: ['new', 'name'] pluginName = context.input.subCommands![1]; } else if (!yesFlag) { const pluginNameQuestion = { @@ -34,10 +33,10 @@ Promise { name: 'pluginName', message: 'What should be the name of the plugin:', default: pluginName, - validate: (input : string) => { + validate: (input: string) => { const pluginNameValidationResult = validPluginNameSync(input); if (!pluginNameValidationResult.isValid) { - return pluginNameValidationResult.message || 'Invalid plugin name' + return pluginNameValidationResult.message || 'Invalid plugin name'; } return true; }, @@ -65,9 +64,7 @@ Promise { return pluginName; } - -async function copyAndUpdateTemplateFiles(context: Context, - pluginParentDirPath: string, pluginName: string) { +async function copyAndUpdateTemplateFiles(context: Context, pluginParentDirPath: string, pluginName: string) { const pluginDirPath = path.join(pluginParentDirPath, pluginName); fs.emptyDirSync(pluginDirPath); @@ -94,18 +91,18 @@ async function promptForPluginType(context: Context): Promise { if (yesFlag) { return AmplifyPluginType.util; - }{ + } + { const pluginTypes = Object.keys(AmplifyPluginType); const LEARNMORE = 'Learn more'; const choices = pluginTypes.concat([LEARNMORE]); - const answer = await inquirer.prompt( - { - type: 'list', - name: 'selection', - message: 'Specify the plugin type', - choices, - default: AmplifyPluginType.util, - }); + const answer = await inquirer.prompt({ + type: 'list', + name: 'selection', + message: 'Specify the plugin type', + choices, + default: AmplifyPluginType.util, + }); if (answer.selection === LEARNMORE) { displayAmplifyPluginTypesLearnMore(context); return await promptForPluginType(context); @@ -143,17 +140,17 @@ async function promptForEventSubscription(context: Context): Promise { if (yesFlag) { return eventHandlers; - }{ + } + { const LEARNMORE = 'Learn more'; const choices = eventHandlers.concat([LEARNMORE]); - const answer = await inquirer.prompt( - { - type: 'checkbox', - name: 'selections', - message: 'What Amplify CLI events do you want the plugin to handle?', - choices, - default: eventHandlers, - }); + const answer = await inquirer.prompt({ + type: 'checkbox', + name: 'selections', + message: 'What Amplify CLI events do you want the plugin to handle?', + choices, + default: eventHandlers, + }); if (answer.selections.includes(LEARNMORE)) { displayAmplifyEventsLearnMore(context); return await promptForEventSubscription(context); @@ -196,12 +193,7 @@ function updatePackageJson(pluginDirPath: string, pluginName: string): void { fs.writeFileSync(filePath, jsonString, 'utf8'); } -function updateAmplifyPluginJson( - pluginDirPath: string, - pluginName: string, - pluginType: string, - eventHandlers: string[], -): void { +function updateAmplifyPluginJson(pluginDirPath: string, pluginName: string, pluginType: string, eventHandlers: string[]): void { const filePath = path.join(pluginDirPath, constants.MANIFEST_FILE_NAME); const amplifyPluginJson = readJsonFileSync(filePath); amplifyPluginJson.name = pluginName; @@ -211,17 +203,14 @@ function updateAmplifyPluginJson( fs.writeFileSync(filePath, jsonString, 'utf8'); } -function updateEventHandlersFolder( - pluginDirPath: string, - eventHandlers: string[], -): void { +function updateEventHandlersFolder(pluginDirPath: string, eventHandlers: string[]): void { const dirPath = path.join(pluginDirPath, 'event-handlers'); const fileNames = fs.readdirSync(dirPath); - fileNames.forEach((fileName) => { + fileNames.forEach(fileName => { const eventName = fileName.replace('handle-', '').split('.')[0]; if (!eventHandlers.includes(eventName)) { fs.removeSync(path.join(dirPath, fileName)); } - }) -} \ No newline at end of file + }); +} diff --git a/packages/amplify-cli/src/plugin-helpers/display-plugin-platform.ts b/packages/amplify-cli/src/plugin-helpers/display-plugin-platform.ts index 95a148478d..3f83c9d568 100644 --- a/packages/amplify-cli/src/plugin-helpers/display-plugin-platform.ts +++ b/packages/amplify-cli/src/plugin-helpers/display-plugin-platform.ts @@ -6,40 +6,35 @@ import PluginPlatform from '../domain/plugin-platform'; const defaultIndentationStr = ' '; -export function displaySimpleString(context: Context, title: string, - contents: string, indentation: number = 0) { +export function displaySimpleString(context: Context, title: string, contents: string, indentation: number = 0) { const indentationStr = createIndentation(indentation); context.print.blue(`${indentationStr}${title}:`); context.print.info(`${indentationStr}${defaultIndentationStr}${contents}`); } -export function displayStringArray(context: Context, title: string, - contents: string[], indentation: number = 0) { +export function displayStringArray(context: Context, title: string, contents: string[], indentation: number = 0) { const indentationStr = createIndentation(indentation); context.print.blue(`${indentationStr}${title}:`); - contents.forEach((strItem) => { + contents.forEach(strItem => { context.print.info(`${indentationStr}${defaultIndentationStr}${strItem}`); - }) + }); } export function displayPluginDirectories(context: Context, pluginPlatform: PluginPlatform) { context.print.info(''); - displayStringArray(context, 'Directories where the CLI scans for plugins:', - pluginPlatform.pluginDirectories); + displayStringArray(context, 'Directories where the CLI scans for plugins:', pluginPlatform.pluginDirectories); context.print.info(''); } export function displayPrefixes(context: Context, pluginPlatform: PluginPlatform) { context.print.info(''); - displayStringArray(context, 'Plugin name prefixes for the CLI to search for:', - pluginPlatform.pluginPrefixes); + displayStringArray(context, 'Plugin name prefixes for the CLI to search for:', pluginPlatform.pluginPrefixes); context.print.info(''); } export function displayScanInterval(context: Context, pluginPlatform: PluginPlatform) { context.print.info(''); - displaySimpleString(context, 'Automatic plugin scan interval in seconds', - pluginPlatform.maxScanIntervalInSeconds.toString()); + displaySimpleString(context, 'Automatic plugin scan interval in seconds', pluginPlatform.maxScanIntervalInSeconds.toString()); context.print.info(''); } @@ -47,8 +42,7 @@ export function displayConfiguration(context: Context, pluginPlatform: PluginPla context.print.info(''); displayStringArray(context, 'Directories for plugin scan', pluginPlatform.pluginDirectories); displayStringArray(context, 'Prefixes for plugin scan', pluginPlatform.pluginPrefixes); - displaySimpleString(context, 'Automatic plugin scan interval in seconds', - pluginPlatform.maxScanIntervalInSeconds.toString()); + displaySimpleString(context, 'Automatic plugin scan interval in seconds', pluginPlatform.maxScanIntervalInSeconds.toString()); context.print.info(''); } @@ -57,8 +51,7 @@ export function displayGeneralInfo(context: Context, pluginPlatform: PluginPlatf displayStringArray(context, 'Directories for plugin scan', pluginPlatform.pluginDirectories); displayStringArray(context, 'Prefixes for plugin scan', pluginPlatform.pluginPrefixes); displayStringArray(context, 'Manually added plugins', pluginPlatform.userAddedLocations); - displaySimpleString(context, 'Automatic plugin scan interval in seconds', - pluginPlatform.maxScanIntervalInSeconds.toString()); + displaySimpleString(context, 'Automatic plugin scan interval in seconds', pluginPlatform.maxScanIntervalInSeconds.toString()); displaySimpleString(context, 'Last scan time stamp', pluginPlatform.lastScanTime.toString()); context.print.info(''); } @@ -69,18 +62,16 @@ export function displayPluginPlatform(context: Context, pluginPlatform: PluginPl displayPluginCollection(context, pluginPlatform.excluded, 'excluded plugin'); } -export function displayPluginCollection(context: Context, - pluginCollection: PluginCollection, group: string = '') { - Object.keys(pluginCollection).forEach((key) => { +export function displayPluginCollection(context: Context, pluginCollection: PluginCollection, group: string = '') { + Object.keys(pluginCollection).forEach(key => { displayPluginInfoArray(context, pluginCollection[key], group); - }) + }); } -export function displayPluginInfoArray(context: Context, - pluginInfoArray: Array, group: string = '') { - pluginInfoArray.forEach((pluginInfo) => { +export function displayPluginInfoArray(context: Context, pluginInfoArray: Array, group: string = '') { + pluginInfoArray.forEach(pluginInfo => { displayPluginInfo(context, pluginInfo, group); - }) + }); } export function displayPluginInfo(context: Context, pluginInfo: PluginInfo, group: string = '') { @@ -96,14 +87,16 @@ export function displayPluginInfo(context: Context, pluginInfo: PluginInfo, grou } export function createIndentation(spaceCount: number): string { - if (spaceCount === 0) { return ''; } + if (spaceCount === 0) { + return ''; + } let charArray = [' ']; // tslint:disable-next-line - while ((charArray.length * 2) <= spaceCount) { + while (charArray.length * 2 <= spaceCount) { charArray = charArray.concat(charArray); } if (charArray.length < spaceCount) { charArray = charArray.concat(charArray.slice(0, spaceCount - charArray.length)); } return charArray.join(''); -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/plugin-helpers/scan-plugin-platform.ts b/packages/amplify-cli/src/plugin-helpers/scan-plugin-platform.ts index 3f09c07cde..6ad5c562b6 100644 --- a/packages/amplify-cli/src/plugin-helpers/scan-plugin-platform.ts +++ b/packages/amplify-cli/src/plugin-helpers/scan-plugin-platform.ts @@ -11,9 +11,8 @@ import { readPluginsJsonFile, writePluginsJsonFile } from './access-plugins-file import { twoPluginsAreTheSame } from './compare-plugins'; import isChildPath from '../utils/is-child-path'; - export async function scanPluginPlatform(pluginPlatform?: PluginPlatform): Promise { - pluginPlatform = pluginPlatform || await readPluginsJsonFile() || new PluginPlatform(); + pluginPlatform = pluginPlatform || (await readPluginsJsonFile()) || new PluginPlatform(); pluginPlatform!.plugins = new PluginCollection(); @@ -23,39 +22,36 @@ export async function scanPluginPlatform(pluginPlatform?: PluginPlatform): Promi if (pluginPlatform!.userAddedLocations && pluginPlatform!.userAddedLocations.length > 0) { // clean up the userAddedLocation first - pluginPlatform!.userAddedLocations = - pluginPlatform!.userAddedLocations.filter((pluginDirPath) => { - const result = fs.existsSync(pluginDirPath); - return result; - }) - - const scanUserLocationTasks = pluginPlatform!.userAddedLocations.map( - pluginDirPath => async () => await verifyAndAdd(pluginPlatform!, pluginDirPath), + pluginPlatform!.userAddedLocations = pluginPlatform!.userAddedLocations.filter(pluginDirPath => { + const result = fs.existsSync(pluginDirPath); + return result; + }); + + const scanUserLocationTasks = pluginPlatform!.userAddedLocations.map(pluginDirPath => async () => + await verifyAndAdd(pluginPlatform!, pluginDirPath) ); await sequential(scanUserLocationTasks); } if (pluginPlatform!.pluginDirectories.length > 0 && pluginPlatform!.pluginPrefixes.length > 0) { - const scanDirTasks = pluginPlatform!.pluginDirectories.map( - directory => async () => { - directory = normalizePluginDirectory(directory); - const exists = await fs.pathExists(directory); - if (exists) { - const subDirNames = await fs.readdir(directory); - if (subDirNames.length > 0) { - const scanSubDirTasks = subDirNames.map((subDirName) => { - return async () => { - if (isMatchingNamePattern(pluginPlatform!.pluginPrefixes, subDirName)) { - const pluginDirPath = path.join(directory, subDirName); - await verifyAndAdd(pluginPlatform!, pluginDirPath); - } + const scanDirTasks = pluginPlatform!.pluginDirectories.map(directory => async () => { + directory = normalizePluginDirectory(directory); + const exists = await fs.pathExists(directory); + if (exists) { + const subDirNames = await fs.readdir(directory); + if (subDirNames.length > 0) { + const scanSubDirTasks = subDirNames.map(subDirName => { + return async () => { + if (isMatchingNamePattern(pluginPlatform!.pluginPrefixes, subDirName)) { + const pluginDirPath = path.join(directory, subDirName); + await verifyAndAdd(pluginPlatform!, pluginDirPath); } - }); - await sequential(scanSubDirTasks); - } + }; + }); + await sequential(scanSubDirTasks); } - }, - ); + } + }); await sequential(scanDirTasks); } @@ -80,7 +76,7 @@ async function addCore(pluginPlatform: PluginPlatform) { pluginPlatform.plugins[manifest.name] = []; pluginPlatform.plugins[manifest.name].push(pluginInfo); } else { - throw new Error('The local Amplify-CLI is corrupted') + throw new Error('The local Amplify-CLI is corrupted'); } } @@ -98,7 +94,7 @@ export function normalizePluginDirectory(directory: string): string { function isMatchingNamePattern(pluginPrefixes: string[], pluginDirName: string): boolean { if (pluginPrefixes && pluginPrefixes.length > 0) { - return pluginPrefixes.some((prefix) => { + return pluginPrefixes.some(prefix => { const regex = new RegExp(`^${prefix}`); return regex.test(pluginDirName); }); @@ -107,25 +103,23 @@ function isMatchingNamePattern(pluginPrefixes: string[], pluginDirName: string): } async function verifyAndAdd(pluginPlatform: PluginPlatform, pluginDirPath: string) { const pluginVerificationResult = await verifyPlugin(pluginDirPath); - if (pluginVerificationResult.verified && - // Only the current core is added by the addCore(.) method, other packages can not be core - pluginVerificationResult.manifest!.name !== constants.CORE) { + if ( + pluginVerificationResult.verified && + // Only the current core is added by the addCore(.) method, other packages can not be core + pluginVerificationResult.manifest!.name !== constants.CORE + ) { const manifest = pluginVerificationResult.manifest as PluginManifest; const { name, version } = pluginVerificationResult.packageJson; const pluginInfo = new PluginInfo(name, version, pluginDirPath, manifest); let isPluginExcluded = false; if (pluginPlatform.excluded && pluginPlatform.excluded[manifest.name]) { - isPluginExcluded = pluginPlatform.excluded[manifest.name].some( - item => twoPluginsAreTheSame(item, pluginInfo), - ); + isPluginExcluded = pluginPlatform.excluded[manifest.name].some(item => twoPluginsAreTheSame(item, pluginInfo)); } if (!isPluginExcluded) { pluginPlatform.plugins[manifest.name] = pluginPlatform.plugins[manifest.name] || []; - const pluginAlreadyAdded = pluginPlatform.plugins[manifest.name].some( - item => twoPluginsAreTheSame(item, pluginInfo), - ); + const pluginAlreadyAdded = pluginPlatform.plugins[manifest.name].some(item => twoPluginsAreTheSame(item, pluginInfo)); if (!pluginAlreadyAdded) { pluginPlatform.plugins[manifest.name].push(pluginInfo); @@ -134,17 +128,13 @@ async function verifyAndAdd(pluginPlatform: PluginPlatform, pluginDirPath: strin } } -export function isUnderScanCoverageSync( - pluginPlatform: PluginPlatform, - pluginDirPath: string, -): boolean { +export function isUnderScanCoverageSync(pluginPlatform: PluginPlatform, pluginDirPath: string): boolean { let result = false; pluginDirPath = path.normalize(pluginDirPath); const pluginDirName = path.basename(pluginDirPath); - if (fs.existsSync(pluginDirPath) && - isMatchingNamePattern(pluginPlatform.pluginPrefixes, pluginDirName)) { - result = pluginPlatform.pluginDirectories.some((directory) => { + if (fs.existsSync(pluginDirPath) && isMatchingNamePattern(pluginPlatform.pluginPrefixes, pluginDirName)) { + result = pluginPlatform.pluginDirectories.some(directory => { directory = normalizePluginDirectory(directory); if (fs.existsSync(directory) && isChildPath(pluginDirPath, directory)) { return true; @@ -153,4 +143,4 @@ export function isUnderScanCoverageSync( } return result; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/plugin-helpers/verify-plugin.ts b/packages/amplify-cli/src/plugin-helpers/verify-plugin.ts index f99268880f..a1477ffcf1 100644 --- a/packages/amplify-cli/src/plugin-helpers/verify-plugin.ts +++ b/packages/amplify-cli/src/plugin-helpers/verify-plugin.ts @@ -10,10 +10,7 @@ export function verifyPluginSync(pluginDirPath: string): PluginVerificationResul if (fs.existsSync(pluginDirPath) && fs.statSync(pluginDirPath).isDirectory()) { return verifyNodePackageSync(pluginDirPath); } - return new PluginVerificationResult( - false, - PluginVerificationError.PluginDirPathNotExist, - ); + return new PluginVerificationResult(false, PluginVerificationError.PluginDirPathNotExist); } export async function verifyPlugin(pluginDirPath: string): Promise { @@ -27,16 +24,13 @@ export async function verifyPlugin(pluginDirPath: string): Promise { const result: PluginNameValidationResult = { isValid: true, @@ -58,7 +51,7 @@ export async function validPluginName(pluginName: string): Promise { +async function verifyAmplifyManifest(pluginDirPath: string, pluginModule: any): Promise { const pluginManifestFilePath = path.join(pluginDirPath, constants.MANIFEST_FILE_NAME); let exists = await fs.pathExists(pluginManifestFilePath); if (exists) { @@ -139,14 +111,11 @@ Promise { exists = stat.isFile(); } if (!exists) { - return new PluginVerificationResult( - false, - PluginVerificationError.MissingManifest, - ); + return new PluginVerificationResult(false, PluginVerificationError.MissingManifest); } try { - const manifest = await readJsonFile(pluginManifestFilePath) as PluginManifest; + const manifest = (await readJsonFile(pluginManifestFilePath)) as PluginManifest; const pluginNameValidationResult = await validPluginName(manifest.name); if (pluginNameValidationResult.isValid) { let result = verifyCommands(manifest, pluginModule); @@ -154,53 +123,40 @@ Promise { result.manifest = manifest; return result; } - return new PluginVerificationResult( - false, - PluginVerificationError.InvalidManifest, - pluginNameValidationResult.message, - ); + return new PluginVerificationResult(false, PluginVerificationError.InvalidManifest, pluginNameValidationResult.message); } catch (err) { - return new PluginVerificationResult( - false, - PluginVerificationError.InvalidManifest, - err, - ); + return new PluginVerificationResult(false, PluginVerificationError.InvalidManifest, err); } } function verifyCommands(manifest: PluginManifest, pluginModule: any): PluginVerificationResult { -// let isVerified = true; -// if (manifest.commands && manifest.commands.length > 0) { -// isVerified = pluginModule.hasOwnProperty(constants.ExecuteAmplifyCommand) && -// typeof pluginModule[constants.ExecuteAmplifyCommand] === 'function'; -// } - -// if (isVerified) { -// return new PluginVerificationResult(true); -// } -// return new PluginVerificationResult( -// false, -// PluginVerificationError.MissingExecuteAmplifyCommandMethod, -// ); - + // let isVerified = true; + // if (manifest.commands && manifest.commands.length > 0) { + // isVerified = pluginModule.hasOwnProperty(constants.ExecuteAmplifyCommand) && + // typeof pluginModule[constants.ExecuteAmplifyCommand] === 'function'; + // } + + // if (isVerified) { + // return new PluginVerificationResult(true); + // } + // return new PluginVerificationResult( + // false, + // PluginVerificationError.MissingExecuteAmplifyCommandMethod, + // ); // verification should be on the plugin type and if it implement all the required METHODS; return new PluginVerificationResult(true); } -function verifyEventHandlers(manifest: PluginManifest, pluginModule: any): -PluginVerificationResult { +function verifyEventHandlers(manifest: PluginManifest, pluginModule: any): PluginVerificationResult { let isVerified = true; if (manifest.eventHandlers && manifest.eventHandlers.length > 0) { - isVerified = pluginModule.hasOwnProperty(constants.HandleAmplifyEvent) && - typeof pluginModule[constants.HandleAmplifyEvent] === 'function'; + isVerified = + pluginModule.hasOwnProperty(constants.HandleAmplifyEvent) && typeof pluginModule[constants.HandleAmplifyEvent] === 'function'; } if (isVerified) { return new PluginVerificationResult(true); } - return new PluginVerificationResult( - false, - PluginVerificationError.MissingHandleAmplifyEventMethod, - ); -} \ No newline at end of file + return new PluginVerificationResult(false, PluginVerificationError.MissingHandleAmplifyEventMethod); +} diff --git a/packages/amplify-cli/src/plugin-manager.ts b/packages/amplify-cli/src/plugin-manager.ts index 52f61b97e4..29475a09b0 100644 --- a/packages/amplify-cli/src/plugin-manager.ts +++ b/packages/amplify-cli/src/plugin-manager.ts @@ -12,13 +12,13 @@ import constants from './domain/constants'; import { print } from './context-extensions'; export async function getPluginPlatform(): Promise { - // This function is called at the beginning of each command execution - // and performs the following actions: - // 1. read the plugins.json file - // 2. checks the last scan time stamp, - // 3. re-scan if needed. - // 4. write to update the plugins.json file if re-scan is performed - // 5. return the pluginsInfo object + // This function is called at the beginning of each command execution + // and performs the following actions: + // 1. read the plugins.json file + // 2. checks the last scan time stamp, + // 3. re-scan if needed. + // 4. write to update the plugins.json file if re-scan is performed + // 5. return the pluginsInfo object let pluginPlatform = readPluginsJsonFileSync(); if (pluginPlatform) { if (isCoreMatching(pluginPlatform)) { @@ -49,66 +49,53 @@ function isCoreMatching(pluginPlatform: PluginPlatform): boolean { } } -export function getPluginsWithName( - pluginPlatform: PluginPlatform, - nameOrAlias: string, -): Array { +export function getPluginsWithName(pluginPlatform: PluginPlatform, nameOrAlias: string): Array { let result = new Array(); - Object.keys(pluginPlatform.plugins).forEach((pluginName) => { + Object.keys(pluginPlatform.plugins).forEach(pluginName => { if (pluginName === nameOrAlias) { result = result.concat(pluginPlatform.plugins[pluginName]); } else { - pluginPlatform.plugins[pluginName].forEach((pluginInfo) => { - if (pluginInfo.manifest.aliases && - pluginInfo.manifest.aliases!.includes(nameOrAlias)) { + pluginPlatform.plugins[pluginName].forEach(pluginInfo => { + if (pluginInfo.manifest.aliases && pluginInfo.manifest.aliases!.includes(nameOrAlias)) { result.push(pluginInfo); } - }) + }); } }); return result; } -export function getPluginsWithNameAndCommand( - pluginPlatform: PluginPlatform, - nameOrAlias: string, - command: string, -): Array { +export function getPluginsWithNameAndCommand(pluginPlatform: PluginPlatform, nameOrAlias: string, command: string): Array { const result = new Array(); - Object.keys(pluginPlatform.plugins).forEach((pluginName) => { - pluginPlatform.plugins[pluginName].forEach((pluginInfo) => { + Object.keys(pluginPlatform.plugins).forEach(pluginName => { + pluginPlatform.plugins[pluginName].forEach(pluginInfo => { const { name, aliases, commands, commandAliases } = pluginInfo.manifest; - const nameOrAliasMatching = (name === nameOrAlias) || - (aliases && aliases!.includes(nameOrAlias)); + const nameOrAliasMatching = name === nameOrAlias || (aliases && aliases!.includes(nameOrAlias)); if (nameOrAliasMatching) { - if ((commands && commands.includes(command)) || - (commandAliases && Object.keys(commandAliases).includes(command))) { + if ((commands && commands.includes(command)) || (commandAliases && Object.keys(commandAliases).includes(command))) { result.push(pluginInfo); } } - }) + }); }); return result; } -export function getPluginsWithEventHandler( - pluginPlatform: PluginPlatform, - event: AmplifyEvent, -): Array { +export function getPluginsWithEventHandler(pluginPlatform: PluginPlatform, event: AmplifyEvent): Array { const result = new Array(); - Object.keys(pluginPlatform.plugins).forEach((pluginName) => { - pluginPlatform.plugins[pluginName].forEach((pluginInfo) => { + Object.keys(pluginPlatform.plugins).forEach(pluginName => { + pluginPlatform.plugins[pluginName].forEach(pluginInfo => { const { eventHandlers } = pluginInfo.manifest; if (eventHandlers && eventHandlers.length > 0 && eventHandlers.includes(event)) { result.push(pluginInfo); } - }) + }); }); return result; @@ -117,17 +104,16 @@ export function getPluginsWithEventHandler( export function getAllPluginNames(pluginPlatform: PluginPlatform): Set { const result = new Set(); - Object.keys(pluginPlatform.plugins).forEach((pluginName) => { + Object.keys(pluginPlatform.plugins).forEach(pluginName => { result.add(pluginName); - pluginPlatform.plugins[pluginName].forEach((pluginInfo) => { - if (pluginInfo.manifest.aliases && - pluginInfo.manifest.aliases.length > 0) { - pluginInfo.manifest.aliases.forEach((alias) => { + pluginPlatform.plugins[pluginName].forEach(pluginInfo => { + if (pluginInfo.manifest.aliases && pluginInfo.manifest.aliases.length > 0) { + pluginInfo.manifest.aliases.forEach(alias => { result.add(alias); - }) + }); } - }) + }); }); return result; @@ -140,7 +126,7 @@ export async function scan(pluginPlatform?: PluginPlatform): Promise 0) { + const pluginInfo = new PluginInfo(packageJson.name, packageJson.version, pluginDirPath, manifest!); + + // take the package out of the excluded + if (pluginPlatform.excluded[pluginInfo.manifest.name] && pluginPlatform.excluded[pluginInfo.manifest.name].length > 0) { const updatedExcluded = new Array(); - pluginPlatform.excluded[pluginInfo.manifest.name].forEach((pluginInfoItem) => { + pluginPlatform.excluded[pluginInfo.manifest.name].forEach(pluginInfoItem => { if (!twoPluginsAreTheSame(pluginInfoItem, pluginInfo)) { updatedExcluded.push(pluginInfoItem); } - }) + }); if (updatedExcluded.length > 0) { pluginPlatform.excluded[pluginInfo.manifest.name] = updatedExcluded; } else { @@ -208,26 +178,24 @@ export function addPluginPackage( } } - // insert into the plugins + // insert into the plugins const updatedPlugins = new Array(); - if (pluginPlatform.plugins[pluginInfo.manifest.name] && - pluginPlatform.plugins[pluginInfo.manifest.name].length > 0) { - pluginPlatform.plugins[pluginInfo.manifest.name].forEach((pluginInfoItem) => { + if (pluginPlatform.plugins[pluginInfo.manifest.name] && pluginPlatform.plugins[pluginInfo.manifest.name].length > 0) { + pluginPlatform.plugins[pluginInfo.manifest.name].forEach(pluginInfoItem => { if (!twoPluginsAreTheSame(pluginInfoItem, pluginInfo)) { updatedPlugins.push(pluginInfoItem); } - }) + }); } updatedPlugins.push(pluginInfo); pluginPlatform.plugins[pluginInfo.manifest.name] = updatedPlugins; - // insert into the userAddedLocations if it's not under scan coverage - if (!isUnderScanCoverageSync(pluginPlatform, pluginDirPath) && - !pluginPlatform.userAddedLocations.includes(pluginDirPath)) { + // insert into the userAddedLocations if it's not under scan coverage + if (!isUnderScanCoverageSync(pluginPlatform, pluginDirPath) && !pluginPlatform.userAddedLocations.includes(pluginDirPath)) { pluginPlatform.userAddedLocations.push(pluginDirPath); } - // write the plugins.json file + // write the plugins.json file writePluginsJsonFileSync(pluginPlatform); result.isAdded = true; } else { @@ -239,19 +207,15 @@ export function addPluginPackage( // remove: select from the plugins only, // if the location belongs to the scan directories, put the info inside the excluded. // if the location is in the useraddedlocaitons, remove it from the user added locations. -export function removePluginPackage( - pluginPlatform: PluginPlatform, - pluginInfo: PluginInfo, -): void { - // remove from the plugins - if (pluginPlatform.plugins[pluginInfo.manifest.name] && - pluginPlatform.plugins[pluginInfo.manifest.name].length > 0) { +export function removePluginPackage(pluginPlatform: PluginPlatform, pluginInfo: PluginInfo): void { + // remove from the plugins + if (pluginPlatform.plugins[pluginInfo.manifest.name] && pluginPlatform.plugins[pluginInfo.manifest.name].length > 0) { const updatedPlugins = new Array(); - pluginPlatform.plugins[pluginInfo.manifest.name].forEach((pluginInfoItem) => { + pluginPlatform.plugins[pluginInfo.manifest.name].forEach(pluginInfoItem => { if (!twoPluginsAreTheSame(pluginInfoItem, pluginInfo)) { updatedPlugins.push(pluginInfoItem); } - }) + }); if (updatedPlugins.length > 0) { pluginPlatform.plugins[pluginInfo.manifest.name] = updatedPlugins; } else { @@ -259,10 +223,10 @@ export function removePluginPackage( } } - // remove from the userAddedLocations + // remove from the userAddedLocations if (pluginPlatform.userAddedLocations.includes(pluginInfo.packageLocation)) { const updatedUserAddedLocations = new Array(); - pluginPlatform.userAddedLocations.forEach((packageLocation) => { + pluginPlatform.userAddedLocations.forEach(packageLocation => { if (packageLocation !== pluginInfo.packageLocation) { updatedUserAddedLocations.push(packageLocation); } @@ -270,12 +234,10 @@ export function removePluginPackage( pluginPlatform.userAddedLocations = updatedUserAddedLocations; } - // if the plugin is under scan coverage, insert into the excluded + // if the plugin is under scan coverage, insert into the excluded if (isUnderScanCoverageSync(pluginPlatform, pluginInfo.packageLocation)) { - pluginPlatform.excluded[pluginInfo.manifest.name] = - pluginPlatform.excluded[pluginInfo.manifest.name] || []; + pluginPlatform.excluded[pluginInfo.manifest.name] = pluginPlatform.excluded[pluginInfo.manifest.name] || []; pluginPlatform.excluded[pluginInfo.manifest.name].push(pluginInfo); } writePluginsJsonFileSync(pluginPlatform); } - diff --git a/packages/amplify-cli/src/utils/global-prefix.ts b/packages/amplify-cli/src/utils/global-prefix.ts index 1bce2f9967..ad76720b28 100644 --- a/packages/amplify-cli/src/utils/global-prefix.ts +++ b/packages/amplify-cli/src/utils/global-prefix.ts @@ -20,4 +20,4 @@ function getYarnPrefix() { yarnPrefix = path.join(process.env.LOCALAPPDATA, 'Yarn', 'config', 'global'); } return yarnPrefix; -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/utils/is-child-path.ts b/packages/amplify-cli/src/utils/is-child-path.ts index f37a56d8fe..f61e1db935 100644 --- a/packages/amplify-cli/src/utils/is-child-path.ts +++ b/packages/amplify-cli/src/utils/is-child-path.ts @@ -7,4 +7,4 @@ export default function isChildPath(child: string, parent: string): boolean { const parentTokens = parent.split(path.sep).filter(i => i.length); const childTokens = child.split(path.sep).filter(i => i.length); return parentTokens.every((element, index) => childTokens[index] === element); -} \ No newline at end of file +} diff --git a/packages/amplify-cli/src/utils/readJsonFile.ts b/packages/amplify-cli/src/utils/readJsonFile.ts index eb3f246df0..e5d4af5f5c 100644 --- a/packages/amplify-cli/src/utils/readJsonFile.ts +++ b/packages/amplify-cli/src/utils/readJsonFile.ts @@ -2,7 +2,7 @@ import fs from 'fs-extra'; function stripBOM(content: string) { // tslint:disable-next-line - if (content.charCodeAt(0) === 0xFEFF) { + if (content.charCodeAt(0) === 0xfeff) { content = content.slice(1); } return content; @@ -15,4 +15,4 @@ export function readJsonFileSync(jsonFilePath: string, encoding: string = 'utf8' export async function readJsonFile(jsonFilePath: string, encoding: string = 'utf8'): Promise { const contents = await fs.readFile(jsonFilePath, encoding); return JSON.parse(stripBOM(contents)); -} \ No newline at end of file +} diff --git a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostInit.js b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostInit.js index 96b50e70a3..dd33a411b0 100644 --- a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostInit.js +++ b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostInit.js @@ -1,4 +1,4 @@ -const eventName = "PostInit"; +const eventName = 'PostInit'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostPush.js b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostPush.js index c6ab1a120a..1392f89442 100644 --- a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostPush.js +++ b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PostPush.js @@ -1,4 +1,4 @@ -const eventName = "PostPush"; +const eventName = 'PostPush'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PreInit.js b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PreInit.js index c7c061fd2d..ef4bb82c65 100644 --- a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PreInit.js +++ b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PreInit.js @@ -1,4 +1,4 @@ -const eventName = "PreInit"; +const eventName = 'PreInit'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PrePush.js b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PrePush.js index d1fea2c357..91cd8da05a 100644 --- a/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PrePush.js +++ b/packages/amplify-cli/templates/plugin-template-frontend/event-handlers/handle-PrePush.js @@ -1,4 +1,4 @@ -const eventName = "PrePush"; +const eventName = 'PrePush'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-frontend/index.js b/packages/amplify-cli/templates/plugin-template-frontend/index.js index c27b4e835a..3698a75f2f 100644 --- a/packages/amplify-cli/templates/plugin-template-frontend/index.js +++ b/packages/amplify-cli/templates/plugin-template-frontend/index.js @@ -1,22 +1,16 @@ const path = require('path'); -function scanProject(projectPath) { -} +function scanProject(projectPath) {} -function init(context) { -} +function init(context) {} -function onInitSuccessful(context) { -} +function onInitSuccessful(context) {} -function configure(context) { -} +function configure(context) {} -function publish(context) { -} +function publish(context) {} -function run(context) { -} +function run(context) {} async function createFrontendConfigs(context, amplifyResources, amplifyCloudResources) { const newOutputsForFrontend = amplifyResources.outputsForFrontend; diff --git a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostInit.js b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostInit.js index 96b50e70a3..dd33a411b0 100644 --- a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostInit.js +++ b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostInit.js @@ -1,4 +1,4 @@ -const eventName = "PostInit"; +const eventName = 'PostInit'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostPush.js b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostPush.js index c6ab1a120a..1392f89442 100644 --- a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostPush.js +++ b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PostPush.js @@ -1,4 +1,4 @@ -const eventName = "PostPush"; +const eventName = 'PostPush'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PreInit.js b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PreInit.js index c7c061fd2d..ef4bb82c65 100644 --- a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PreInit.js +++ b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PreInit.js @@ -1,4 +1,4 @@ -const eventName = "PreInit"; +const eventName = 'PreInit'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PrePush.js b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PrePush.js index d1fea2c357..91cd8da05a 100644 --- a/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PrePush.js +++ b/packages/amplify-cli/templates/plugin-template-provider/event-handlers/handle-PrePush.js @@ -1,4 +1,4 @@ -const eventName = "PrePush"; +const eventName = 'PrePush'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template-provider/index.js b/packages/amplify-cli/templates/plugin-template-provider/index.js index c47f353e85..d0f4ad50f9 100644 --- a/packages/amplify-cli/templates/plugin-template-provider/index.js +++ b/packages/amplify-cli/templates/plugin-template-provider/index.js @@ -1,22 +1,16 @@ const path = require('path'); -function init(context) { -} +function init(context) {} -function initEnv(context, providerMetadata) { -} +function initEnv(context, providerMetadata) {} -function onInitSuccessful(context) { -} +function onInitSuccessful(context) {} -function pushResources(context, resourceList) { -} +function pushResources(context, resourceList) {} -function deleteEnv(context, envName) { -} +function deleteEnv(context, envName) {} -function configure(context) { -} +function configure(context) {} async function executeAmplifyCommand(context) { const commandsDirPath = path.normalize(path.join(__dirname, 'commands')); diff --git a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostInit.js b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostInit.js index 96b50e70a3..dd33a411b0 100644 --- a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostInit.js +++ b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostInit.js @@ -1,4 +1,4 @@ -const eventName = "PostInit"; +const eventName = 'PostInit'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostPush.js b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostPush.js index c6ab1a120a..1392f89442 100644 --- a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostPush.js +++ b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PostPush.js @@ -1,4 +1,4 @@ -const eventName = "PostPush"; +const eventName = 'PostPush'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PreInit.js b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PreInit.js index c7c061fd2d..ef4bb82c65 100644 --- a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PreInit.js +++ b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PreInit.js @@ -1,4 +1,4 @@ -const eventName = "PreInit"; +const eventName = 'PreInit'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PrePush.js b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PrePush.js index d1fea2c357..91cd8da05a 100644 --- a/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PrePush.js +++ b/packages/amplify-cli/templates/plugin-template/event-handlers/handle-PrePush.js @@ -1,4 +1,4 @@ -const eventName = "PrePush"; +const eventName = 'PrePush'; async function run(context, args) { // insert your code to handle the amplify cli PostAddResource event diff --git a/packages/amplify-cli/tests/amplify-helpers/input-validation.test.js b/packages/amplify-cli/tests/amplify-helpers/input-validation.test.js index 002ca20575..7e957d4ae9 100644 --- a/packages/amplify-cli/tests/amplify-helpers/input-validation.test.js +++ b/packages/amplify-cli/tests/amplify-helpers/input-validation.test.js @@ -116,7 +116,6 @@ describe('input-validation helper: ', () => { }); }); - describe('case: validation operator "noEmptyArray"', () => { beforeEach(() => { question.validation = { diff --git a/packages/amplify-cli/tests/amplify-helpers/trigger-flow.test.js b/packages/amplify-cli/tests/amplify-helpers/trigger-flow.test.js index 53f469f124..c2678e093f 100644 --- a/packages/amplify-cli/tests/amplify-helpers/trigger-flow.test.js +++ b/packages/amplify-cli/tests/amplify-helpers/trigger-flow.test.js @@ -6,12 +6,10 @@ const inquirer = require('inquirer'); const fs = require('fs'); const fsExtra = require('fs-extra'); - jest.mock('inquirer'); jest.mock('fs'); jest.mock('fs-extra'); - const key = 'foo'; const values = ['bar']; let context = {}; @@ -58,7 +56,6 @@ describe('TriggerFlow: ', () => { }; }); - describe('When adding a trigger...', () => { let spyAdd; let readdirSyncSpy; @@ -139,7 +136,7 @@ describe('TriggerFlow: ', () => { beforeEach(() => { readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['file1']); spyUpdate = jest.spyOn(func, 'update').mockImplementation(() => Promise.resolve()); - unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync').mockImplementation(() => (Promise.resolve())); + unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync').mockImplementation(() => Promise.resolve()); metadataSpy = jest.spyOn(context.amplify, 'getTriggerMetadata').mockImplementation(() => { return { stark: { @@ -147,10 +144,12 @@ describe('TriggerFlow: ', () => { policyName: 'policy', trigger: 'arya', actions: ['actions'], - resources: [{ - type: 'foo', - attribute: 'bar', - }], + resources: [ + { + type: 'foo', + attribute: 'bar', + }, + ], }, }, }; @@ -263,37 +262,19 @@ describe('TriggerFlow: ', () => { it('...it should call deleteTrigger twice when two triggers are removed', async () => { previousTriggers = ['arya', 'sandor', 'joffrey']; - await triggerFlow.deleteDeselectedTriggers( - currentTriggers, - previousTriggers, - functionName, - path, - context, - ); + await triggerFlow.deleteDeselectedTriggers(currentTriggers, previousTriggers, functionName, path, context); expect(deleteSpy).toHaveBeenCalledTimes(2); }); it('...it should call deleteTrigger once when one trigger is removed', async () => { previousTriggers = ['arya', 'sandor']; - await triggerFlow.deleteDeselectedTriggers( - currentTriggers, - previousTriggers, - functionName, - path, - context, - ); + await triggerFlow.deleteDeselectedTriggers(currentTriggers, previousTriggers, functionName, path, context); expect(deleteSpy).toHaveBeenCalledTimes(1); }); it('...it should not call deleteTrigger when nothing is removed', async () => { previousTriggers = ['arya']; - await triggerFlow.deleteDeselectedTriggers( - currentTriggers, - previousTriggers, - functionName, - path, - context, - ); + await triggerFlow.deleteDeselectedTriggers(currentTriggers, previousTriggers, functionName, path, context); expect(deleteSpy).toHaveBeenCalledTimes(0); }); }); @@ -310,10 +291,12 @@ describe('TriggerFlow: ', () => { policyName: 'policy', trigger: 'arya', actions: ['actions'], - resources: [{ - type: 'foo', - attribute: 'bar', - }], + resources: [ + { + type: 'foo', + attribute: 'bar', + }, + ], }, }, dragon: { @@ -321,10 +304,12 @@ describe('TriggerFlow: ', () => { policyName: 'policy', trigger: 'drogon', actions: ['actions'], - resources: [{ - type: 'foo', - attribute: 'bar', - }], + resources: [ + { + type: 'foo', + attribute: 'bar', + }, + ], }, }, }; @@ -338,45 +323,25 @@ describe('TriggerFlow: ', () => { it('...it should call getTriggerMetadata once for each key (1 result)', async () => { triggers = { arya: ['stark'] }; - await triggerFlow.getTriggerPermissions( - context, - JSON.stringify(triggers), - category, - functionName, - ); + await triggerFlow.getTriggerPermissions(context, JSON.stringify(triggers), category, functionName); expect(metadataSpy).toHaveBeenCalledTimes(1); }); it('...it should call getTriggerMetadata once for each key (2 results)', async () => { triggers = { arya: ['stark'], tyrion: ['lannister'] }; - await triggerFlow.getTriggerPermissions( - context, - JSON.stringify(triggers), - category, - functionName, - ); + await triggerFlow.getTriggerPermissions(context, JSON.stringify(triggers), category, functionName); expect(metadataSpy).toHaveBeenCalledTimes(2); }); it('...it should return permissions corresponding to the key (1 result)', async () => { triggers = { arya: ['stark'], tyrion: ['lannister'] }; - const result = await triggerFlow.getTriggerPermissions( - context, - JSON.stringify(triggers), - category, - functionName, - ); + const result = await triggerFlow.getTriggerPermissions(context, JSON.stringify(triggers), category, functionName); expect(result.length).toEqual(1); }); it('...it should return permissions corresponding to the key (2 results)', async () => { triggers = { arya: ['stark'], drogon: ['dragon'] }; - const result = await triggerFlow.getTriggerPermissions( - context, - JSON.stringify(triggers), - category, - functionName, - ); + const result = await triggerFlow.getTriggerPermissions(context, JSON.stringify(triggers), category, functionName); expect(result.length).toEqual(2); }); }); @@ -412,29 +377,17 @@ describe('TriggerFlow: ', () => { }); it('...it should call getTriggerMetadata once', async () => { - await triggerFlow.getTriggerEnvVariables( - context, - { key: 'stark' }, - functionName, - ); + await triggerFlow.getTriggerEnvVariables(context, { key: 'stark' }, functionName); expect(metadataSpy).toHaveBeenCalledTimes(1); }); it('...it should call getTriggerMetadata once and return one result', async () => { - const result = await triggerFlow.getTriggerEnvVariables( - context, - { key: 'arya', modules: ['stark'] }, - functionName, - ); + const result = await triggerFlow.getTriggerEnvVariables(context, { key: 'arya', modules: ['stark'] }, functionName); expect(result.length).toEqual(1); }); it('...it should call getTriggerMetadata once and return zero results', async () => { - const result = await triggerFlow.getTriggerEnvVariables( - context, - { key: 'arya', modules: ['lannister'] }, - functionName, - ); + const result = await triggerFlow.getTriggerEnvVariables(context, { key: 'arya', modules: ['lannister'] }, functionName); expect(result.length).toEqual(0); }); }); @@ -501,15 +454,17 @@ describe('TriggerFlow: ', () => { metadataSpy = jest.spyOn(context.amplify, 'getTriggerMetadata').mockImplementation(() => { return { stark: { - env: [{ - key: 'REDIRECTURL', - value: 'askUser', - question: { - name: 'REDIRECTURL', - type: 'input', - message: 'Enter the URL that your users will be redirected to upon account confirmation:', + env: [ + { + key: 'REDIRECTURL', + value: 'askUser', + question: { + name: 'REDIRECTURL', + type: 'input', + message: 'Enter the URL that your users will be redirected to upon account confirmation:', + }, }, - }], + ], }, }; }); @@ -539,16 +494,14 @@ describe('TriggerFlow: ', () => { it('...it should return the prompt answers (when no currentEnvValues are present)', async () => { triggerKey = 'arya'; vals = ['stark']; - const result = await triggerFlow - .getTriggerEnvInputs(context, path, triggerKey, vals, envVals); + const result = await triggerFlow.getTriggerEnvInputs(context, path, triggerKey, vals, envVals); expect(result.REDIRECTURL).toEqual('hello'); }); it('...it should return the prompt answers along with currentEnvValues', async () => { triggerKey = 'arya'; vals = ['stark']; envVals = { anotherVar: 'world' }; - const result = await triggerFlow - .getTriggerEnvInputs(context, path, triggerKey, vals, envVals); + const result = await triggerFlow.getTriggerEnvInputs(context, path, triggerKey, vals, envVals); expect(result.REDIRECTURL).toEqual('hello'); expect(result.anotherVar).toEqual('world'); expect(Object.keys(result).length).toEqual(2); @@ -615,7 +568,7 @@ describe('TriggerFlow: ', () => { let val = ''; beforeEach(() => { - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['file1', 'file2'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['file1', 'file2']); copySyncSpy = jest.spyOn(fsExtra, 'copySync').mockImplementation(() => Promise.resolve()); }); @@ -652,7 +605,7 @@ describe('TriggerFlow: ', () => { let unlinkSyncSpy; beforeEach(() => { - unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync').mockImplementation(() => (Promise.resolve())); + unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync').mockImplementation(() => Promise.resolve()); metadataSpy = jest.spyOn(context.amplify, 'getTriggerMetadata').mockImplementation(() => { return { stark: { @@ -660,10 +613,12 @@ describe('TriggerFlow: ', () => { policyName: 'policy', trigger: 'arya', actions: ['actions'], - resources: [{ - type: 'foo', - attribute: 'bar', - }], + resources: [ + { + type: 'foo', + attribute: 'bar', + }, + ], }, }, dragon: { @@ -671,10 +626,12 @@ describe('TriggerFlow: ', () => { policyName: 'policy', trigger: 'drogon', actions: ['actions'], - resources: [{ - type: 'foo', - attribute: 'bar', - }], + resources: [ + { + type: 'foo', + attribute: 'bar', + }, + ], }, }, }; @@ -686,37 +643,37 @@ describe('TriggerFlow: ', () => { }); it('...should call getTriggerMetadata once', async () => { - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['file1', 'file2'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['file1', 'file2']); await triggerFlow.cleanFunctions(key, values, category, context, path); expect(metadataSpy).toHaveBeenCalledTimes(1); }); it('...should call readdirSync once', async () => { - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['file1', 'file2'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['file1', 'file2']); await triggerFlow.cleanFunctions(key, values, category, context, path); expect(readdirSyncSpy).toHaveBeenCalledTimes(1); }); it('...should call readdirSync once', async () => { - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['file1', 'file2'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['file1', 'file2']); await triggerFlow.cleanFunctions(key, values, category, context, path); expect(readdirSyncSpy).toHaveBeenCalledTimes(1); }); it('...should call unlinkSync if the dir contents have values not present in passed values', async () => { - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['stark.js'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['stark.js']); await triggerFlow.cleanFunctions(key, values, category, context, path); expect(unlinkSyncSpy).toHaveBeenCalledTimes(1); }); it('...should call unlinkSync if the dir contents have values not present in passed values (custom file)', async () => { - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['custom.js'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['custom.js']); await triggerFlow.cleanFunctions(key, values, category, context, path); expect(unlinkSyncSpy).toHaveBeenCalledTimes(1); }); it('...should throw an error if parameters are missing', async () => { - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['file1', 'file2'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['file1', 'file2']); let error; try { await triggerFlow.cleanFunctions(); @@ -727,7 +684,7 @@ describe('TriggerFlow: ', () => { }); }); - describe(('When calling choicesFromMetadata...'), () => { + describe('When calling choicesFromMetadata...', () => { let readdirSyncSpy; let readFileSync; let statSyncSpy; @@ -735,7 +692,7 @@ describe('TriggerFlow: ', () => { beforeEach(() => { statSyncSpy = jest.spyOn(fs, 'statSync').mockImplementation(() => ({ isDirectory: jest.fn() })); readFileSync = jest.spyOn(fs, 'readFileSync').mockImplementation(() => '{}'); - readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => (['file1', 'file2'])); + readdirSyncSpy = jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['file1', 'file2']); }); afterEach(() => { diff --git a/packages/amplify-codegen/commands/codegen/add.js b/packages/amplify-codegen/commands/codegen/add.js index 8ca298563a..89ac185817 100644 --- a/packages/amplify-codegen/commands/codegen/add.js +++ b/packages/amplify-codegen/commands/codegen/add.js @@ -5,7 +5,7 @@ const featureName = 'add'; module.exports = { name: featureName, - run: async (context) => { + run: async context => { try { const { options = {} } = context.parameters; const keys = Object.keys(options); @@ -21,4 +21,4 @@ module.exports = { context.print.error(ex.message); } }, -}; \ No newline at end of file +}; diff --git a/packages/amplify-codegen/commands/codegen/codegen.js b/packages/amplify-codegen/commands/codegen/codegen.js index 19ea2b2ddf..40f8c3759c 100644 --- a/packages/amplify-codegen/commands/codegen/codegen.js +++ b/packages/amplify-codegen/commands/codegen/codegen.js @@ -5,7 +5,7 @@ const featureName = 'codegen'; module.exports = { name: featureName, - run: async (context) => { + run: async context => { if (context.parameters.options.help) { const header = `amplify ${featureName} [subcommand] [[--nodownload] [--max-depth ]]\nDescriptions: Generates GraphQL statements (queries, mutations and subscriptions) and type annotations. \nSub Commands:`; @@ -35,7 +35,7 @@ module.exports = { context.print.info(constants.CMD_DESCRIPTION_NOT_SUPPORTED); process.exit(1); } - + try { let forceDownloadSchema = !context.parameters.options.nodownload; let { maxDepth } = context.parameters.options; diff --git a/packages/amplify-codegen/commands/codegen/configure.js b/packages/amplify-codegen/commands/codegen/configure.js index cec139b4eb..32d9fed9f1 100644 --- a/packages/amplify-codegen/commands/codegen/configure.js +++ b/packages/amplify-codegen/commands/codegen/configure.js @@ -5,7 +5,7 @@ const featureName = 'configure'; module.exports = { name: featureName, alias: 'update', - run: async (context) => { + run: async context => { try { await codeGen.configure(context); } catch (ex) { diff --git a/packages/amplify-codegen/commands/codegen/remove.js b/packages/amplify-codegen/commands/codegen/remove.js index 58130edb92..f9cc8f780f 100644 --- a/packages/amplify-codegen/commands/codegen/remove.js +++ b/packages/amplify-codegen/commands/codegen/remove.js @@ -4,7 +4,7 @@ const featureName = 'remove'; module.exports = { name: featureName, - run: async (context) => { + run: async context => { try { await codeGen.remove(context); } catch (ex) { diff --git a/packages/amplify-codegen/commands/codegen/statements.js b/packages/amplify-codegen/commands/codegen/statements.js index fd4c2589e7..58b7ccb73b 100644 --- a/packages/amplify-codegen/commands/codegen/statements.js +++ b/packages/amplify-codegen/commands/codegen/statements.js @@ -4,7 +4,7 @@ const featureName = 'statements'; module.exports = { name: featureName, - run: async (context) => { + run: async context => { try { const forceDownloadSchema = !context.parameters.options.nodownload; const { maxDepth } = context.parameters.options; diff --git a/packages/amplify-codegen/commands/codegen/types.js b/packages/amplify-codegen/commands/codegen/types.js index 8ffafff829..e070cd2a8e 100644 --- a/packages/amplify-codegen/commands/codegen/types.js +++ b/packages/amplify-codegen/commands/codegen/types.js @@ -4,7 +4,7 @@ const featureName = 'types'; module.exports = { name: featureName, - run: async (context) => { + run: async context => { try { const forceDownloadSchema = !context.parameters.options.nodownload; await codeGen.generateTypes(context, forceDownloadSchema); diff --git a/packages/amplify-dynamodb-simulator/__test__/index.test.js b/packages/amplify-dynamodb-simulator/__test__/index.test.js index 8cdfdbb740..e2a1020b68 100644 --- a/packages/amplify-dynamodb-simulator/__test__/index.test.js +++ b/packages/amplify-dynamodb-simulator/__test__/index.test.js @@ -3,86 +3,86 @@ const fs = require('fs-extra'); const { execSync } = require('child_process'); describe('emulator operations', () => { - const dbPath = `${__dirname}/dynamodb-data/${process.pid}`; - // taken from dynamodb examples. - const dbParams = { - AttributeDefinitions: [ - { - AttributeName: 'Artist', - AttributeType: 'S' - }, - { - AttributeName: 'SongTitle', - AttributeType: 'S' - } - ], - KeySchema: [ - { - AttributeName: 'Artist', - KeyType: 'HASH' - }, - { - AttributeName: 'SongTitle', - KeyType: 'RANGE' - } - ], - ProvisionedThroughput: { - ReadCapacityUnits: 5, - WriteCapacityUnits: 5 - } - }; + const dbPath = `${__dirname}/dynamodb-data/${process.pid}`; + // taken from dynamodb examples. + const dbParams = { + AttributeDefinitions: [ + { + AttributeName: 'Artist', + AttributeType: 'S', + }, + { + AttributeName: 'SongTitle', + AttributeType: 'S', + }, + ], + KeySchema: [ + { + AttributeName: 'Artist', + KeyType: 'HASH', + }, + { + AttributeName: 'SongTitle', + KeyType: 'RANGE', + }, + ], + ProvisionedThroughput: { + ReadCapacityUnits: 5, + WriteCapacityUnits: 5, + }, + }; - const ensureNoDbPath = () => { - if (fs.existsSync(dbPath)) { - fs.removeSync(dbPath); - } - }; + const ensureNoDbPath = () => { + if (fs.existsSync(dbPath)) { + fs.removeSync(dbPath); + } + }; - beforeEach(ensureNoDbPath); - afterEach(ensureNoDbPath); + beforeEach(ensureNoDbPath); + afterEach(ensureNoDbPath); - let emulators; - beforeEach(() => { - emulators = []; - jest.setTimeout(40 * 1000); - }); - afterEach(async () => await Promise.all(emulators.map(emu => emu.terminate()))); + let emulators; + beforeEach(() => { + emulators = []; + jest.setTimeout(40 * 1000); + }); + afterEach(async () => await Promise.all(emulators.map(emu => emu.terminate()))); - it('should support in memory operations', async () => { - const emu = await ddbSimulator.launch(); - emulators.push(emu); - const dynamo = ddbSimulator.getClient(emu); + it('should support in memory operations', async () => { + const emu = await ddbSimulator.launch(); + emulators.push(emu); + const dynamo = ddbSimulator.getClient(emu); - const tables = await dynamo.listTables().promise(); - expect(tables).toEqual({ TableNames: [] }); - }); + const tables = await dynamo.listTables().promise(); + expect(tables).toEqual({ TableNames: [] }); + }); - it('should preserve state between restarts with dbPath', async () => { - const emuOne = await ddbSimulator.launch({ dbPath }); - emulators.push(emuOne); - const dynamoOne = ddbSimulator.getClient(emuOne); - await dynamoOne - .createTable({ - TableName: 'foo', - ...dbParams - }) - .promise(); - await emuOne.terminate(); + it('should preserve state between restarts with dbPath', async () => { + const emuOne = await ddbSimulator.launch({ dbPath }); + emulators.push(emuOne); + const dynamoOne = ddbSimulator.getClient(emuOne); + await dynamoOne + .createTable({ + TableName: 'foo', + ...dbParams, + }) + .promise(); + await emuOne.terminate(); - const emuTwo = await ddbSimulator.launch({ dbPath }); - emulators.push(emuTwo); - const dynamoTwo = await ddbSimulator.getClient(emuTwo); - const t = await dynamoTwo.listTables().promise(); - expect(t).toEqual({ - TableNames: ['foo'] - }); - emuTwo.terminate(); + const emuTwo = await ddbSimulator.launch({ dbPath }); + emulators.push(emuTwo); + const dynamoTwo = await ddbSimulator.getClient(emuTwo); + const t = await dynamoTwo.listTables().promise(); + expect(t).toEqual({ + TableNames: ['foo'], }); + emuTwo.terminate(); + }); - it('should start on specific port', async () => { - const port = await require('portfinder').getPortPromise(); - const emu = await ddbSimulator.launch({ port }); - emulators.push(emu); - expect(emu.port).toBe(port); - }); + it('should start on specific port', async () => { + const port = await require('portfinder').getPortPromise(); + const emu = await ddbSimulator.launch({ port }); + emulators.push(emu); + expect(emu.port).toBe(port); + }); }); diff --git a/packages/amplify-dynamodb-simulator/index.js b/packages/amplify-dynamodb-simulator/index.js index 13f2b0b5d2..aecf017568 100644 --- a/packages/amplify-dynamodb-simulator/index.js +++ b/packages/amplify-dynamodb-simulator/index.js @@ -10,10 +10,10 @@ const execa = require('execa'); const basePort = 62224; const defaultOptions = { - inMemory: true, - sharedDb: false, - dbPath: null, - startTimeout: 20 * 1000 + inMemory: true, + sharedDb: false, + dbPath: null, + startTimeout: 20 * 1000, }; const emulatorPath = path.join(__dirname, 'emulator'); @@ -21,201 +21,193 @@ const retryInterval = 20; const maxRetries = 5; class Emulator { - constructor(proc, opts) { - this.proc = proc; - this.opts = opts; - return this; - } - - get pid() { - return this.proc.pid; - } - - get port() { - return this.opts.port; - } - - get url() { - return `http://localhost:${this.port}/`; - } - - terminate() { - // already exited - if (this.proc.exitCode != null) { - return this.proc.exitCode; - } - this.proc.kill(); - return e2p(this.proc, 'exit'); + constructor(proc, opts) { + this.proc = proc; + this.opts = opts; + return this; + } + + get pid() { + return this.proc.pid; + } + + get port() { + return this.opts.port; + } + + get url() { + return `http://localhost:${this.port}/`; + } + + terminate() { + // already exited + if (this.proc.exitCode != null) { + return this.proc.exitCode; } + this.proc.kill(); + return e2p(this.proc, 'exit'); + } } const wait = ms => { - let timeoutHandle; - const promise = new Promise(accept => { - timeoutHandle = setTimeout(accept, ms); - }); - - return { - promise, - cancel: () => { - clearTimeout(timeoutHandle); - } - }; + let timeoutHandle; + const promise = new Promise(accept => { + timeoutHandle = setTimeout(accept, ms); + }); + + return { + promise, + cancel: () => { + clearTimeout(timeoutHandle); + }, + }; }; async function which(bin) { - return new Promise((accept, reject) => { - require('which')(bin, (err, value) => { - if (err) return reject(err); - return accept(value); - }); + return new Promise((accept, reject) => { + require('which')(bin, (err, value) => { + if (err) return reject(err); + return accept(value); }); + }); } function buildArgs(options) { - const args = [ - '-Djava.library.path=./DynamoDBLocal_lib', - '-jar', - 'DynamoDBLocal.jar', - '-port', - options.port - ]; - if (options.dbPath) { - args.push('-dbPath'); - args.push(options.dbPath); - } - - // dbPath overrides in memory - if (options.inMemory && !options.dbPath) { - args.push('-inMemory'); - } - if (options.sharedDb) { - args.push('-sharedDb'); - } - return args; + const args = ['-Djava.library.path=./DynamoDBLocal_lib', '-jar', 'DynamoDBLocal.jar', '-port', options.port]; + if (options.dbPath) { + args.push('-dbPath'); + args.push(options.dbPath); + } + + // dbPath overrides in memory + if (options.inMemory && !options.dbPath) { + args.push('-inMemory'); + } + if (options.sharedDb) { + args.push('-sharedDb'); + } + return args; } async function launch(givenOptions = {}, retry = 0, startTime = Date.now()) { - log.info('launching', { retry, givenOptions }); - // launch will retry but ensure it will not retry indefinitely. - if (retry >= maxRetries) { - throw new Error('max retries hit for starting dynamodb emulator'); - } - - if (givenOptions.inMemory && givenOptions.dbPath) { - throw new Error('inMemory and dbPath are mutually exclusive options'); - } - let { port } = givenOptions; - if (!port) { - port = await detectPort(basePort); - log.info('found open port', { port }); - } else { - const freePort = await detectPort(port); - if(freePort !== port) { - throw new Error(`Port ${port} is not free. Please use a different port`); - } - } - const opts = { ...defaultOptions, ...givenOptions, port }; - - if (opts.dbPath) { - fs.ensureDirSync(opts.dbPath); + log.info('launching', { retry, givenOptions }); + // launch will retry but ensure it will not retry indefinitely. + if (retry >= maxRetries) { + throw new Error('max retries hit for starting dynamodb emulator'); + } + + if (givenOptions.inMemory && givenOptions.dbPath) { + throw new Error('inMemory and dbPath are mutually exclusive options'); + } + let { port } = givenOptions; + if (!port) { + port = await detectPort(basePort); + log.info('found open port', { port }); + } else { + const freePort = await detectPort(port); + if (freePort !== port) { + throw new Error(`Port ${port} is not free. Please use a different port`); } - - const java = await which('java'); - const args = buildArgs(opts); - log.info('Spawning Emulator:', { args, cwd: emulatorPath }); - - const proc = execa(java, args, { - cwd: emulatorPath - }); - - function startingTimeout() { - log.error('Failed to start within timeout'); - // ensure process is halted. - proc.kill(); - const err = new Error('start has timed out!'); - err.code = 'timeout'; - throw err; - } - - // define this now so we can use it later to remove a listener. - let prematureExit; - // This is a fairly complex set of logic to retry starting - // the emulator if it fails to start. We need this logic due - // to possible race conditions between when we find an open - // port and bind to it. This situation is particularly common - // in jest where each test file is running in it's own process - // each competing for the open port. - try { - const waiter = wait(opts.startTimeout); - await Promise.race([ - new Promise(accept => { - function readStdoutBuffer(buffer) { - if (buffer.toString().indexOf(opts.port) !== -1) { - proc.stdout.removeListener('data', readStdoutBuffer); - log.info('Emulator has started but need to verify socket'); - accept( - waitPort({ - host: 'localhost', - port, - output: 'silent' - }) - ); - } - } - proc.stdout.on('data', readStdoutBuffer); - }), - waiter.promise.then(startingTimeout), - new Promise((accept, reject) => { - prematureExit = () => { - log.error('Dynamo DB Simulator has prematurely exited... need to retry'); - const err = new Error('premature exit'); - err.code = 'premature'; - proc.removeListener('exit', prematureExit); - reject(err); - }; - proc.on('exit', prematureExit); - }) - ]); - // eventually the process will exit... ensure our logic only - // will run on _premature_ exits. - proc.removeListener('exit', prematureExit); - waiter.cancel(); - - log.info('Successfully launched emulator on', { - port, - time: Date.now() - startTime - }); - } catch (err) { - // retry starting the emulator after a small "back off" time - // if we have a premature exit or the port is bound in a different process. - if (err.code === 'premature' || err.code === 'port_taken') { - if (givenOptions.port) { - throw new Error(`${givenOptions.port} is bound and unavailable`); - } - log.info('Queue retry in', retryInterval); - return wait(retryInterval).promise.then(() => - launch(givenOptions, retry + 1, startTime) + } + const opts = { ...defaultOptions, ...givenOptions, port }; + + if (opts.dbPath) { + fs.ensureDirSync(opts.dbPath); + } + + const java = await which('java'); + const args = buildArgs(opts); + log.info('Spawning Emulator:', { args, cwd: emulatorPath }); + + const proc = execa(java, args, { + cwd: emulatorPath, + }); + + function startingTimeout() { + log.error('Failed to start within timeout'); + // ensure process is halted. + proc.kill(); + const err = new Error('start has timed out!'); + err.code = 'timeout'; + throw err; + } + + // define this now so we can use it later to remove a listener. + let prematureExit; + // This is a fairly complex set of logic to retry starting + // the emulator if it fails to start. We need this logic due + // to possible race conditions between when we find an open + // port and bind to it. This situation is particularly common + // in jest where each test file is running in it's own process + // each competing for the open port. + try { + const waiter = wait(opts.startTimeout); + await Promise.race([ + new Promise(accept => { + function readStdoutBuffer(buffer) { + if (buffer.toString().indexOf(opts.port) !== -1) { + proc.stdout.removeListener('data', readStdoutBuffer); + log.info('Emulator has started but need to verify socket'); + accept( + waitPort({ + host: 'localhost', + port, + output: 'silent', + }) ); + } } - throw err; + proc.stdout.on('data', readStdoutBuffer); + }), + waiter.promise.then(startingTimeout), + new Promise((accept, reject) => { + prematureExit = () => { + log.error('Dynamo DB Simulator has prematurely exited... need to retry'); + const err = new Error('premature exit'); + err.code = 'premature'; + proc.removeListener('exit', prematureExit); + reject(err); + }; + proc.on('exit', prematureExit); + }), + ]); + // eventually the process will exit... ensure our logic only + // will run on _premature_ exits. + proc.removeListener('exit', prematureExit); + waiter.cancel(); + + log.info('Successfully launched emulator on', { + port, + time: Date.now() - startTime, + }); + } catch (err) { + // retry starting the emulator after a small "back off" time + // if we have a premature exit or the port is bound in a different process. + if (err.code === 'premature' || err.code === 'port_taken') { + if (givenOptions.port) { + throw new Error(`${givenOptions.port} is bound and unavailable`); + } + log.info('Queue retry in', retryInterval); + return wait(retryInterval).promise.then(() => launch(givenOptions, retry + 1, startTime)); } + throw err; + } - return new Emulator(proc, opts); + return new Emulator(proc, opts); } function getClient(emu, options = {}) { - const { DynamoDB } = require('aws-sdk'); - return new DynamoDB({ - endpoint: emu.url, - region: 'us-fake-1', - accessKeyId: 'fake', - secretAccessKey: 'fake', - ...options - }); + const { DynamoDB } = require('aws-sdk'); + return new DynamoDB({ + endpoint: emu.url, + region: 'us-fake-1', + accessKeyId: 'fake', + secretAccessKey: 'fake', + ...options, + }); } module.exports = { - launch, - getClient + launch, + getClient, }; diff --git a/packages/amplify-e2e-tests/__tests__/api.test.ts b/packages/amplify-e2e-tests/__tests__/api.test.ts index 5a7416b968..4356612b96 100644 --- a/packages/amplify-e2e-tests/__tests__/api.test.ts +++ b/packages/amplify-e2e-tests/__tests__/api.test.ts @@ -1,10 +1,5 @@ require('../src/aws-matchers/'); // custom matcher for assertion -import { - initProjectWithProfile, - deleteProject, - amplifyPush, - amplifyPushUpdate -} from '../src/init'; +import { initProjectWithProfile, deleteProject, amplifyPush, amplifyPushUpdate } from '../src/init'; import * as path from 'path'; import { existsSync } from 'fs'; import { addApiWithSchema, updateApiSchema, updateApiWithMultiAuth } from '../src/categories/api'; @@ -35,9 +30,9 @@ describe('amplify add api', () => { const { GraphQLAPIIdOutput, GraphQLAPIEndpointOutput, GraphQLAPIKeyOutput } = output; const { graphqlApi } = await getAppSyncApi(GraphQLAPIIdOutput, meta.providers.awscloudformation.Region); - expect(GraphQLAPIIdOutput).toBeDefined() - expect(GraphQLAPIEndpointOutput).toBeDefined() - expect(GraphQLAPIKeyOutput).toBeDefined() + expect(GraphQLAPIIdOutput).toBeDefined(); + expect(GraphQLAPIEndpointOutput).toBeDefined(); + expect(GraphQLAPIKeyOutput).toBeDefined(); expect(graphqlApi).toBeDefined(); expect(graphqlApi.apiId).toEqual(GraphQLAPIIdOutput); @@ -55,9 +50,9 @@ describe('amplify add api', () => { const { output } = getProjectMeta(projRoot).api[projectName]; const { GraphQLAPIIdOutput, GraphQLAPIEndpointOutput, GraphQLAPIKeyOutput } = output; - await expect(GraphQLAPIIdOutput).toBeDefined() - await expect(GraphQLAPIEndpointOutput).toBeDefined() - await expect(GraphQLAPIKeyOutput).toBeDefined() + await expect(GraphQLAPIIdOutput).toBeDefined(); + await expect(GraphQLAPIEndpointOutput).toBeDefined(); + await expect(GraphQLAPIKeyOutput).toBeDefined(); }); it('init a project and add the simple_model api with multiple authorization providers', async () => { @@ -94,9 +89,9 @@ describe('amplify add api', () => { expect(oidc.openIDConnectConfig.iatTTL).toEqual(1000); expect(oidc.openIDConnectConfig.authTTL).toEqual(2000); - expect(GraphQLAPIIdOutput).toBeDefined() - expect(GraphQLAPIEndpointOutput).toBeDefined() - expect(GraphQLAPIKeyOutput).toBeDefined() + expect(GraphQLAPIIdOutput).toBeDefined(); + expect(GraphQLAPIEndpointOutput).toBeDefined(); + expect(GraphQLAPIKeyOutput).toBeDefined(); expect(graphqlApi).toBeDefined(); expect(graphqlApi.apiId).toEqual(GraphQLAPIIdOutput); diff --git a/packages/amplify-e2e-tests/__tests__/auth.test.ts b/packages/amplify-e2e-tests/__tests__/auth.test.ts index 6d16f31a32..c08ff17e5e 100644 --- a/packages/amplify-e2e-tests/__tests__/auth.test.ts +++ b/packages/amplify-e2e-tests/__tests__/auth.test.ts @@ -1,11 +1,6 @@ require('../src/aws-matchers/'); // custom matcher for assertion const fs = require('fs'); -import { - initProjectWithProfile, - deleteProject, - amplifyPushAuth, - amplifyPush -} from '../src/init'; +import { initProjectWithProfile, deleteProject, amplifyPushAuth, amplifyPush } from '../src/init'; import { addAuthWithDefault, addAuthWithDefaultSocial, @@ -14,13 +9,13 @@ import { addAuthWithCustomTrigger, updateAuthWithoutCustomTrigger, addAuthViaAPIWithTrigger, - addAuthWithMaxOptions + addAuthWithMaxOptions, } from '../src/categories/auth'; import { createNewProjectDir, deleteProjectDir, getProjectMeta, getUserPool, getUserPoolClients, getLambdaFunction } from '../src/utils'; const defaultsSettings = { name: 'authTest', -} +}; describe('amplify add auth...', () => { let projRoot: string; @@ -41,7 +36,7 @@ describe('amplify add auth...', () => { const meta = getProjectMeta(projRoot); const id = Object.keys(meta.auth).map(key => meta.auth[key])[0].output.UserPoolId; const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); - await expect(userPool.UserPool).toBeDefined() + await expect(userPool.UserPool).toBeDefined(); }); it('...should init a project and add auth with defaultSocial', async () => { @@ -69,7 +64,7 @@ describe('amplify add auth...', () => { const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); const clients = await getUserPoolClients(id, meta.providers.awscloudformation.Region); - const lambdaFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region) + const lambdaFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region); await expect(userPool.UserPool).toBeDefined(); await expect(clients).toHaveLength(2); await expect(lambdaFunction).toBeDefined(); @@ -86,7 +81,7 @@ describe('amplify add auth...', () => { const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); const clients = await getUserPoolClients(id, meta.providers.awscloudformation.Region); - const lambdaFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region) + const lambdaFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region); await expect(userPool.UserPool).toBeDefined(); await expect(clients).toHaveLength(2); await expect(lambdaFunction).toBeDefined(); @@ -104,9 +99,9 @@ describe('amplify add auth...', () => { const verifyFunctionName = `${Object.keys(meta.auth)[0]}VerifyAuthChallengeResponse-integtest`; const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); const clients = await getUserPoolClients(id, meta.providers.awscloudformation.Region); - const createFunction = await getLambdaFunction(createFunctionName, meta.providers.awscloudformation.Region) - const defineFunction = await getLambdaFunction(defineFunctionName, meta.providers.awscloudformation.Region) - const verifyFunction = await getLambdaFunction(verifyFunctionName, meta.providers.awscloudformation.Region) + const createFunction = await getLambdaFunction(createFunctionName, meta.providers.awscloudformation.Region); + const defineFunction = await getLambdaFunction(defineFunctionName, meta.providers.awscloudformation.Region); + const verifyFunction = await getLambdaFunction(verifyFunctionName, meta.providers.awscloudformation.Region); await expect(userPool.UserPool).toBeDefined(); await expect(clients).toHaveLength(2); @@ -126,8 +121,8 @@ describe('amplify add auth...', () => { const defineFunctionName = `${Object.keys(meta.auth)[0]}DefineAuthChallenge-integtest`; const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); const clients = await getUserPoolClients(id, meta.providers.awscloudformation.Region); - const createFunction = await getLambdaFunction(createFunctionName, meta.providers.awscloudformation.Region) - const defineFunction = await getLambdaFunction(defineFunctionName, meta.providers.awscloudformation.Region) + const createFunction = await getLambdaFunction(createFunctionName, meta.providers.awscloudformation.Region); + const defineFunction = await getLambdaFunction(defineFunctionName, meta.providers.awscloudformation.Region); await expect(userPool.UserPool).toBeDefined(); await expect(clients).toHaveLength(2); @@ -136,8 +131,7 @@ describe('amplify add auth...', () => { await expect(createFunction.Configuration.Environment.Variables.MODULES).toEqual('custom'); await expect(defineFunction.Configuration.Environment.Variables.MODULES).toEqual('custom'); - }) - + }); }); describe('amplify updating auth...', () => { @@ -161,8 +155,8 @@ describe('amplify updating auth...', () => { const functionName = `${Object.keys(meta.auth)[0]}PreSignup-integtest`; const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); const clients = await getUserPoolClients(id, meta.providers.awscloudformation.Region); - const lambdaFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region) - const dirContents = fs.readdirSync(`${projRoot}/amplify/backend/function/${Object.keys(meta.auth)[0]}PreSignup/src`) + const lambdaFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region); + const dirContents = fs.readdirSync(`${projRoot}/amplify/backend/function/${Object.keys(meta.auth)[0]}PreSignup/src`); await expect(dirContents.includes('custom.js')).toBeTruthy(); await expect(userPool.UserPool).toBeDefined(); await expect(clients).toHaveLength(2); @@ -171,8 +165,8 @@ describe('amplify updating auth...', () => { await updateAuthWithoutCustomTrigger(projRoot, {}); await amplifyPushAuth(projRoot); - const updatedFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region) - const updatedDirContents = fs.readdirSync(`${projRoot}/amplify/backend/function/${Object.keys(meta.auth)[0]}PreSignup/src`) + const updatedFunction = await getLambdaFunction(functionName, meta.providers.awscloudformation.Region); + const updatedDirContents = fs.readdirSync(`${projRoot}/amplify/backend/function/${Object.keys(meta.auth)[0]}PreSignup/src`); await expect(updatedDirContents.includes('custom.js')).toBeFalsy(); await expect(updatedDirContents.includes('email-filter-blacklist.js')).toBeTruthy(); await expect(updatedFunction.Configuration.Environment.Variables.MODULES).toEqual('email-filter-blacklist'); diff --git a/packages/amplify-e2e-tests/__tests__/function.test.ts b/packages/amplify-e2e-tests/__tests__/function.test.ts index 3680e22a05..d21519f002 100644 --- a/packages/amplify-e2e-tests/__tests__/function.test.ts +++ b/packages/amplify-e2e-tests/__tests__/function.test.ts @@ -21,9 +21,7 @@ describe('amplify add function', () => { await functionBuild(projRoot, {}); await amplifyPushAuth(projRoot); const meta = getProjectMeta(projRoot); - const { Arn: functionArn, Name: functionName, Region: region } = Object.keys( - meta.function - ).map(key => meta.function[key])[0].output; + const { Arn: functionArn, Name: functionName, Region: region } = Object.keys(meta.function).map(key => meta.function[key])[0].output; expect(functionArn).toBeDefined(); expect(functionName).toBeDefined(); expect(region).toBeDefined(); diff --git a/packages/amplify-e2e-tests/__tests__/init.test.ts b/packages/amplify-e2e-tests/__tests__/init.test.ts index 86fdf9da58..a6a664adff 100644 --- a/packages/amplify-e2e-tests/__tests__/init.test.ts +++ b/packages/amplify-e2e-tests/__tests__/init.test.ts @@ -4,7 +4,7 @@ import { deleteProject, initProjectWithAccessKey, initNewEnvWithAccessKey, - initNewEnvWithProfile + initNewEnvWithProfile, } from '../src/init'; import { createNewProjectDir, deleteProjectDir, getEnvVars, getProjectMeta } from '../src/utils'; import { access } from 'fs'; @@ -40,7 +40,7 @@ describe('amplify init', () => { UnauthRoleName: newEnvUnAuthRoleName, UnauthRoleArn: newEnvUnauthRoleArn, AuthRoleArn: newEnvAuthRoleArn, - DeploymentBucketName: newEnvDeploymentBucketName + DeploymentBucketName: newEnvDeploymentBucketName, } = newEnvMeta; expect(newEnvAuthRoleName).not.toEqual(AuthRoleName); @@ -57,13 +57,11 @@ describe('amplify init', () => { it('should init project without profile', async () => { const { ACCESS_KEY_ID, SECRET_ACCESS_KEY } = getEnvVars(); if (!ACCESS_KEY_ID || !SECRET_ACCESS_KEY) { - throw new Error( - 'Set ACCESS_KEY_ID and SECRET_ACCESS_KEY either in .env file or as Environment variable' - ); + throw new Error('Set ACCESS_KEY_ID and SECRET_ACCESS_KEY either in .env file or as Environment variable'); } await initProjectWithAccessKey(projRoot, { accessKeyId: ACCESS_KEY_ID, - secretAccessKey: SECRET_ACCESS_KEY + secretAccessKey: SECRET_ACCESS_KEY, }); const meta = getProjectMeta(projRoot).providers.awscloudformation; @@ -78,7 +76,7 @@ describe('amplify init', () => { await initNewEnvWithAccessKey(projRoot, { envName: 'foo', accessKeyId: ACCESS_KEY_ID, - secretAccessKey: SECRET_ACCESS_KEY + secretAccessKey: SECRET_ACCESS_KEY, }); const newEnvMeta = getProjectMeta(projRoot).providers.awscloudformation; @@ -87,7 +85,7 @@ describe('amplify init', () => { UnauthRoleName: newEnvUnAuthRoleName, UnauthRoleArn: newEnvUnauthRoleArn, AuthRoleArn: newEnvAuthRoleArn, - DeploymentBucketName: newEnvDeploymentBucketName + DeploymentBucketName: newEnvDeploymentBucketName, } = newEnvMeta; expect(newEnvAuthRoleName).not.toEqual(AuthRoleName); diff --git a/packages/amplify-e2e-tests/__tests__/interactions.test.ts b/packages/amplify-e2e-tests/__tests__/interactions.test.ts index 0e07b0c0e2..99fae00c81 100644 --- a/packages/amplify-e2e-tests/__tests__/interactions.test.ts +++ b/packages/amplify-e2e-tests/__tests__/interactions.test.ts @@ -20,9 +20,9 @@ describe('amplify add interactions', () => { await addSampleInteraction(projRoot, {}); await amplifyPushAuth(projRoot); const meta = getProjectMeta(projRoot); - const { FunctionArn: functionArn, BotName: botName, Region: region } = Object.keys( - meta.interactions - ).map(key => meta.interactions[key])[0].output; + const { FunctionArn: functionArn, BotName: botName, Region: region } = Object.keys(meta.interactions).map( + key => meta.interactions[key] + )[0].output; expect(functionArn).toBeDefined(); expect(botName).toBeDefined(); expect(region).toBeDefined(); diff --git a/packages/amplify-e2e-tests/__tests__/migration/api.connection.migration.test.ts b/packages/amplify-e2e-tests/__tests__/migration/api.connection.migration.test.ts index 20b590de86..6f5458fb3f 100644 --- a/packages/amplify-e2e-tests/__tests__/migration/api.connection.migration.test.ts +++ b/packages/amplify-e2e-tests/__tests__/migration/api.connection.migration.test.ts @@ -1,10 +1,5 @@ require('../../src/aws-matchers/'); // custom matcher for assertion -import { - initProjectWithProfile, - deleteProject, - amplifyPush, - amplifyPushUpdate -} from '../../src/init'; +import { initProjectWithProfile, deleteProject, amplifyPush, amplifyPushUpdate } from '../../src/init'; import { addApiWithSchema, updateApiSchema } from '../../src/categories/api'; import { createNewProjectDir, deleteProjectDir } from '../../src/utils'; @@ -29,8 +24,8 @@ describe('amplify add api', () => { await amplifyPush(projRoot); updateApiSchema(projRoot, projectName, nextSchema1); await amplifyPushUpdate( - projRoot, - /Attempting to edit the global secondary index gsi-PostComments on the CommentTable table in the Comment stack.*/ + projRoot, + /Attempting to edit the global secondary index gsi-PostComments on the CommentTable table in the Comment stack.*/ ); }); @@ -43,8 +38,8 @@ describe('amplify add api', () => { await amplifyPush(projRoot); updateApiSchema(projRoot, projectName, nextSchema1); await amplifyPushUpdate( - projRoot, - /Attempting to add and remove a global secondary index at the same time on the CommentTable table in the Comment stack.*/ + projRoot, + /Attempting to add and remove a global secondary index at the same time on the CommentTable table in the Comment stack.*/ ); }); @@ -57,8 +52,8 @@ describe('amplify add api', () => { await amplifyPush(projRoot); updateApiSchema(projRoot, projectName, nextSchema1); await amplifyPushUpdate( - projRoot, - /Attempting to edit the global secondary index gsi-PostComments on the CommentTable table in the Comment stack.*/ + projRoot, + /Attempting to edit the global secondary index gsi-PostComments on the CommentTable table in the Comment stack.*/ ); }); @@ -78,8 +73,7 @@ describe('amplify add api', () => { it(`init project, run valid migration to add a connection that uses the \ existing key fields, then run another migration that removes the old connection \ - and renames the field.`, - async () => { + and renames the field.`, async () => { const projectName = 'addremoveconnection'; const initialSchema = 'migrations_connection/initial_schema.graphql'; const nextSchema1 = 'migrations_connection/add_a_sort_key_before_remove.graphql'; diff --git a/packages/amplify-e2e-tests/__tests__/migration/api.key.migration.test.ts b/packages/amplify-e2e-tests/__tests__/migration/api.key.migration.test.ts index 39bd184209..6e12712f43 100644 --- a/packages/amplify-e2e-tests/__tests__/migration/api.key.migration.test.ts +++ b/packages/amplify-e2e-tests/__tests__/migration/api.key.migration.test.ts @@ -1,10 +1,5 @@ require('../../src/aws-matchers/'); // custom matcher for assertion -import { - initProjectWithProfile, - deleteProject, - amplifyPush, - amplifyPushUpdate -} from '../../src/init'; +import { initProjectWithProfile, deleteProject, amplifyPush, amplifyPushUpdate } from '../../src/init'; import { addApiWithSchema, updateApiSchema } from '../../src/categories/api'; import { createNewProjectDir, deleteProjectDir } from '../../src/utils'; @@ -29,8 +24,8 @@ describe('amplify add api', () => { await amplifyPush(projRoot); updateApiSchema(projRoot, projectName, nextSchema1); await amplifyPushUpdate( - projRoot, - /Attempting to add a local secondary index to the TodoTable table in the Todo stack. Local secondary indexes must be created when the table is created.*/ + projRoot, + /Attempting to add a local secondary index to the TodoTable table in the Todo stack. Local secondary indexes must be created when the table is created.*/ ); }); @@ -42,10 +37,7 @@ describe('amplify add api', () => { await addApiWithSchema(projRoot, initialSchema); await amplifyPush(projRoot); updateApiSchema(projRoot, projectName, nextSchema1); - await amplifyPushUpdate( - projRoot, - /Attempting to edit the global secondary index SomeGSI on the TodoTable table in the Todo stack.*/ - ); + await amplifyPushUpdate(projRoot, /Attempting to edit the global secondary index SomeGSI on the TodoTable table in the Todo stack.*/); }); it('init project, run invalid migration trying to change the key schema, and check for error', async () => { @@ -56,10 +48,7 @@ describe('amplify add api', () => { await addApiWithSchema(projRoot, initialSchema); await amplifyPush(projRoot); updateApiSchema(projRoot, projectName, nextSchema1); - await amplifyPushUpdate( - projRoot, - /Attempting to edit the key schema of the TodoTable table in the Todo stack.*/ - ); + await amplifyPushUpdate(projRoot, /Attempting to edit the key schema of the TodoTable table in the Todo stack.*/); }); it('init project, run invalid migration trying to change an lsi, and check for error', async () => { @@ -70,10 +59,7 @@ describe('amplify add api', () => { await addApiWithSchema(projRoot, initialSchema); await amplifyPush(projRoot); updateApiSchema(projRoot, projectName, nextSchema1); - await amplifyPushUpdate( - projRoot, - /Attempting to edit the local secondary index SomeLSI on the TodoTable table in the Todo stack.*/ - ); + await amplifyPushUpdate(projRoot, /Attempting to edit the local secondary index SomeLSI on the TodoTable table in the Todo stack.*/); }); it('init project, run valid migration adding a GSI', async () => { diff --git a/packages/amplify-e2e-tests/__tests__/predictions.test.ts b/packages/amplify-e2e-tests/__tests__/predictions.test.ts index 86fd69766d..c58504c4e9 100644 --- a/packages/amplify-e2e-tests/__tests__/predictions.test.ts +++ b/packages/amplify-e2e-tests/__tests__/predictions.test.ts @@ -4,48 +4,47 @@ import { createNewProjectDir, deleteProjectDir, getProjectMeta, getAWSExports, g import { addConvert, addInterpret, addIdentifyCollection } from '../src/categories/predictions'; import { addAuthWithDefault } from '../src/categories/auth'; - describe('amplify add predictions', () => { - let projRoot: string; - beforeEach(() => { - projRoot = createNewProjectDir(); - jest.setTimeout(1000 * 60 * 60); // 1 hour - }); + let projRoot: string; + beforeEach(() => { + projRoot = createNewProjectDir(); + jest.setTimeout(1000 * 60 * 60); // 1 hour + }); - afterEach(async () => { - await deleteProject(projRoot); - deleteProjectDir(projRoot); - }); + afterEach(async () => { + await deleteProject(projRoot); + deleteProjectDir(projRoot); + }); - it('init a project with convert subcategory translate text', async () => { - await initProjectWithProfile(projRoot, {}); - await addAuthWithDefault(projRoot, {}); - await addConvert(projRoot, {}); - await addInterpret(projRoot, {}); - await amplifyPushAuth(projRoot); - const awsExports: any = getAWSExports(projRoot).default; - console.log(`AWS Exports \n${JSON.stringify(awsExports, null, 4)}`); - const { sourceLanguage, targetLanguage } = awsExports.predictions.convert.translateText.defaults; - const { type } = awsExports.predictions.interpret.interpretText.defaults; - expect(sourceLanguage).toBeDefined(); - expect(targetLanguage).toBeDefined(); - expect(type).toEqual('ALL'); - }); + it('init a project with convert subcategory translate text', async () => { + await initProjectWithProfile(projRoot, {}); + await addAuthWithDefault(projRoot, {}); + await addConvert(projRoot, {}); + await addInterpret(projRoot, {}); + await amplifyPushAuth(projRoot); + const awsExports: any = getAWSExports(projRoot).default; + console.log(`AWS Exports \n${JSON.stringify(awsExports, null, 4)}`); + const { sourceLanguage, targetLanguage } = awsExports.predictions.convert.translateText.defaults; + const { type } = awsExports.predictions.interpret.interpretText.defaults; + expect(sourceLanguage).toBeDefined(); + expect(targetLanguage).toBeDefined(); + expect(type).toEqual('ALL'); + }); - it('init a project with identify sub category identifyEntities with collection config', async () => { - await initProjectWithProfile(projRoot, {}); - await addAuthWithDefault(projRoot, {}); - await addIdentifyCollection(projRoot, {}); - await amplifyPushAuth(projRoot); - const awsExports: any = getAWSExports(projRoot).default; - console.log(`AWS Exports \n${JSON.stringify(awsExports, null, 4)}`); - const { collectionId: collectionID, maxEntities: maxFaces } = awsExports.predictions.identify.identifyEntities.defaults; - const { region, celebrityDetectionEnabled } = awsExports.predictions.identify.identifyEntities; - expect(collectionID).toBeDefined(); - expect(maxFaces).toEqual(50); - expect(celebrityDetectionEnabled).toBeTruthy(); - const cID = await getCollection(collectionID, region); - console.log(`Rekog Collection Response ${JSON.stringify(cID, null, 4)}`); - expect(cID.CollectionARN.split("/").pop()).toEqual(collectionID); - }); -}); \ No newline at end of file + it('init a project with identify sub category identifyEntities with collection config', async () => { + await initProjectWithProfile(projRoot, {}); + await addAuthWithDefault(projRoot, {}); + await addIdentifyCollection(projRoot, {}); + await amplifyPushAuth(projRoot); + const awsExports: any = getAWSExports(projRoot).default; + console.log(`AWS Exports \n${JSON.stringify(awsExports, null, 4)}`); + const { collectionId: collectionID, maxEntities: maxFaces } = awsExports.predictions.identify.identifyEntities.defaults; + const { region, celebrityDetectionEnabled } = awsExports.predictions.identify.identifyEntities; + expect(collectionID).toBeDefined(); + expect(maxFaces).toEqual(50); + expect(celebrityDetectionEnabled).toBeTruthy(); + const cID = await getCollection(collectionID, region); + console.log(`Rekog Collection Response ${JSON.stringify(cID, null, 4)}`); + expect(cID.CollectionARN.split('/').pop()).toEqual(collectionID); + }); +}); diff --git a/packages/amplify-e2e-tests/__tests__/storage.test.ts b/packages/amplify-e2e-tests/__tests__/storage.test.ts index b033832424..4444c28b5e 100644 --- a/packages/amplify-e2e-tests/__tests__/storage.test.ts +++ b/packages/amplify-e2e-tests/__tests__/storage.test.ts @@ -4,7 +4,6 @@ import { addAuthWithDefault } from '../src/categories/auth'; import { addSimpleDDB, addDDBWithTrigger, updateDDBWithTrigger, addS3WithTrigger } from '../src/categories/storage'; import { createNewProjectDir, deleteProjectDir, getProjectMeta, getDDBTable, checkIfBucketExists } from '../src/utils'; - describe('amplify add/update storage(S3)', () => { let projRoot: string; beforeEach(() => { @@ -24,16 +23,13 @@ describe('amplify add/update storage(S3)', () => { await amplifyPushAuth(projRoot); const meta = getProjectMeta(projRoot); - const { BucketName: bucketName, Region: region } = Object.keys( - meta.storage - ).map(key => meta.storage[key])[0].output; + const { BucketName: bucketName, Region: region } = Object.keys(meta.storage).map(key => meta.storage[key])[0].output; expect(bucketName).toBeDefined(); expect(region).toBeDefined(); const bucketExists = await checkIfBucketExists(bucketName, region); expect(bucketExists).toMatchObject({}); - }); }); @@ -58,28 +54,27 @@ describe('amplify add/update storage(DDB)', () => { await amplifyPushAuth(projRoot); const meta = getProjectMeta(projRoot); - const { Name: table1Name, Arn: table1Arn, Region: table1Region, StreamArn: table1StreamArn } = Object.keys( - meta.storage - ).map(key => meta.storage[key])[0].output; + const { Name: table1Name, Arn: table1Arn, Region: table1Region, StreamArn: table1StreamArn } = Object.keys(meta.storage).map( + key => meta.storage[key] + )[0].output; expect(table1Name).toBeDefined(); expect(table1Arn).toBeDefined(); expect(table1Region).toBeDefined(); - expect(table1StreamArn).toBeDefined() + expect(table1StreamArn).toBeDefined(); const table1Configs = await getDDBTable(table1Name, table1Region); expect(table1Configs.Table.TableArn).toEqual(table1Arn); - const { Name: table2Name, Arn: table2Arn, Region: table2Region, StreamArn: table2StreamArn } = Object.keys( - meta.storage - ).map(key => meta.storage[key])[1].output; + const { Name: table2Name, Arn: table2Arn, Region: table2Region, StreamArn: table2StreamArn } = Object.keys(meta.storage).map( + key => meta.storage[key] + )[1].output; expect(table2Name).toBeDefined(); expect(table2Arn).toBeDefined(); expect(table2Region).toBeDefined(); - expect(table2StreamArn).toBeDefined() + expect(table2StreamArn).toBeDefined(); const table2Configs = await getDDBTable(table2Name, table2Region); expect(table2Configs.Table.TableArn).toEqual(table2Arn); - }); }); diff --git a/packages/amplify-e2e-tests/src/aws-matchers/iamMatcher.ts b/packages/amplify-e2e-tests/src/aws-matchers/iamMatcher.ts index af5805eed5..5f0d77d978 100644 --- a/packages/amplify-e2e-tests/src/aws-matchers/iamMatcher.ts +++ b/packages/amplify-e2e-tests/src/aws-matchers/iamMatcher.ts @@ -5,7 +5,7 @@ expect.extend({ let pass: boolean; let message: string; try { - const {Role: role} = await iam.getRole({ RoleName: roleName }).promise(); + const { Role: role } = await iam.getRole({ RoleName: roleName }).promise(); if (arn) { pass = role.Arn === arn ? true : false; if (pass) { @@ -23,8 +23,8 @@ expect.extend({ const result = { message: () => message, - pass + pass, }; return result; - } + }, }); diff --git a/packages/amplify-e2e-tests/src/aws-matchers/index.ts b/packages/amplify-e2e-tests/src/aws-matchers/index.ts index cb96ea8505..61b94fd28b 100644 --- a/packages/amplify-e2e-tests/src/aws-matchers/index.ts +++ b/packages/amplify-e2e-tests/src/aws-matchers/index.ts @@ -1,2 +1,2 @@ require('./s3matcher'); -require('./iamMatcher'); \ No newline at end of file +require('./iamMatcher'); diff --git a/packages/amplify-e2e-tests/src/aws-matchers/s3matcher.ts b/packages/amplify-e2e-tests/src/aws-matchers/s3matcher.ts index 8a0585d039..99e50fdc80 100644 --- a/packages/amplify-e2e-tests/src/aws-matchers/s3matcher.ts +++ b/packages/amplify-e2e-tests/src/aws-matchers/s3matcher.ts @@ -10,12 +10,10 @@ expect.extend({ pass = false; } - const messageStr = pass - ? `expected S3 bucket ${bucketName} exist` - : `expected S3 bucket ${bucketName} does exist`; + const messageStr = pass ? `expected S3 bucket ${bucketName} exist` : `expected S3 bucket ${bucketName} does exist`; return { message: () => messageStr, pass, }; - } + }, }); diff --git a/packages/amplify-e2e-tests/src/categories/api.ts b/packages/amplify-e2e-tests/src/categories/api.ts index c7b800af9a..8d52f5784d 100644 --- a/packages/amplify-e2e-tests/src/categories/api.ts +++ b/packages/amplify-e2e-tests/src/categories/api.ts @@ -9,7 +9,7 @@ const defaultSettings = { }; function readSchemaDocument(schemaName: string): string { - const docPath = `${__dirname}/../../schemas/${schemaName}.graphql` + const docPath = `${__dirname}/../../schemas/${schemaName}.graphql`; if (fs.existsSync(docPath)) { return fs.readFileSync(docPath).toString(); } else { @@ -18,14 +18,10 @@ function readSchemaDocument(schemaName: string): string { } function getSchemaPath(schemaName: string): string { - return `${__dirname}/../../schemas/${schemaName}`; + return `${__dirname}/../../schemas/${schemaName}`; } -export function addApiWithSchema( - cwd: string, - schemaFile: string, - verbose: boolean = !isCI() -) { +export function addApiWithSchema(cwd: string, schemaFile: string, verbose: boolean = !isCI()) { const schemaPath = getSchemaPath(schemaFile); return new Promise((resolve, reject) => { nexpect @@ -47,32 +43,26 @@ export function addApiWithSchema( .wait('Provide your schema file path:') .sendline(schemaPath) // tslint:disable-next-line - .wait('"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud') + .wait( + '"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud' + ) .run(function(err: Error) { if (!err) { resolve(); } else { reject(err); } - }) - }) + }); + }); } -export function updateApiSchema( - cwd: string, - projectName: string, - schemaName: string -) { +export function updateApiSchema(cwd: string, projectName: string, schemaName: string) { const testSchemaPath = getSchemaPath(schemaName); const schemaText = fs.readFileSync(testSchemaPath).toString(); updateSchema(cwd, projectName, schemaText); } -export function updateApiWithMultiAuth( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function updateApiWithMultiAuth(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['update', 'api'], { cwd, stripColors: true, verbose }) @@ -114,6 +104,6 @@ export function updateApiWithMultiAuth( } else { reject(err); } - }) - }) + }); + }); } diff --git a/packages/amplify-e2e-tests/src/categories/auth.ts b/packages/amplify-e2e-tests/src/categories/auth.ts index 49a3d816fd..6ff6888969 100644 --- a/packages/amplify-e2e-tests/src/categories/auth.ts +++ b/packages/amplify-e2e-tests/src/categories/auth.ts @@ -1,16 +1,10 @@ - import * as nexpect from 'nexpect'; import { join } from 'path'; import * as fs from 'fs'; import { getCLIPath, isCI, getEnvVars } from '../utils'; - -export function addAuthWithDefault( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function addAuthWithDefault(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true, verbose }) @@ -28,15 +22,11 @@ export function addAuthWithDefault( } else { reject(err); } - }) - }) + }); + }); } -export function addAuthWithGroupTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI(), -) { +export function addAuthWithGroupTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true, verbose }) @@ -64,15 +54,11 @@ export function addAuthWithGroupTrigger( } else { reject(err); } - }) - }) + }); + }); } -export function addAuthViaAPIWithTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI(), -) { +export function addAuthViaAPIWithTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'api'], { cwd, stripColors: true, verbose }) @@ -120,15 +106,11 @@ export function addAuthViaAPIWithTrigger( } else { reject(err); } - }) - }) + }); + }); } -export function addAuthWithCustomTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI(), -) { +export function addAuthWithCustomTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true, verbose }) @@ -163,7 +145,7 @@ export function addAuthWithCustomTrigger( .send('\r') .wait(' What attributes are required for signing up?') .send('\r') - .wait('Specify the app\'s refresh token expiration period (in days):') + .wait("Specify the app's refresh token expiration period (in days):") .send('\r') .wait('Do you want to specify the user attributes this app can read and write?') .send('\r') @@ -197,15 +179,11 @@ export function addAuthWithCustomTrigger( } else { reject(err); } - }) - }) + }); + }); } -export function updateAuthWithoutCustomTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI(), -) { +export function updateAuthWithoutCustomTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['update', 'auth'], { cwd, stripColors: true, verbose }) @@ -229,7 +207,7 @@ export function updateAuthWithoutCustomTrigger( .send('\r') .wait('Do you want to override the default password policy for this User Pool?') .send('\r') - .wait('Specify the app\'s refresh token expiration period (in days):') + .wait("Specify the app's refresh token expiration period (in days):") .send('\r') .wait('Do you want to specify the user attributes this app can read and write?') .send('\r') @@ -251,15 +229,11 @@ export function updateAuthWithoutCustomTrigger( } else { reject(err); } - }) - }) + }); + }); } -export function addAuthWithRecaptchaTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI(), -) { +export function addAuthWithRecaptchaTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true, verbose }) @@ -293,32 +267,33 @@ export function addAuthWithRecaptchaTrigger( } else { reject(err); } - }) - }) + }); + }); } -export function addAuthWithDefaultSocial( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function addAuthWithDefaultSocial(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { - const { - FACEBOOK_APP_ID, - FACEBOOK_APP_SECRET, - GOOGLE_APP_ID, - GOOGLE_APP_SECRET, - AMAZON_APP_ID, - AMAZON_APP_SECRET, - }: any = getEnvVars(); + const { FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, GOOGLE_APP_ID, GOOGLE_APP_SECRET, AMAZON_APP_ID, AMAZON_APP_SECRET }: any = getEnvVars(); const missingVars = []; - if (!FACEBOOK_APP_ID) { missingVars.push('FACEBOOK_APP_ID') }; - if (!FACEBOOK_APP_SECRET) { missingVars.push('FACEBOOK_APP_SECRET') }; - if (!GOOGLE_APP_ID) { missingVars.push('GOOGLE_APP_ID') }; - if (!GOOGLE_APP_SECRET) { missingVars.push('GOOGLE_APP_SECRET') }; - if (!AMAZON_APP_ID) { missingVars.push('AMAZON_APP_ID') }; - if (!AMAZON_APP_SECRET) { missingVars.push('AMAZON_APP_SECRET') }; + if (!FACEBOOK_APP_ID) { + missingVars.push('FACEBOOK_APP_ID'); + } + if (!FACEBOOK_APP_SECRET) { + missingVars.push('FACEBOOK_APP_SECRET'); + } + if (!GOOGLE_APP_ID) { + missingVars.push('GOOGLE_APP_ID'); + } + if (!GOOGLE_APP_SECRET) { + missingVars.push('GOOGLE_APP_SECRET'); + } + if (!AMAZON_APP_ID) { + missingVars.push('AMAZON_APP_ID'); + } + if (!AMAZON_APP_SECRET) { + missingVars.push('AMAZON_APP_SECRET'); + } if (missingVars.length > 0) { throw new Error(`.env file is missing the following key/values: ${missingVars.join(', ')} `); @@ -374,15 +349,11 @@ export function addAuthWithDefaultSocial( } else { reject(err); } - }) - }) + }); + }); } -export function addAuthWithMaxOptions( - cwd: string, - settings: any, - verbose: boolean = !isCI(), -) { +export function addAuthWithMaxOptions(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true, verbose }) @@ -554,6 +525,6 @@ export function addAuthWithMaxOptions( } else { reject(err); } - }) - }) + }); + }); } diff --git a/packages/amplify-e2e-tests/src/categories/function.ts b/packages/amplify-e2e-tests/src/categories/function.ts index b03172da66..d79a87bf4e 100644 --- a/packages/amplify-e2e-tests/src/categories/function.ts +++ b/packages/amplify-e2e-tests/src/categories/function.ts @@ -1,4 +1,3 @@ - import * as nexpect from 'nexpect'; import { join } from 'path'; import * as fs from 'fs'; @@ -8,12 +7,7 @@ const defaultSettings = { projectName: 'CLI Function test', }; - -export function addHelloWorldFunction( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function addHelloWorldFunction(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'function'], { cwd, stripColors: true, verbose }) @@ -35,15 +29,11 @@ export function addHelloWorldFunction( } else { reject(err); } - }) - }) + }); + }); } -export function functionBuild( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function functionBuild(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['function', 'build'], { cwd, stripColors: true, verbose }) @@ -57,6 +47,6 @@ export function functionBuild( } else { reject(err); } - }) - }) + }); + }); } diff --git a/packages/amplify-e2e-tests/src/categories/interactions.ts b/packages/amplify-e2e-tests/src/categories/interactions.ts index 9cc7554a1c..15acc369be 100644 --- a/packages/amplify-e2e-tests/src/categories/interactions.ts +++ b/packages/amplify-e2e-tests/src/categories/interactions.ts @@ -7,12 +7,7 @@ const defaultSettings = { projectName: 'CLI Interaction test', }; - -export function addSampleInteraction( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function addSampleInteraction(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'interactions'], { cwd, stripColors: true, verbose }) @@ -22,7 +17,7 @@ export function addSampleInteraction( .sendline('\r') .wait('Choose a sample chatbot:') .sendline('\r') - .wait('Please indicate if your use of this bot is subject to the Children\'s') + .wait("Please indicate if your use of this bot is subject to the Children's") .sendline('y') .sendEof() // tslint:disable-next-line @@ -32,8 +27,6 @@ export function addSampleInteraction( } else { reject(err); } - }) - }) + }); + }); } - - diff --git a/packages/amplify-e2e-tests/src/categories/predictions.ts b/packages/amplify-e2e-tests/src/categories/predictions.ts index c2a9e7bddc..1daf714fac 100644 --- a/packages/amplify-e2e-tests/src/categories/predictions.ts +++ b/packages/amplify-e2e-tests/src/categories/predictions.ts @@ -1,118 +1,106 @@ -import { isCI } from "../utils"; +import { isCI } from '../utils'; import * as nexpect from 'nexpect'; import { getCLIPath } from '../utils/index'; // add convert resource -export function addConvert( - cwd: string, - settings: any, - verbose: boolean = !isCI() - ) { - const resourceName = 'convertTest1'; - return new Promise((resolve, reject) => { - nexpect - .spawn(getCLIPath(), ['predictions', 'add'], { cwd, stripColors: true, verbose } ) - .wait('Please select from one of the categories below') - // j = down arrow - .sendline('j') - .sendline('\r') - .wait('What would you like to convert?') - .sendline('\r') - .wait('Provide a friendly name for your resource') - .sendline(`${resourceName}\r`) - .wait('What is the source language?') - .sendline('\r') - .wait('What is the target language?') - .sendline('\r') - .wait('Who should have access?') - .sendline('j') - .sendline('\r') - .sendEof() - // .send - .run(function(err: Error) { - if (!err) { - resolve(resourceName); - } else { - reject(err); - } - }) - }) +export function addConvert(cwd: string, settings: any, verbose: boolean = !isCI()) { + const resourceName = 'convertTest1'; + return new Promise((resolve, reject) => { + nexpect + .spawn(getCLIPath(), ['predictions', 'add'], { cwd, stripColors: true, verbose }) + .wait('Please select from one of the categories below') + // j = down arrow + .sendline('j') + .sendline('\r') + .wait('What would you like to convert?') + .sendline('\r') + .wait('Provide a friendly name for your resource') + .sendline(`${resourceName}\r`) + .wait('What is the source language?') + .sendline('\r') + .wait('What is the target language?') + .sendline('\r') + .wait('Who should have access?') + .sendline('j') + .sendline('\r') + .sendEof() + // .send + .run(function(err: Error) { + if (!err) { + resolve(resourceName); + } else { + reject(err); + } + }); + }); } // add identify test -export function addIdentifyCollection( - cwd: string, - settings: any, - verbose: boolean = !isCI() - ) { - const resourceName = 'identifyCollectionTest1'; - return new Promise((resolve, reject) => { - nexpect - .spawn(getCLIPath(), ['predictions', 'add'], { cwd, stripColors: true, verbose } ) - .wait('Please select from one of the categories below') - // j = down arrow - // .sendline('j') - .sendline('\r') - .wait('What would you like to identify?') - .sendline('j') - .sendline('\r') - .wait('Provide a friendly name for your resource') - .sendline(`${resourceName}\r`) - .wait('Would you like use the default configuration?') - .sendline('j') - .sendline('\r') - .wait('Would you like to enable celebrity detection?') - .sendline('y\r') - .wait('Would you like to identify entities from a collection of images?') - .sendline('y\r') - .wait('How many entities would you like to identify?') - .sendline('\r') - .wait('Would you like to allow users to add images to this collection?') - .sendline('y\r') - .wait('Who should have access?') - .sendline('j\r') - .wait('The CLI would be provisioning an S3 bucket') - .sendline('\r') - .sendEof() - // .send - .run(function(err: Error) { - if (!err) { - resolve(resourceName); - } else { - reject(err); - } - }) - }) +export function addIdentifyCollection(cwd: string, settings: any, verbose: boolean = !isCI()) { + const resourceName = 'identifyCollectionTest1'; + return new Promise((resolve, reject) => { + nexpect + .spawn(getCLIPath(), ['predictions', 'add'], { cwd, stripColors: true, verbose }) + .wait('Please select from one of the categories below') + // j = down arrow + // .sendline('j') + .sendline('\r') + .wait('What would you like to identify?') + .sendline('j') + .sendline('\r') + .wait('Provide a friendly name for your resource') + .sendline(`${resourceName}\r`) + .wait('Would you like use the default configuration?') + .sendline('j') + .sendline('\r') + .wait('Would you like to enable celebrity detection?') + .sendline('y\r') + .wait('Would you like to identify entities from a collection of images?') + .sendline('y\r') + .wait('How many entities would you like to identify?') + .sendline('\r') + .wait('Would you like to allow users to add images to this collection?') + .sendline('y\r') + .wait('Who should have access?') + .sendline('j\r') + .wait('The CLI would be provisioning an S3 bucket') + .sendline('\r') + .sendEof() + // .send + .run(function(err: Error) { + if (!err) { + resolve(resourceName); + } else { + reject(err); + } + }); + }); } // add interpret resource -export function addInterpret( - cwd: string, - settings: any, - verbose: boolean = !isCI() - ) { - const resourceName = 'interpretTest1'; - return new Promise((resolve, reject) => { - nexpect - .spawn(getCLIPath(), ['add', 'predictions'], { cwd, stripColors: true, verbose } ) - .wait('Please select from one of the categories below') - // j = down arrow - .sendline('jj') - .wait('What would you like to interpret?') - .sendline('\r') - .wait('Provide a friendly name for your resource') - .sendline(`${resourceName}\r`) - .wait('What kind of interpretation would you like?') - .sendline('k') - .wait('Who should have access?') - .sendline('j') - .sendEof() - .run(function(err: Error) { - if (!err) { - resolve(resourceName); - } else { - reject(err); - } - }) - }) +export function addInterpret(cwd: string, settings: any, verbose: boolean = !isCI()) { + const resourceName = 'interpretTest1'; + return new Promise((resolve, reject) => { + nexpect + .spawn(getCLIPath(), ['add', 'predictions'], { cwd, stripColors: true, verbose }) + .wait('Please select from one of the categories below') + // j = down arrow + .sendline('jj') + .wait('What would you like to interpret?') + .sendline('\r') + .wait('Provide a friendly name for your resource') + .sendline(`${resourceName}\r`) + .wait('What kind of interpretation would you like?') + .sendline('k') + .wait('Who should have access?') + .sendline('j') + .sendEof() + .run(function(err: Error) { + if (!err) { + resolve(resourceName); + } else { + reject(err); + } + }); + }); } diff --git a/packages/amplify-e2e-tests/src/categories/storage.ts b/packages/amplify-e2e-tests/src/categories/storage.ts index c059f29049..c0ac7eadae 100644 --- a/packages/amplify-e2e-tests/src/categories/storage.ts +++ b/packages/amplify-e2e-tests/src/categories/storage.ts @@ -1,4 +1,3 @@ - import * as nexpect from 'nexpect'; import { join } from 'path'; import * as fs from 'fs'; @@ -8,12 +7,7 @@ const defaultSettings = { projectName: 'CLI Storage test', }; - -export function addSimpleDDB( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function addSimpleDDB(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true, verbose }) @@ -52,15 +46,11 @@ export function addSimpleDDB( } else { reject(err); } - }) - }) + }); + }); } -export function addDDBWithTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function addDDBWithTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true, verbose }) @@ -106,19 +96,15 @@ export function addDDBWithTrigger( } else { reject(err); } - }) - }) + }); + }); } -export function updateDDBWithTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function updateDDBWithTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['update', 'storage'], { cwd, stripColors: true, verbose }) - .wait('Please select from one of the below mentioned services') + .wait('Please select from one of the below mentioned services') // j = down arrow .sendline('j') .sendline('\r') @@ -130,7 +116,7 @@ export function updateDDBWithTrigger( .wait('Do you want to add global secondary indexes to your table') .sendline('n') .sendline('\r') - .wait('Do you want to add a Lambda Trigger for your Table') + .wait('Do you want to add a Lambda Trigger for your Table') .sendline('y') .sendline('\r') .wait('Select from the following options') @@ -148,16 +134,11 @@ export function updateDDBWithTrigger( } else { reject(err); } - }) - }) + }); + }); } - -export function addS3WithTrigger( - cwd: string, - settings: any, - verbose: boolean = !isCI() -) { +export function addS3WithTrigger(cwd: string, settings: any, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true, verbose }) @@ -189,7 +170,6 @@ export function addS3WithTrigger( } else { reject(err); } - }) - }) + }); + }); } - diff --git a/packages/amplify-e2e-tests/src/configure/index.ts b/packages/amplify-e2e-tests/src/configure/index.ts index 242307b5f4..d228b5c841 100644 --- a/packages/amplify-e2e-tests/src/configure/index.ts +++ b/packages/amplify-e2e-tests/src/configure/index.ts @@ -3,9 +3,9 @@ import { join } from 'path'; import { getCLIPath, isCI } from '../utils'; type AmplifyConfiguration = { - accessKeyId: string, - secretAccessKey: string, - profileName?: string, + accessKeyId: string; + secretAccessKey: string; + profileName?: string; }; const defaultSettings = { profileName: 'amplify-integ-test-user', @@ -14,10 +14,7 @@ const defaultSettings = { }; const MANDATORY_PARAMS = ['accessKeyId', 'secretAccessKey']; -export default function amplifyConfigure( - settings: AmplifyConfiguration, - verbose: Boolean = isCI() ? false : true -) { +export default function amplifyConfigure(settings: AmplifyConfiguration, verbose: Boolean = isCI() ? false : true) { const s = { ...defaultSettings, ...settings }; const missingParam = MANDATORY_PARAMS.filter(p => !Object.keys(s).includes(p)); if (missingParam.length) { @@ -34,7 +31,7 @@ export default function amplifyConfigure( .sendline('\r') .wait('user name:') .sendline('\r') - .wait("Press Enter to continue") + .wait('Press Enter to continue') .sendline('\r') .wait('accessKeyId') .sendline(s.accessKeyId) @@ -42,9 +39,7 @@ export default function amplifyConfigure( .sendline(s.secretAccessKey) .wait('Profile Name:') .sendline(s.profileName) - .wait( - 'Successfully set up the new user.' - ) + .wait('Successfully set up the new user.') .run(function(err) { if (!err) { resolve(); diff --git a/packages/amplify-e2e-tests/src/configure_tests.ts b/packages/amplify-e2e-tests/src/configure_tests.ts index bdcff597e0..ad3cc76484 100644 --- a/packages/amplify-e2e-tests/src/configure_tests.ts +++ b/packages/amplify-e2e-tests/src/configure_tests.ts @@ -15,14 +15,12 @@ async function setUpAmplify() { AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { - throw new Error( - 'Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env' - ); + throw new Error('Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env'); } await configure({ accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY, - profileName: 'amplify-integ-test-user' + profileName: 'amplify-integ-test-user', }); } else { console.log('AWS Profile is already configured'); diff --git a/packages/amplify-e2e-tests/src/init/amplifyPush.ts b/packages/amplify-e2e-tests/src/init/amplifyPush.ts index c3241ff38e..cb25b148db 100644 --- a/packages/amplify-e2e-tests/src/init/amplifyPush.ts +++ b/packages/amplify-e2e-tests/src/init/amplifyPush.ts @@ -1,10 +1,7 @@ import * as nexpect from 'nexpect'; import { getCLIPath, isCI } from '../utils'; -function amplifyPush( - cwd: string, - verbose: Boolean = isCI() ? false : true -) { +function amplifyPush(cwd: string, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['push'], { cwd, stripColors: true, verbose }) @@ -23,11 +20,7 @@ function amplifyPush( }); } -function amplifyPushUpdate( - cwd: string, - waitForText?: RegExp, - verbose: Boolean = isCI() ? false : true, -) { +function amplifyPushUpdate(cwd: string, waitForText?: RegExp, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['push'], { cwd, stripColors: true, verbose }) @@ -44,10 +37,7 @@ function amplifyPushUpdate( }); } -function amplifyPushAuth( - cwd: string, - verbose: Boolean = isCI() ? false : true -) { +function amplifyPushAuth(cwd: string, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['push'], { cwd, stripColors: true, verbose }) diff --git a/packages/amplify-e2e-tests/src/init/deleteProject.ts b/packages/amplify-e2e-tests/src/init/deleteProject.ts index 21ce220832..aa8b550e36 100644 --- a/packages/amplify-e2e-tests/src/init/deleteProject.ts +++ b/packages/amplify-e2e-tests/src/init/deleteProject.ts @@ -3,11 +3,7 @@ import * as nexpect from 'nexpect'; import { getCLIPath, isCI } from '../utils'; import { getProjectMeta } from '../utils'; -export default function deleteProject( - cwd: string, - deleteDeploymentBucket: Boolean = true, - verbose: Boolean = isCI() ? false : true -) { +export default function deleteProject(cwd: string, deleteDeploymentBucket: Boolean = true, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { const meta = getProjectMeta(cwd).providers.awscloudformation; nexpect @@ -20,9 +16,7 @@ export default function deleteProject( const { DeploymentBucketName } = meta; if (deleteDeploymentBucket) { const s3 = new AWS.S3(); - const { Contents: items } = await s3 - .listObjects({ Bucket: DeploymentBucketName }) - .promise(); + const { Contents: items } = await s3.listObjects({ Bucket: DeploymentBucketName }).promise(); const promises = []; items.forEach(item => { promises.push(s3.deleteObject({ Bucket: DeploymentBucketName, Key: item.Key }).promise()); diff --git a/packages/amplify-e2e-tests/src/init/index.ts b/packages/amplify-e2e-tests/src/init/index.ts index 03f2012fda..0872f4e307 100644 --- a/packages/amplify-e2e-tests/src/init/index.ts +++ b/packages/amplify-e2e-tests/src/init/index.ts @@ -2,7 +2,7 @@ export { default as initProjectWithProfile, initProjectWithAccessKey, initNewEnvWithAccessKey, - initNewEnvWithProfile + initNewEnvWithProfile, } from './initProjectHelper'; export * from './amplifyPush'; export { getProjectMeta } from '../utils'; diff --git a/packages/amplify-e2e-tests/src/init/initProjectHelper.ts b/packages/amplify-e2e-tests/src/init/initProjectHelper.ts index b2e31dbdde..2effbcd52b 100644 --- a/packages/amplify-e2e-tests/src/init/initProjectHelper.ts +++ b/packages/amplify-e2e-tests/src/init/initProjectHelper.ts @@ -13,14 +13,10 @@ const defaultSettings = { buildCmd: '\r', startCmd: '\r', useProfile: '\r', - profileName: '\r' + profileName: '\r', }; -export default function initProjectWithProfile( - cwd: string, - settings: Object, - verbose: Boolean = isCI() ? false : true -) { +export default function initProjectWithProfile(cwd: string, settings: Object, verbose: Boolean = isCI() ? false : true) { const s = { ...defaultSettings, ...settings }; return new Promise((resolve, reject) => { nexpect @@ -48,9 +44,7 @@ export default function initProjectWithProfile( .sendline('y') .wait('Please choose the profile you want to use') .sendline(s.profileName) - .wait( - 'Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything' - ) + .wait('Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything') .run((err: Error) => { if (!err) { resolve(); @@ -97,9 +91,7 @@ export function initProjectWithAccessKey( .sendline(s.secretAccessKey) .wait('region') .sendline('us-east-1') - .wait( - 'Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything' - ) + .wait('Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything') .run((err: Error) => { if (!err) { resolve(); @@ -131,9 +123,7 @@ export function initNewEnvWithAccessKey( .sendline(s.secretAccessKey) .wait('region') .sendline('us-east-1') - .wait( - 'Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything' - ) + .wait('Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything') .run((err: Error) => { if (!err) { resolve(); @@ -144,11 +134,7 @@ export function initNewEnvWithAccessKey( }); } -export function initNewEnvWithProfile( - cwd: string, - s: { envName: string }, - verbose: Boolean = isCI() ? false : true -) { +export function initNewEnvWithProfile(cwd: string, s: { envName: string }, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['init'], { cwd, stripColors: true, verbose }) @@ -161,9 +147,7 @@ export function initNewEnvWithProfile( .sendline('y') .wait('Please choose the profile you want to use') .sendline('\r') - .wait( - 'Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything' - ) + .wait('Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything') .run(function(err: Error) { if (!err) { resolve(); diff --git a/packages/amplify-e2e-tests/src/utils/api.ts b/packages/amplify-e2e-tests/src/utils/api.ts index fbea0fdec6..e7ac7c093b 100644 --- a/packages/amplify-e2e-tests/src/utils/api.ts +++ b/packages/amplify-e2e-tests/src/utils/api.ts @@ -1,11 +1,11 @@ import * as path from 'path'; import * as fs from 'fs'; export function updateSchema(projectDir: string, projectName: string, schemaText: string) { - const schemaPath = path.join(projectDir, 'amplify', 'backend', 'api', projectName, 'schema.graphql'); - fs.writeFileSync(schemaPath, schemaText); + const schemaPath = path.join(projectDir, 'amplify', 'backend', 'api', projectName, 'schema.graphql'); + fs.writeFileSync(schemaPath, schemaText); } export function updateConfig(projectDir: string, projectName: string, config: any = {}) { - const configPath = path.join(projectDir, 'amplify', 'backend', 'api', projectName, 'transform.conf.json'); - fs.writeFileSync(configPath, JSON.stringify(config, null, 4)); -} \ No newline at end of file + const configPath = path.join(projectDir, 'amplify', 'backend', 'api', projectName, 'transform.conf.json'); + fs.writeFileSync(configPath, JSON.stringify(config, null, 4)); +} diff --git a/packages/amplify-e2e-tests/src/utils/awsExports.ts b/packages/amplify-e2e-tests/src/utils/awsExports.ts index 31b0e99571..838447606e 100644 --- a/packages/amplify-e2e-tests/src/utils/awsExports.ts +++ b/packages/amplify-e2e-tests/src/utils/awsExports.ts @@ -3,4 +3,4 @@ import { join } from 'path'; export default function getAWSExports(projectRoot: string) { const metaFilePath = join(projectRoot, 'src', 'aws-exports.js'); return require(metaFilePath); -} \ No newline at end of file +} diff --git a/packages/amplify-e2e-tests/src/utils/index.ts b/packages/amplify-e2e-tests/src/utils/index.ts index a5b53c6911..db26b32440 100644 --- a/packages/amplify-e2e-tests/src/utils/index.ts +++ b/packages/amplify-e2e-tests/src/utils/index.ts @@ -2,9 +2,9 @@ import { join } from 'path'; import { mkdirSync } from 'fs'; import * as rimraf from 'rimraf'; import { config } from 'dotenv'; -export { default as getProjectMeta } from './projectMeta'; -export { default as getAWSExports } from './awsExports'; -export * from './sdk-calls' +export { default as getProjectMeta } from './projectMeta'; +export { default as getAWSExports } from './awsExports'; +export * from './sdk-calls'; export * from './api'; // run dotenv config to update env variable @@ -16,11 +16,7 @@ export function getCLIPath() { export function createNewProjectDir(root?: string): string { if (!root) { - root = join( - __dirname, - '../../../..', - `amplify-integ-${Math.round(Math.random() * 100)}-test-${Math.round(Math.random() * 1000)}` - ); + root = join(__dirname, '../../../..', `amplify-integ-${Math.round(Math.random() * 100)}-test-${Math.round(Math.random() * 1000)}`); } mkdirSync(root); return root; @@ -34,6 +30,6 @@ export function isCI(): Boolean { return process.env.CI ? true : false; } -export function getEnvVars(): { } { +export function getEnvVars(): {} { return { ...process.env }; } diff --git a/packages/amplify-e2e-tests/src/utils/nexpect-modified/index.d.ts b/packages/amplify-e2e-tests/src/utils/nexpect-modified/index.d.ts index 15f0d90153..6daf085a20 100644 --- a/packages/amplify-e2e-tests/src/utils/nexpect-modified/index.d.ts +++ b/packages/amplify-e2e-tests/src/utils/nexpect-modified/index.d.ts @@ -5,8 +5,7 @@ /// - -import child_process = require("child_process"); +import child_process = require('child_process'); declare function spawn(command: string[], options?: ISpawnOptions): IChain; @@ -15,21 +14,21 @@ declare function spawn(command: string, params?: any[], options?: ISpawnOptions) declare function spawn(command: string, options?: ISpawnOptions): IChain; interface IChain { - expect(expectation: string): IChain; - expect(expectation: RegExp): IChain; - wait(expectation: string): IChain; - wait(expectation: RegExp): IChain; - send(line: string): IChain; - sendline(line: string): IChain; - sendEof(): IChain; - run(callback: (err: Error, output: string[], exit: string | number) => void): child_process.ChildProcess; + expect(expectation: string): IChain; + expect(expectation: RegExp): IChain; + wait(expectation: string): IChain; + wait(expectation: RegExp): IChain; + send(line: string): IChain; + sendline(line: string): IChain; + sendEof(): IChain; + run(callback: (err: Error, output: string[], exit: string | number) => void): child_process.ChildProcess; } interface ISpawnOptions { - cwd?: string; - env?: any; - ignoreCase?: any; - stripColors?: any; - stream?: any; - verbose?: any; + cwd?: string; + env?: any; + ignoreCase?: any; + stripColors?: any; + stream?: any; + verbose?: any; } diff --git a/packages/amplify-e2e-tests/src/utils/nexpect-modified/lib/nexpect.js b/packages/amplify-e2e-tests/src/utils/nexpect-modified/lib/nexpect.js index eac438fe9d..40780a685c 100644 --- a/packages/amplify-e2e-tests/src/utils/nexpect-modified/lib/nexpect.js +++ b/packages/amplify-e2e-tests/src/utils/nexpect-modified/lib/nexpect.js @@ -9,10 +9,10 @@ var spawn = require('child_process').spawn; var util = require('util'); var AssertionError = require('assert').AssertionError; -function chain (context) { +function chain(context) { return { - expect: function (expectation) { - var _expect = function _expect (data) { + expect: function(expectation) { + var _expect = function _expect(data) { return testExpectation(data, expectation); }; @@ -24,8 +24,8 @@ function chain (context) { return chain(context); }, - wait: function (expectation, callback) { - var _wait = function _wait (data) { + wait: function(expectation, callback) { + var _wait = function _wait(data) { var val = testExpectation(data, expectation); if (val === true && typeof callback === 'function') { callback(data); @@ -40,8 +40,8 @@ function chain (context) { context.queue.push(_wait); return chain(context); }, - sendline: function (line) { - var _sendline = function _sendline () { + sendline: function(line) { + var _sendline = function _sendline() { context.process.stdin.write(line + '\n'); if (context.verbose) { @@ -55,8 +55,8 @@ function chain (context) { context.queue.push(_sendline); return chain(context); }, - send: function (line) { - var _send = function _send () { + send: function(line) { + var _send = function _send() { context.process.stdin.write(line); if (context.verbose) { @@ -71,7 +71,7 @@ function chain (context) { return chain(context); }, sendEof: function() { - var _sendEof = function _sendEof () { + var _sendEof = function _sendEof() { context.process.stdin.destroy(); }; _sendEof.shift = true; @@ -80,11 +80,11 @@ function chain (context) { context.queue.push(_sendEof); return chain(context); }, - run: function (callback) { + run: function(callback) { var errState = null, - responded = false, - stdout = [], - options; + responded = false, + stdout = [], + options; // // **onError** @@ -92,7 +92,7 @@ function chain (context) { // Helper function to respond to the callback with a // specified error. Kills the child process if necessary. // - function onError (err, kill) { + function onError(err, kill) { if (errState || responded) { return; } @@ -101,8 +101,9 @@ function chain (context) { responded = true; if (kill) { - try { context.process.kill(); } - catch (ex) { } + try { + context.process.kill(); + } catch (ex) {} } callback(err); @@ -114,15 +115,14 @@ function chain (context) { // Helper function to validate the `currentFn` in the // `context.queue` for the target chain. // - function validateFnType (currentFn) { + function validateFnType(currentFn) { if (typeof currentFn !== 'function') { // // If the `currentFn` is not a function, short-circuit with an error. // onError(new Error('Cannot process non-function on nexpect stack.'), true); return false; - } - else if (['_expect', '_sendline', '_send', '_wait', '_sendEof'].indexOf(currentFn.name) === -1) { + } else if (['_expect', '_sendline', '_send', '_wait', '_sendEof'].indexOf(currentFn.name) === -1) { // // If the `currentFn` is a function, but not those set by `.sendline()` or // `.expect()` then short-circuit with an error. @@ -141,7 +141,7 @@ function chain (context) { // `context.queue` against the specified `data` where the last // function run had `name`. // - function evalContext (data, name) { + function evalContext(data, name) { var currentFn = context.queue[0]; if (!currentFn || (name === '_expect' && currentFn.name === '_expect')) { @@ -165,11 +165,10 @@ function chain (context) { // If this is an `_expect` function, then evaluate it and attempt // to evaluate the next function (in case it is a `_sendline` function). // - return currentFn(data) === true ? - evalContext(data, '_expect') : - onError(createExpectationError(currentFn.expectation, data), true); - } - else if (currentFn.name === '_wait') { + return currentFn(data) === true + ? evalContext(data, '_expect') + : onError(createExpectationError(currentFn.expectation, data), true); + } else if (currentFn.name === '_wait') { // // If this is a `_wait` function, then evaluate it and if it returns true, // then evaluate the function (in case it is a `_sendline` function). @@ -178,8 +177,7 @@ function chain (context) { context.queue.shift(); evalContext(data, '_expect'); } - } - else { + } else { // // If the `currentFn` is any other function then evaluate it // @@ -187,8 +185,7 @@ function chain (context) { // Evaluate the next function if it does not need input var nextFn = context.queue[0]; - if (nextFn && !nextFn.requiresInput) - evalContext(data); + if (nextFn && !nextFn.requiresInput) evalContext(data); } } @@ -202,7 +199,7 @@ function chain (context) { // 2. Removing case sensitivity (if necessary) // 3. Splitting `data` into multiple lines. // - function onLine (data) { + function onLine(data) { data = data.toString(); if (context.stripColors) { @@ -213,7 +210,9 @@ function chain (context) { data = data.toLowerCase(); } - var lines = data.split('\n').filter(function (line) { return line.length > 0; }); + var lines = data.split('\n').filter(function(line) { + return line.length > 0; + }); stdout = stdout.concat(lines); while (lines.length > 0) { @@ -227,30 +226,24 @@ function chain (context) { // Helper function which flushes any remaining functions from // `context.queue` and responds to the `callback` accordingly. // - function flushQueue () { + function flushQueue() { var remainingQueue = context.queue.slice(), - currentFn = context.queue.shift(), - lastLine = stdout[stdout.length - 1]; + currentFn = context.queue.shift(), + lastLine = stdout[stdout.length - 1]; if (!lastLine) { - onError(createUnexpectedEndError( - 'No data from child with non-empty queue.', remainingQueue)); + onError(createUnexpectedEndError('No data from child with non-empty queue.', remainingQueue)); return false; - } - else if (context.queue.length > 0) { - onError(createUnexpectedEndError( - 'Non-empty queue on spawn exit.', remainingQueue)); + } else if (context.queue.length > 0) { + onError(createUnexpectedEndError('Non-empty queue on spawn exit.', remainingQueue)); return false; - } - else if (!validateFnType(currentFn)) { + } else if (!validateFnType(currentFn)) { // onError was called return false; - } - else if (currentFn.name === '_sendline') { + } else if (currentFn.name === '_sendline') { onError(new Error('Cannot call sendline after the process has exited')); return false; - } - else if (currentFn.name === '_wait' || currentFn.name === '_expect') { + } else if (currentFn.name === '_wait' || currentFn.name === '_expect') { if (currentFn(lastLine) !== true) { onError(createExpectationError(currentFn.expectation, lastLine)); return false; @@ -266,13 +259,13 @@ function chain (context) { // Helper function for writing any data from a stream // to `process.stdout`. // - function onData (data) { + function onData(data) { process.stdout.write(data); } options = { cwd: context.cwd, - env: context.env + env: context.env, }; // @@ -300,7 +293,7 @@ function chain (context) { // flush `context.queue` (if necessary) and respond to the callback // appropriately. // - context.process.on('close', function (code, signal) { + context.process.on('close', function(code, signal) { if (code === 127) { // XXX(sam) Not how node works (anymore?), 127 is what /bin/sh returns, // but it appears node does not, or not in all conditions, blithely @@ -310,8 +303,7 @@ function chain (context) { // If the response code is `127` then `context.command` was not found. // return onError(new Error('Command not found: ' + context.command)); - } - else if (context.queue.length && !flushQueue()) { + } else if (context.queue.length && !flushQueue()) { // if flushQueue returned false, onError was called return; } @@ -320,7 +312,7 @@ function chain (context) { }); return context.process; - } + }, }; } @@ -336,50 +328,48 @@ function createUnexpectedEndError(message, remainingQueue) { var desc = []; if (isCI() === false) { - var desc = remainingQueue.map(function(it) { return it.description; }); + var desc = remainingQueue.map(function(it) { + return it.description; + }); var msg = message + '\n' + desc.join('\n'); } return new AssertionError({ message: msg, expected: [], - actual: desc + actual: desc, }); } function createExpectationError(expected, actual) { var expectation; - if (util.isRegExp(expected)) - expectation = 'to match ' + expected; - else - expectation = 'to contain ' + JSON.stringify(expected); + if (util.isRegExp(expected)) expectation = 'to match ' + expected; + else expectation = 'to contain ' + JSON.stringify(expected); var err = new AssertionError({ message: util.format('expected %j %s', actual, expectation), actual: actual, - expected: expected + expected: expected, }); return err; } -function nspawn (command, params, options) { +function nspawn(command, params, options) { if (arguments.length === 2) { if (Array.isArray(arguments[1])) { options = {}; - } - else { + } else { options = arguments[1]; params = null; } } if (Array.isArray(command)) { - params = command; + params = command; command = params.shift(); - } - else if (typeof command === 'string') { + } else if (typeof command === 'string') { command = command.split(' '); - params = params || command.slice(1); + params = params || command.slice(1); command = command[0]; } @@ -393,7 +383,7 @@ function nspawn (command, params, options) { queue: [], stream: options.stream || 'stdout', stripColors: options.stripColors, - verbose: options.verbose + verbose: options.verbose, }; return chain(context); @@ -407,7 +397,7 @@ function isCI() { // Export the core `nspawn` function as well as `nexpect.nspawn` for // backwards compatibility. // -module.exports.spawn = nspawn; +module.exports.spawn = nspawn; module.exports.nspawn = { - spawn: nspawn + spawn: nspawn, }; diff --git a/packages/amplify-e2e-tests/src/utils/nexpect-modified/test/nexpect-test.js b/packages/amplify-e2e-tests/src/utils/nexpect-modified/test/nexpect-test.js index 954c3f3953..f4a7f50d3f 100644 --- a/packages/amplify-e2e-tests/src/utils/nexpect-modified/test/nexpect-test.js +++ b/packages/amplify-e2e-tests/src/utils/nexpect-modified/test/nexpect-test.js @@ -6,138 +6,140 @@ */ var assert = require('assert'), - path = require('path'), - vows = require('vows'), - spawn = require('child_process').spawn, - nexpect = require('../lib/nexpect'); + path = require('path'), + vows = require('vows'), + spawn = require('child_process').spawn, + nexpect = require('../lib/nexpect'); -function assertSpawn (expect) { +function assertSpawn(expect) { return { - topic: function () { + topic: function() { expect.run(this.callback); }, - "should respond with no error": function (err, stdout) { + 'should respond with no error': function(err, stdout) { assert.isTrue(!err); assert.isArray(stdout); - } + }, }; } -function assertError (expect) { +function assertError(expect) { return { - topic: function () { + topic: function() { expect.run(this.callback.bind(this, null)); }, - "should respond with error": function (err) { + 'should respond with error': function(err) { assert.isObject(err); - } + }, }; } -vows.describe('nexpect').addBatch({ - "When using the nexpect module": { - "it should have the correct methods defined": function () { - assert.isFunction(nexpect.spawn); - assert.isObject(nexpect.nspawn); - assert.isFunction(nexpect.nspawn.spawn); - }, - "spawning": { - "`echo hello`": assertSpawn( - nexpect.spawn("echo", ["hello"]) - .expect("hello") - ), - "`ls -l /tmp/undefined`": assertSpawn( - nexpect.spawn("ls -la /tmp/undefined", { stream: 'stderr' }) - .expect("No such file or directory") - ), - "a command that does not exist": assertError( - nexpect.spawn("idontexist") - .expect("This will never work") - ), - "and using the sendline() method": assertSpawn( - nexpect.spawn("node --interactive") - .expect(">") - .sendline("console.log('testing')") - .expect("testing") - .sendline("process.exit()") - ), - "and using the expect() method": { - "when RegExp expectation is met": assertSpawn( - nexpect.spawn("echo", ["hello"]) - .expect(/^hello$/) - ), +vows + .describe('nexpect') + .addBatch({ + 'When using the nexpect module': { + 'it should have the correct methods defined': function() { + assert.isFunction(nexpect.spawn); + assert.isObject(nexpect.nspawn); + assert.isFunction(nexpect.nspawn.spawn); }, - "and using the wait() method": { - "when assertions are met": assertSpawn( - nexpect.spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) - .wait('first') - .sendline('first-prompt') - .expect('first-prompt') - .wait('second') - .sendline('second-prompt') - .expect('second-prompt') + spawning: { + '`echo hello`': assertSpawn(nexpect.spawn('echo', ['hello']).expect('hello')), + '`ls -l /tmp/undefined`': assertSpawn( + nexpect.spawn('ls -la /tmp/undefined', { stream: 'stderr' }).expect('No such file or directory') ), - "when the last assertion is never met": assertError( - nexpect.spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) - .wait('first') - .sendline('first-prompt') - .expect('first-prompt') - .wait('second') - .sendline('second-prompt') - .wait('this-never-shows-up') + 'a command that does not exist': assertError(nexpect.spawn('idontexist').expect('This will never work')), + 'and using the sendline() method': assertSpawn( + nexpect + .spawn('node --interactive') + .expect('>') + .sendline("console.log('testing')") + .expect('testing') + .sendline('process.exit()') ), - "when a callback is provided and output is matched": { - topic: function() { - var expect = nexpect.spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) - .wait('first', this.callback) + 'and using the expect() method': { + 'when RegExp expectation is met': assertSpawn(nexpect.spawn('echo', ['hello']).expect(/^hello$/)), + }, + 'and using the wait() method': { + 'when assertions are met': assertSpawn( + nexpect + .spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) + .wait('first') .sendline('first-prompt') .expect('first-prompt') .wait('second') .sendline('second-prompt') - .expect('second-prompt').run(function() {}); - }, - 'should call callback': function(matchData, b) { - assert.ok(matchData.indexOf('first') > 0, "Found 'first' in output") - } - }, - "when a callback is provided and output is not matched": { - topic: function() { - var args = {hasRunCallback: false}, - waitCallback = function() { - args.hasRunCallback = true; - }; - - var expect = nexpect.spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) + .expect('second-prompt') + ), + 'when the last assertion is never met': assertError( + nexpect + .spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) .wait('first') .sendline('first-prompt') .expect('first-prompt') .wait('second') .sendline('second-prompt') - .wait('this-never-shows-up', waitCallback).run(this.callback.bind(this, args)); + .wait('this-never-shows-up') + ), + 'when a callback is provided and output is matched': { + topic: function() { + var expect = nexpect + .spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) + .wait('first', this.callback) + .sendline('first-prompt') + .expect('first-prompt') + .wait('second') + .sendline('second-prompt') + .expect('second-prompt') + .run(function() {}); + }, + 'should call callback': function(matchData, b) { + assert.ok(matchData.indexOf('first') > 0, "Found 'first' in output"); + }, }, - 'should not call callback': function(args, a) { - assert.equal(args.hasRunCallback, false, 'Should not have run callback'); - } - } + 'when a callback is provided and output is not matched': { + topic: function() { + var args = { hasRunCallback: false }, + waitCallback = function() { + args.hasRunCallback = true; + }; + + var expect = nexpect + .spawn(path.join(__dirname, 'fixtures', 'prompt-and-respond')) + .wait('first') + .sendline('first-prompt') + .expect('first-prompt') + .wait('second') + .sendline('second-prompt') + .wait('this-never-shows-up', waitCallback) + .run(this.callback.bind(this, args)); + }, + 'should not call callback': function(args, a) { + assert.equal(args.hasRunCallback, false, 'Should not have run callback'); + }, + }, + }, + 'when options.stripColors is set': assertSpawn( + nexpect + .spawn(path.join(__dirname, 'fixtures', 'log-colors'), { stripColors: true }) + .wait('second has colors') + .expect('third has colors') + ), + 'when options.ignoreCase is set': assertSpawn( + nexpect + .spawn(path.join(__dirname, 'fixtures', 'multiple-cases'), { ignoreCase: true }) + .wait('this has many cases') + .expect('this also has many cases') + ), + 'when options.cwd is set': assertSpawn( + nexpect + .spawn(path.join(__dirname, 'fixtures', 'show-cwd'), { cwd: path.join(__dirname, 'fixtures') }) + .wait(path.join(__dirname, 'fixtures')) + ), + 'when options.env is set': assertSpawn( + nexpect.spawn(path.join(__dirname, 'fixtures', 'show-env'), { env: { foo: 'bar', PATH: process.env.PATH } }).expect('foo=bar') + ), }, - "when options.stripColors is set": assertSpawn( - nexpect.spawn(path.join(__dirname, 'fixtures', 'log-colors'), { stripColors: true }) - .wait('second has colors') - .expect('third has colors') - ), - "when options.ignoreCase is set": assertSpawn( - nexpect.spawn(path.join(__dirname, 'fixtures', 'multiple-cases'), { ignoreCase: true }) - .wait('this has many cases') - .expect('this also has many cases') - ), - "when options.cwd is set": assertSpawn( - nexpect.spawn(path.join(__dirname, 'fixtures', 'show-cwd'), { cwd: path.join(__dirname, 'fixtures') }) - .wait(path.join(__dirname, 'fixtures')) - ), - "when options.env is set": assertSpawn( - nexpect.spawn(path.join(__dirname, 'fixtures', 'show-env'), { env: { foo: 'bar', PATH: process.env.PATH }}) - .expect('foo=bar') - ) - } - } -}).export(module); + }, + }) + .export(module); diff --git a/packages/amplify-e2e-tests/src/utils/projectMeta.ts b/packages/amplify-e2e-tests/src/utils/projectMeta.ts index 52e6faf8dd..98dc06f9ff 100644 --- a/packages/amplify-e2e-tests/src/utils/projectMeta.ts +++ b/packages/amplify-e2e-tests/src/utils/projectMeta.ts @@ -3,4 +3,4 @@ import { readFileSync } from 'fs'; export default function getProjectMeta(projectRoot: string) { const metaFilePath = join(projectRoot, 'amplify', '#current-cloud-backend', 'amplify-meta.json'); return JSON.parse(readFileSync(metaFilePath, 'utf8')); -} \ No newline at end of file +} diff --git a/packages/amplify-e2e-tests/src/utils/sdk-calls.ts b/packages/amplify-e2e-tests/src/utils/sdk-calls.ts index f6bf5303a3..c3dafa4136 100644 --- a/packages/amplify-e2e-tests/src/utils/sdk-calls.ts +++ b/packages/amplify-e2e-tests/src/utils/sdk-calls.ts @@ -1,6 +1,5 @@ import * as AWS from 'aws-sdk'; - const getDDBTable = async (tableName: string, region: string) => { const service = new AWS.DynamoDB({ region }); return await service.describeTable({ TableName: tableName }).promise(); @@ -16,9 +15,7 @@ const getUserPool = async (userpoolId, region) => { const CognitoIdentityServiceProvider = AWS.CognitoIdentityServiceProvider; let res; try { - res = await new CognitoIdentityServiceProvider() - .describeUserPool({ UserPoolId: userpoolId }) - .promise(); + res = await new CognitoIdentityServiceProvider().describeUserPool({ UserPoolId: userpoolId }).promise(); } catch (e) { console.log(e); } @@ -30,9 +27,7 @@ const getLambdaFunction = async (functionName, region) => { const lambda = new AWS.Lambda(); let res; try { - res = await lambda - .getFunction({ FunctionName: functionName }) - .promise(); + res = await lambda.getFunction({ FunctionName: functionName }).promise(); } catch (e) { console.log(e); } @@ -50,7 +45,7 @@ const getUserPoolClients = async (userpoolId, region) => { let clientData = await provider .describeUserPoolClient({ UserPoolId: userpoolId, - ClientId: clients.UserPoolClients[i].ClientId + ClientId: clients.UserPoolClients[i].ClientId, }) .promise(); res.push(clientData); @@ -79,20 +74,28 @@ const getCollection = async (collectionId: string, region: string) => { const getTable = async (tableName: string, region: string) => { const service = new AWS.DynamoDB({ region }); return await service.describeTable({ TableName: tableName }).promise(); -} +}; const deleteTable = async (tableName: string, region: string) => { const service = new AWS.DynamoDB({ region }); return await service.deleteTable({ TableName: tableName }).promise(); -} +}; -const getAppSyncApi = async(appSyncApiId: string, region: string) => { +const getAppSyncApi = async (appSyncApiId: string, region: string) => { const service = new AWS.AppSync({ region }); return await service.getGraphqlApi({ apiId: appSyncApiId }).promise(); -} +}; export { - getDDBTable, checkIfBucketExists, getUserPool, - getUserPoolClients, getBot, getLambdaFunction, - getFunction, getTable, deleteTable, getAppSyncApi, getCollection, + getDDBTable, + checkIfBucketExists, + getUserPool, + getUserPoolClients, + getBot, + getLambdaFunction, + getFunction, + getTable, + deleteTable, + getAppSyncApi, + getCollection, }; diff --git a/packages/amplify-graphiql-explorer/src/serviceWorker.ts b/packages/amplify-graphiql-explorer/src/serviceWorker.ts index 21c461f74b..caf8e18173 100644 --- a/packages/amplify-graphiql-explorer/src/serviceWorker.ts +++ b/packages/amplify-graphiql-explorer/src/serviceWorker.ts @@ -11,13 +11,11 @@ // opt-in, read https://bit.ly/CRA-PWA const isLocalhost = Boolean( - window.location.hostname === "localhost" || + window.location.hostname === 'localhost' || // [::1] is the IPv6 localhost address. - window.location.hostname === "[::1]" || + window.location.hostname === '[::1]' || // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) + window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) ); type Config = { @@ -26,12 +24,9 @@ type Config = { }; export function register(config?: Config) { - if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL( - (process as { env: { [key: string]: string } }).env.PUBLIC_URL, - window.location.href - ); + const publicUrl = new URL((process as { env: { [key: string]: string } }).env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to @@ -39,7 +34,7 @@ export function register(config?: Config) { return; } - window.addEventListener("load", () => { + window.addEventListener('load', () => { const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { @@ -49,10 +44,7 @@ export function register(config?: Config) { // Add some additional logging to localhost, pointing developers to the // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { - console.log( - "This web app is being served cache-first by a service " + - "worker. To learn more, visit https://bit.ly/CRA-PWA" - ); + console.log('This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://bit.ly/CRA-PWA'); }); } else { // Is not localhost. Just register service worker @@ -72,14 +64,13 @@ function registerValidSW(swUrl: string, config?: Config) { return; } installingWorker.onstatechange = () => { - if (installingWorker.state === "installed") { + if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the updated precached content has been fetched, // but the previous service worker will still serve the older // content until all client tabs are closed. console.log( - "New content is available and will be used when all " + - "tabs for this page are closed. See https://bit.ly/CRA-PWA." + 'New content is available and will be used when all ' + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' ); // Execute callback @@ -90,7 +81,7 @@ function registerValidSW(swUrl: string, config?: Config) { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. - console.log("Content is cached for offline use."); + console.log('Content is cached for offline use.'); // Execute callback if (config && config.onSuccess) { @@ -102,7 +93,7 @@ function registerValidSW(swUrl: string, config?: Config) { }; }) .catch(error => { - console.error("Error during service worker registration:", error); + console.error('Error during service worker registration:', error); }); } @@ -111,11 +102,8 @@ function checkValidServiceWorker(swUrl: string, config?: Config) { fetch(swUrl) .then(response => { // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get("content-type"); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf("javascript") === -1) - ) { + const contentType = response.headers.get('content-type'); + if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) { // No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then(registration => { registration.unregister().then(() => { @@ -128,14 +116,12 @@ function checkValidServiceWorker(swUrl: string, config?: Config) { } }) .catch(() => { - console.log( - "No internet connection found. App is running in offline mode." - ); + console.log('No internet connection found. App is running in offline mode.'); }); } export function unregister() { - if ("serviceWorker" in navigator) { + if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { registration.unregister(); }); diff --git a/packages/amplify-graphiql-explorer/src/utils/jwt.ts b/packages/amplify-graphiql-explorer/src/utils/jwt.ts index 8ced95e7a1..eaa51bb0d6 100644 --- a/packages/amplify-graphiql-explorer/src/utils/jwt.ts +++ b/packages/amplify-graphiql-explorer/src/utils/jwt.ts @@ -1,13 +1,13 @@ import { decode, sign, verify } from 'jsonwebtoken'; -export function generateToken(decodedToken: string| object):string { +export function generateToken(decodedToken: string | object): string { try { - if(typeof decodedToken === 'string') { + if (typeof decodedToken === 'string') { decodedToken = JSON.parse(decodedToken); } const token = sign(decodedToken, 'open-secrete'); - verify(token,'open-secrete'); - return token + verify(token, 'open-secrete'); + return token; } catch (e) { const err = new Error('Error when generating OIDC token: ' + e.message); throw err; @@ -16,7 +16,7 @@ export function generateToken(decodedToken: string| object):string { export function parse(token): object { const decodedToken = decode(token); - return decodedToken; + return decodedToken; } /** @@ -27,12 +27,12 @@ export function parse(token): object { */ export function refreshToken(token: string, issuer?: string): string { const tokenObj: any = parse(token); - if(!Object.keys(tokenObj).length){ + if (!Object.keys(tokenObj).length) { throw new Error(`Invalid token ${token}`); } - if(issuer) { + if (issuer) { tokenObj.iss = issuer; } tokenObj.exp = Math.floor(Date.now() / 100 + 20000); return generateToken(JSON.stringify(tokenObj)); -} \ No newline at end of file +} diff --git a/packages/amplify-graphql-docs-generator/__integration__/e2e.test.ts b/packages/amplify-graphql-docs-generator/__integration__/e2e.test.ts index b9b26124fd..8280214ce3 100644 --- a/packages/amplify-graphql-docs-generator/__integration__/e2e.test.ts +++ b/packages/amplify-graphql-docs-generator/__integration__/e2e.test.ts @@ -1,10 +1,10 @@ -import { resolve } from 'path' +import { resolve } from 'path'; import * as fs from 'fs'; -import generate from '../src' +import generate from '../src'; describe('end 2 end tests', () => { const schemaPath = resolve(__dirname, '../fixtures/schema.json'); - const outputpath = resolve(__dirname, './output.graphql') + const outputpath = resolve(__dirname, './output.graphql'); afterEach(() => { // delete the generated file @@ -25,17 +25,17 @@ describe('end 2 end tests', () => { generate(schemaPath, outputpath, { separateFiles: false, maxDepth: 3, language: 'javascript' }); const generatedOutput = fs.readFileSync(outputpath, 'utf8'); expect(generatedOutput).toMatchSnapshot(); - }) + }); it('should generate statements in Typescript', () => { generate(schemaPath, outputpath, { separateFiles: false, maxDepth: 3, language: 'typescript' }); const generatedOutput = fs.readFileSync(outputpath, 'utf8'); expect(generatedOutput).toMatchSnapshot(); - }) + }); it('should generate statements in flow', () => { generate(schemaPath, outputpath, { separateFiles: false, maxDepth: 3, language: 'flow' }); const generatedOutput = fs.readFileSync(outputpath, 'utf8'); expect(generatedOutput).toMatchSnapshot(); - }) -}) + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/generate.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/generate.test.ts index 7be60782f7..12a1059615 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/generate.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/generate.test.ts @@ -1,65 +1,46 @@ -import generate from '../../src/generator/generate' -import { - generateQueries, - generateMutations, - generateSubscriptions, -} from '../../src/generator/generateAllOperations' -import { buildClientSchema } from 'graphql' -import { GQLDocsGenOptions } from '../../src/generator/types' +import generate from '../../src/generator/generate'; +import { generateQueries, generateMutations, generateSubscriptions } from '../../src/generator/generateAllOperations'; +import { buildClientSchema } from 'graphql'; +import { GQLDocsGenOptions } from '../../src/generator/types'; -jest.mock('../../src/generator/generateAllOperations') -jest.mock('graphql') +jest.mock('../../src/generator/generateAllOperations'); +jest.mock('graphql'); describe('generate', () => { - const getQueryType = jest.fn() - const getMutationType = jest.fn() - const getSubscriptionType = jest.fn() + const getQueryType = jest.fn(); + const getMutationType = jest.fn(); + const getSubscriptionType = jest.fn(); const mockSchema = { getQueryType, getMutationType, getSubscriptionType, - } - const maxDepth = 4 - const generateOption: GQLDocsGenOptions = { useExternalFragmentForS3Object: true } + }; + const maxDepth = 4; + const generateOption: GQLDocsGenOptions = { useExternalFragmentForS3Object: true }; beforeEach(() => { - jest.resetAllMocks() - getQueryType.mockReturnValue('QUERY_TYPE') - getMutationType.mockReturnValue('MUTATION_TYPE') - getSubscriptionType.mockReturnValue('SUBSCRIPTION_TYPE') + jest.resetAllMocks(); + getQueryType.mockReturnValue('QUERY_TYPE'); + getMutationType.mockReturnValue('MUTATION_TYPE'); + getSubscriptionType.mockReturnValue('SUBSCRIPTION_TYPE'); - buildClientSchema.mockReturnValue(mockSchema) - generateQueries.mockReturnValue('MOCK_GENERATED_QUERY') - generateMutations.mockReturnValue('MOCK_GENERATED_MUTATION') - generateSubscriptions.mockReturnValue('MOCK_GENERATED_SUBSCRIPTION') - }) + buildClientSchema.mockReturnValue(mockSchema); + generateQueries.mockReturnValue('MOCK_GENERATED_QUERY'); + generateMutations.mockReturnValue('MOCK_GENERATED_MUTATION'); + generateSubscriptions.mockReturnValue('MOCK_GENERATED_SUBSCRIPTION'); + }); it('should generate operations using the helper methods', () => { - generate(mockSchema, maxDepth, generateOption) - expect(generateQueries).toHaveBeenCalledWith( - mockSchema.getQueryType(), - mockSchema, - maxDepth, - generateOption - ) - expect(generateMutations).toHaveBeenCalledWith( - mockSchema.getMutationType(), - mockSchema, - maxDepth, - generateOption - ) - expect(generateSubscriptions).toHaveBeenCalledWith( - mockSchema.getSubscriptionType(), - mockSchema, - maxDepth, - generateOption - ) - }) + generate(mockSchema, maxDepth, generateOption); + expect(generateQueries).toHaveBeenCalledWith(mockSchema.getQueryType(), mockSchema, maxDepth, generateOption); + expect(generateMutations).toHaveBeenCalledWith(mockSchema.getMutationType(), mockSchema, maxDepth, generateOption); + expect(generateSubscriptions).toHaveBeenCalledWith(mockSchema.getSubscriptionType(), mockSchema, maxDepth, generateOption); + }); it('should call the individual operation generator and return the value from them', () => { expect(generate(mockSchema, maxDepth, generateOption)).toEqual({ queries: 'MOCK_GENERATED_QUERY', subscriptions: 'MOCK_GENERATED_SUBSCRIPTION', mutations: 'MOCK_GENERATED_MUTATION', - }) - }) -}) + }); + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/generateAllOperations.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/generateAllOperations.test.ts index fa06080937..b1ca54387d 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/generateAllOperations.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/generateAllOperations.test.ts @@ -1,36 +1,32 @@ -import { - generateQueries, - generateMutations, - generateSubscriptions, -} from '../../src/generator/generateAllOperations' +import { generateQueries, generateMutations, generateSubscriptions } from '../../src/generator/generateAllOperations'; -import generateOperation from '../../src/generator/generateOperation' -import { GQLDocsGenOptions } from '../../src/generator/types' +import generateOperation from '../../src/generator/generateOperation'; +import { GQLDocsGenOptions } from '../../src/generator/types'; -jest.mock('../../src/generator/generateOperation') +jest.mock('../../src/generator/generateOperation'); const mockOperationResult = { args: ['MOCK_ARG'], body: 'MOCK_BODY', -} -generateOperation.mockReturnValue(mockOperationResult) +}; +generateOperation.mockReturnValue(mockOperationResult); const mockFields = { f1: 'f1', -} -const getFields = jest.fn() -getFields.mockReturnValue(mockFields) +}; +const getFields = jest.fn(); +getFields.mockReturnValue(mockFields); const operations = { getFields, -} -const maxDepth = 10 +}; +const maxDepth = 10; -const mockSchema = 'MOCK_SCHEMA' -const generateOptions: GQLDocsGenOptions = { useExternalFragmentForS3Object: true } +const mockSchema = 'MOCK_SCHEMA'; +const generateOptions: GQLDocsGenOptions = { useExternalFragmentForS3Object: true }; describe('generateAllOperations', () => { beforeEach(() => { - jest.clearAllMocks() - }) + jest.clearAllMocks(); + }); it('generateQueries - should call generateOperation', () => { expect(generateQueries(operations, mockSchema, maxDepth, generateOptions)).toEqual([ @@ -39,17 +35,12 @@ describe('generateAllOperations', () => { name: 'F1', ...mockOperationResult, }, - ]) - expect(generateOperation).toHaveBeenCalledWith( - mockFields.f1, - mockSchema, - maxDepth, - generateOptions - ) - expect(getFields).toHaveBeenCalled() - expect(generateOperation).toHaveBeenCalledTimes(1) - expect(getFields).toHaveBeenCalledTimes(1) - }) + ]); + expect(generateOperation).toHaveBeenCalledWith(mockFields.f1, mockSchema, maxDepth, generateOptions); + expect(getFields).toHaveBeenCalled(); + expect(generateOperation).toHaveBeenCalledTimes(1); + expect(getFields).toHaveBeenCalledTimes(1); + }); it('generateMutation - should call generateOperation', () => { expect(generateMutations(operations, mockSchema, maxDepth, generateOptions)).toEqual([ @@ -58,17 +49,12 @@ describe('generateAllOperations', () => { name: 'F1', ...mockOperationResult, }, - ]) - expect(generateOperation).toHaveBeenCalledWith( - mockFields.f1, - mockSchema, - maxDepth, - generateOptions - ) - expect(getFields).toHaveBeenCalled() - expect(generateOperation).toHaveBeenCalledTimes(1) - expect(getFields).toHaveBeenCalledTimes(1) - }) + ]); + expect(generateOperation).toHaveBeenCalledWith(mockFields.f1, mockSchema, maxDepth, generateOptions); + expect(getFields).toHaveBeenCalled(); + expect(generateOperation).toHaveBeenCalledTimes(1); + expect(getFields).toHaveBeenCalledTimes(1); + }); it('generateSubscription - should call generateOperation', () => { expect(generateSubscriptions(operations, mockSchema, maxDepth, generateOptions)).toEqual([ @@ -77,14 +63,9 @@ describe('generateAllOperations', () => { name: 'F1', ...mockOperationResult, }, - ]) - expect(generateOperation).toHaveBeenCalledTimes(1) - expect(getFields).toHaveBeenCalledTimes(1) - expect(generateOperation).toHaveBeenCalledWith( - mockFields.f1, - mockSchema, - maxDepth, - generateOptions - ) - }) -}) + ]); + expect(generateOperation).toHaveBeenCalledTimes(1); + expect(getFields).toHaveBeenCalledTimes(1); + expect(generateOperation).toHaveBeenCalledWith(mockFields.f1, mockSchema, maxDepth, generateOptions); + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/generateOperation.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/generateOperation.test.ts index eff7695149..a13b98f0a1 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/generateOperation.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/generateOperation.test.ts @@ -1,31 +1,31 @@ -import generateOperation from '../../src/generator/generateOperation' -import getArgs from '../../src/generator/getArgs' -import getBody from '../../src/generator/getBody' +import generateOperation from '../../src/generator/generateOperation'; +import getArgs from '../../src/generator/getArgs'; +import getBody from '../../src/generator/getBody'; import { GQLDocsGenOptions } from '../../src/generator/types'; -jest.mock('../../src/generator/getArgs') -jest.mock('../../src/generator/getBody') +jest.mock('../../src/generator/getArgs'); +jest.mock('../../src/generator/getBody'); -const maxDepth = 4 -const generateOption: GQLDocsGenOptions = { useExternalFragmentForS3Object: true } +const maxDepth = 4; +const generateOption: GQLDocsGenOptions = { useExternalFragmentForS3Object: true }; describe('generateOperation', () => { beforeEach(() => { - jest.resetAllMocks() - getArgs.mockReturnValue(['MOCK_ARG']) - getBody.mockReturnValue('MOCK_BODY') - }) + jest.resetAllMocks(); + getArgs.mockReturnValue(['MOCK_ARG']); + getBody.mockReturnValue('MOCK_BODY'); + }); it('should generate operation', () => { const op = { args: ['arg1'], - } - const doc = 'MOCK_DOCUMENT' + }; + const doc = 'MOCK_DOCUMENT'; expect(generateOperation(op, 'MOCK_DOCUMENT', maxDepth, generateOption)).toEqual({ args: ['MOCK_ARG'], body: 'MOCK_BODY', - }) + }); - expect(getArgs).toHaveBeenCalledWith(op.args) - expect(getBody).toBeCalledWith(op, doc, maxDepth, generateOption) - }) -}) + expect(getArgs).toHaveBeenCalledWith(op.args); + expect(getBody).toBeCalledWith(op, doc, maxDepth, generateOption); + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/getArgs.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/getArgs.test.ts index ee52d2afad..3fcc9aeedb 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/getArgs.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/getArgs.test.ts @@ -1,21 +1,15 @@ -import { - GraphQLSchema, - GraphQLObjectType, - GraphQLArgument, - GraphQLString, - GraphQLID, -} from 'graphql' +import { GraphQLSchema, GraphQLObjectType, GraphQLArgument, GraphQLString, GraphQLID } from 'graphql'; -import isRequired from '../../src/generator/utils/isRequired' -import getType from '../../src/generator/utils/getType' +import isRequired from '../../src/generator/utils/isRequired'; +import getType from '../../src/generator/utils/getType'; -import getArgs from '../../src/generator/getArgs' +import getArgs from '../../src/generator/getArgs'; import isList from '../../src/generator/utils/isList'; import isRequiredList from '../../src/generator/utils/isRequiredList'; -jest.mock('../../src/generator/utils/isRequired') -jest.mock('../../src/generator/utils/getType') -jest.mock('../../src/generator/utils/isList') +jest.mock('../../src/generator/utils/isRequired'); +jest.mock('../../src/generator/utils/getType'); +jest.mock('../../src/generator/utils/isList'); jest.mock('../../src/generator/utils/isRequiredList'); describe('getArgs', () => { @@ -23,12 +17,12 @@ describe('getArgs', () => { name: 'id', type: GraphQLID, defaultValue: '1', - } + }; const query: GraphQLArgument = { name: 'query', type: GraphQLString, - } + }; const blogArticle = new GraphQLObjectType({ name: 'BlogArticle', @@ -36,7 +30,7 @@ describe('getArgs', () => { id: { type: GraphQLID }, content: { type: GraphQLString }, }, - }) + }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -48,19 +42,19 @@ describe('getArgs', () => { }, }, }), - }) + }); beforeEach(() => { - jest.resetAllMocks() - isRequired.mockReturnValue(false) - getType.mockReturnValue({ name: 'mockType' }) + jest.resetAllMocks(); + isRequired.mockReturnValue(false); + getType.mockReturnValue({ name: 'mockType' }); isRequiredList.mockReturnValue(false); isList.mockReturnValue(false); - }) + }); it('should return arguments', () => { isList.mockReturnValueOnce(true); - const query = schema.getQueryType().getFields().searchArticle + const query = schema.getQueryType().getFields().searchArticle; expect(getArgs(query.args)).toEqual([ { name: 'id', @@ -78,16 +72,16 @@ describe('getArgs', () => { isList: false, isListRequired: false, }, - ]) - expect(getType).toHaveBeenCalledTimes(2) - expect(getType.mock.calls[0][0]).toEqual(GraphQLID) - expect(getType.mock.calls[1][0]).toEqual(GraphQLString) + ]); + expect(getType).toHaveBeenCalledTimes(2); + expect(getType.mock.calls[0][0]).toEqual(GraphQLID); + expect(getType.mock.calls[1][0]).toEqual(GraphQLString); - expect(isRequired).toHaveBeenCalledTimes(2) - expect(isRequired.mock.calls[0][0]).toEqual(query.args[0].type) - expect(isRequired.mock.calls[1][0]).toEqual(query.args[1].type) + expect(isRequired).toHaveBeenCalledTimes(2); + expect(isRequired.mock.calls[0][0]).toEqual(query.args[0].type); + expect(isRequired.mock.calls[1][0]).toEqual(query.args[1].type); expect(isList).toHaveBeenCalledTimes(2); expect(isRequiredList).toHaveBeenCalledTimes(2); - }) -}) + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/getBody.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/getBody.test.ts index 72f6dfd6c6..652e41338c 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/getBody.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/getBody.test.ts @@ -1,35 +1,24 @@ -import { - GraphQLField, - GraphQLSchema, - GraphQLObjectType, - GraphQLArgument, - GraphQLString, - GraphQLID, -} from 'graphql' +import { GraphQLField, GraphQLSchema, GraphQLObjectType, GraphQLArgument, GraphQLString, GraphQLID } from 'graphql'; -import getFields from '../../src/generator/getFields' -import { - GQLTemplateOpBody, - GQLTemplateArgInvocation, - GQLTemplateField, -} from '../../src/generator/types' -import getBody from '../../src/generator/getBody' +import getFields from '../../src/generator/getFields'; +import { GQLTemplateOpBody, GQLTemplateArgInvocation, GQLTemplateField } from '../../src/generator/types'; +import getBody from '../../src/generator/getBody'; -jest.mock('../../src/generator/getFields') -const maxDepth = 2 +jest.mock('../../src/generator/getFields'); +const maxDepth = 2; describe('getBody', () => { const arg: GraphQLArgument = { name: 'id', type: GraphQLID, - } + }; const blogArticle = new GraphQLObjectType({ name: 'BlogArticle', fields: { id: { type: GraphQLID }, content: { type: GraphQLString }, }, - }) + }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -41,22 +30,22 @@ describe('getBody', () => { }, }, }), - }) + }); const mockFields = { filed1: 'field1', field2: 'field2', - } + }; beforeEach(() => { - getFields.mockReturnValue(mockFields) - }) + getFields.mockReturnValue(mockFields); + }); it('should return a list of arguments', () => { - const query = schema.getQueryType().getFields().article - expect(getBody(query, schema, maxDepth, { useExternalFragmentForS3Object: true})).toEqual({ + const query = schema.getQueryType().getFields().article; + expect(getBody(query, schema, maxDepth, { useExternalFragmentForS3Object: true })).toEqual({ args: [{ name: 'id', value: '$id' }], ...mockFields, - }) - expect(getFields).toHaveBeenCalledWith(query, schema, maxDepth, { useExternalFragmentForS3Object: true}) - }) -}) + }); + expect(getFields).toHaveBeenCalledWith(query, schema, maxDepth, { useExternalFragmentForS3Object: true }); + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/getFields.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/getFields.test.ts index 2502466a88..36212f0a86 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/getFields.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/getFields.test.ts @@ -1,17 +1,10 @@ -import { - GraphQLSchema, - GraphQLObjectType, - GraphQLString, - GraphQLInt, - GraphQLInterfaceType, - GraphQLUnionType, -} from 'graphql' - -import getFields from '../../src/generator/getFields' -import getFragment from '../../src/generator/getFragment' -import getType from '../../src/generator/utils/getType' - -jest.mock('../../src/generator/getFragment') +import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLInterfaceType, GraphQLUnionType } from 'graphql'; + +import getFields from '../../src/generator/getFields'; +import getFragment from '../../src/generator/getFragment'; +import getType from '../../src/generator/utils/getType'; + +jest.mock('../../src/generator/getFragment'); describe('getField', () => { const nestedType = new GraphQLObjectType({ name: 'NestedObject', @@ -19,7 +12,7 @@ describe('getField', () => { level: { type: GraphQLInt }, subObj: { type: nestedType }, }), - }) + }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -29,22 +22,22 @@ describe('getField', () => { nested: { type: nestedType }, }, }), - }) + }); it('should support simple scalar', () => { - const queries = schema.getQueryType().getFields() - expect(getFields(queries.foo, schema, 3, {useExternalFragmentForS3Object: false})).toEqual({ + const queries = schema.getQueryType().getFields(); + expect(getFields(queries.foo, schema, 3, { useExternalFragmentForS3Object: false })).toEqual({ name: 'foo', fields: [], fragments: [], hasBody: false, - }) - expect(getFragment).not.toHaveBeenCalled() - }) + }); + expect(getFragment).not.toHaveBeenCalled(); + }); it('it should recursively resolve fields up to max depth', () => { - const queries = schema.getQueryType().getFields() - expect(getFields(queries.nested, schema, 2, { useExternalFragmentForS3Object: false})).toEqual({ + const queries = schema.getQueryType().getFields(); + expect(getFields(queries.nested, schema, 2, { useExternalFragmentForS3Object: false })).toEqual({ name: 'nested', fields: [ { @@ -69,23 +62,23 @@ describe('getField', () => { ], fragments: [], hasBody: true, - }) - }) + }); + }); it('should not return anything for complex type when the depth is < 1', () => { - const queries = schema.getQueryType().getFields() - expect(getFields(queries.nested, schema, 0, { useExternalFragmentForS3Object: false})).toBeUndefined() - }) + const queries = schema.getQueryType().getFields(); + expect(getFields(queries.nested, schema, 0, { useExternalFragmentForS3Object: false })).toBeUndefined(); + }); describe('When type is an Interface', () => { beforeEach(() => { - jest.resetAllMocks() - }) + jest.resetAllMocks(); + }); const shapeInterfaceType = new GraphQLInterfaceType({ name: 'Entity', fields: { name: { type: GraphQLString }, }, - }) + }); const rectangleType = new GraphQLObjectType({ name: 'Rectangle', fields: { @@ -94,7 +87,7 @@ describe('getField', () => { width: { type: GraphQLInt }, }, interfaces: () => [shapeInterfaceType], - }) + }); const circleType = new GraphQLObjectType({ name: 'Circle', @@ -103,7 +96,7 @@ describe('getField', () => { radius: { type: GraphQLInt }, }, interfaces: () => [shapeInterfaceType], - }) + }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -113,55 +106,55 @@ describe('getField', () => { }, }), types: [circleType, rectangleType], - }) + }); it('interface - should return fragments of all the implementations', () => { - const maxDepth = 2 - const getPossibleTypeSpy = jest.spyOn(schema, 'getPossibleTypes') - getFields(schema.getQueryType().getFields().shapeInterface, schema, maxDepth, { useExternalFragmentForS3Object: false}) - expect(getPossibleTypeSpy).toHaveBeenCalled() - expect(getFragment).toHaveBeenCalled() + const maxDepth = 2; + const getPossibleTypeSpy = jest.spyOn(schema, 'getPossibleTypes'); + getFields(schema.getQueryType().getFields().shapeInterface, schema, maxDepth, { useExternalFragmentForS3Object: false }); + expect(getPossibleTypeSpy).toHaveBeenCalled(); + expect(getFragment).toHaveBeenCalled(); const commonField = { name: 'name', fragments: [], hasBody: false, fields: [], - } - - expect(getFragment.mock.calls[0][0]).toEqual(circleType) - expect(getFragment.mock.calls[0][1]).toEqual(schema) - expect(getFragment.mock.calls[0][2]).toEqual(maxDepth) - expect(getFragment.mock.calls[0][3]).toEqual([commonField]) - - expect(getFragment.mock.calls[1][0]).toEqual(rectangleType) - expect(getFragment.mock.calls[1][1]).toEqual(schema) - expect(getFragment.mock.calls[1][2]).toEqual(maxDepth) - expect(getFragment.mock.calls[1][3]).toEqual([commonField]) - }) - }) + }; + + expect(getFragment.mock.calls[0][0]).toEqual(circleType); + expect(getFragment.mock.calls[0][1]).toEqual(schema); + expect(getFragment.mock.calls[0][2]).toEqual(maxDepth); + expect(getFragment.mock.calls[0][3]).toEqual([commonField]); + + expect(getFragment.mock.calls[1][0]).toEqual(rectangleType); + expect(getFragment.mock.calls[1][1]).toEqual(schema); + expect(getFragment.mock.calls[1][2]).toEqual(maxDepth); + expect(getFragment.mock.calls[1][3]).toEqual([commonField]); + }); + }); describe('When type is an union', () => { beforeEach(() => { - jest.resetAllMocks() - }) + jest.resetAllMocks(); + }); const rectangleType = new GraphQLObjectType({ name: 'Rectangle', fields: { length: { type: GraphQLInt }, width: { type: GraphQLInt }, }, - }) + }); const circleType = new GraphQLObjectType({ name: 'Circle', fields: { radius: { type: GraphQLInt }, }, - }) + }); const shapeResultUnion = new GraphQLUnionType({ name: 'ShapeResultUnion', types: [circleType, rectangleType], - }) + }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -170,26 +163,26 @@ describe('getField', () => { shapeResult: { type: shapeResultUnion }, }, }), - }) + }); it('union - should return fragments of all the types', () => { - const maxDepth = 2 - const getPossibleTypeSpy = jest.spyOn(schema, 'getPossibleTypes') - getFields(schema.getQueryType().getFields().shapeResult, schema, maxDepth, { useExternalFragmentForS3Object: false}) - expect(getPossibleTypeSpy).toHaveBeenCalled() - expect(getFragment).toHaveBeenCalled() - - const commonField = [] // unions don't have to have common field - - expect(getFragment.mock.calls[0][0]).toEqual(circleType) - expect(getFragment.mock.calls[0][1]).toEqual(schema) - expect(getFragment.mock.calls[0][2]).toEqual(maxDepth) - expect(getFragment.mock.calls[0][3]).toEqual(commonField) - - expect(getFragment.mock.calls[1][0]).toEqual(rectangleType) - expect(getFragment.mock.calls[1][1]).toEqual(schema) - expect(getFragment.mock.calls[1][2]).toEqual(maxDepth) - expect(getFragment.mock.calls[1][3]).toEqual(commonField) - }) - }) -}) + const maxDepth = 2; + const getPossibleTypeSpy = jest.spyOn(schema, 'getPossibleTypes'); + getFields(schema.getQueryType().getFields().shapeResult, schema, maxDepth, { useExternalFragmentForS3Object: false }); + expect(getPossibleTypeSpy).toHaveBeenCalled(); + expect(getFragment).toHaveBeenCalled(); + + const commonField = []; // unions don't have to have common field + + expect(getFragment.mock.calls[0][0]).toEqual(circleType); + expect(getFragment.mock.calls[0][1]).toEqual(schema); + expect(getFragment.mock.calls[0][2]).toEqual(maxDepth); + expect(getFragment.mock.calls[0][3]).toEqual(commonField); + + expect(getFragment.mock.calls[1][0]).toEqual(rectangleType); + expect(getFragment.mock.calls[1][1]).toEqual(schema); + expect(getFragment.mock.calls[1][2]).toEqual(maxDepth); + expect(getFragment.mock.calls[1][3]).toEqual(commonField); + }); + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/getFragments.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/getFragments.test.ts index 683f0dfa21..0004387048 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/getFragments.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/getFragments.test.ts @@ -1,16 +1,10 @@ -import { - GraphQLObjectType, - GraphQLSchema, - GraphQLInterfaceType, - GraphQLString, - GraphQLInt, -} from 'graphql' +import { GraphQLObjectType, GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLInt } from 'graphql'; -import { GQLTemplateField, GQLTemplateFragment, GQLDocsGenOptions } from '../../src/generator/types' -import getFields from '../../src/generator/getFields' -import getFragment from '../../src/generator/getFragment' +import { GQLTemplateField, GQLTemplateFragment, GQLDocsGenOptions } from '../../src/generator/types'; +import getFields from '../../src/generator/getFields'; +import getFragment from '../../src/generator/getFragment'; -jest.mock('../../src/generator/getFields') +jest.mock('../../src/generator/getFields'); describe('getFragments', () => { const shapeInterfaceType = new GraphQLInterfaceType({ @@ -18,7 +12,7 @@ describe('getFragments', () => { fields: { name: { type: GraphQLString }, }, - }) + }); const rectangleType = new GraphQLObjectType({ name: 'Rectangle', fields: { @@ -27,7 +21,7 @@ describe('getFragments', () => { width: { type: GraphQLInt }, }, interfaces: () => [shapeInterfaceType], - }) + }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -38,35 +32,35 @@ describe('getFragments', () => { }, }), types: [rectangleType], - }) + }); beforeEach(() => { - jest.resetAllMocks() - getFields.mockImplementation((field) => ({ name: field.name })) - }) + jest.resetAllMocks(); + getFields.mockImplementation(field => ({ name: field.name })); + }); it('should call getField on each field of interface implimentation', () => { - const impl = schema.getType('Rectangle') - const currentDepth = 3 + const impl = schema.getType('Rectangle'); + const currentDepth = 3; expect(getFragment(impl, schema, currentDepth, [])).toEqual({ fields: [{ name: 'name' }, { name: 'length' }, { name: 'width' }], on: 'Rectangle', external: false, name: 'RectangleFragment', - }) - expect(getFields).toHaveBeenCalledTimes(3) - }) + }); + expect(getFields).toHaveBeenCalledTimes(3); + }); it('should decrease the current depth when calling sub fieds', () => { - const impl = schema.getType('Rectangle') - const currentDepth = 3 - getFragment(impl, schema, currentDepth, []) - expect(getFields.mock.calls[0][2]).toEqual(currentDepth - 1) - }) + const impl = schema.getType('Rectangle'); + const currentDepth = 3; + getFragment(impl, schema, currentDepth, []); + expect(getFields.mock.calls[0][2]).toEqual(currentDepth - 1); + }); it('should filter out the fields that listed in filterFields', () => { - const impl = schema.getType('Rectangle') - const currentDepth = 3 + const impl = schema.getType('Rectangle'); + const currentDepth = 3; const fieldsToFilter = [ { name: 'length', @@ -74,41 +68,41 @@ describe('getFragments', () => { fields: [], fragments: [], }, - ] + ]; expect(getFragment(impl, schema, currentDepth, fieldsToFilter)).toEqual({ fields: [{ name: 'name' }, { name: 'width' }], on: 'Rectangle', name: 'RectangleFragment', external: false, - }) - }) + }); + }); it('should not render anything if the field is scalar', () => { - const impl = schema.getQueryType().getFields().simpleScalar - const currentDepth = 3 - expect(getFragment(impl, schema, currentDepth)).toBeUndefined() - }) + const impl = schema.getQueryType().getFields().simpleScalar; + const currentDepth = 3; + expect(getFragment(impl, schema, currentDepth)).toBeUndefined(); + }); it('should use the name passed as fragment name', () => { - const impl = schema.getType('Rectangle') - const currentDepth = 3 - const fragment = getFragment(impl, schema, currentDepth, [], 'FooFragment') - expect(fragment.name).toEqual('FooFragment') - }) + const impl = schema.getType('Rectangle'); + const currentDepth = 3; + const fragment = getFragment(impl, schema, currentDepth, [], 'FooFragment'); + expect(fragment.name).toEqual('FooFragment'); + }); it('should use the mark fragment as external when passed', () => { - const impl = schema.getType('Rectangle') - const currentDepth = 3 - const fragment = getFragment(impl, schema, currentDepth, [], 'FooFragment', true) - expect(fragment.external).toEqual(true) - }) + const impl = schema.getType('Rectangle'); + const currentDepth = 3; + const fragment = getFragment(impl, schema, currentDepth, [], 'FooFragment', true); + expect(fragment.external).toEqual(true); + }); it('should pass the options to getFields call', () => { - const options: GQLDocsGenOptions = { useExternalFragmentForS3Object: true } - const impl = schema.getType('Rectangle') - const currentDepth = 3 - const fragment = getFragment(impl, schema, currentDepth, [], 'FooFragment', false, options) - expect(getFields).toHaveBeenCalledTimes(3) - expect(getFields.mock.calls[0][3]).toEqual(options) - }) -}) + const options: GQLDocsGenOptions = { useExternalFragmentForS3Object: true }; + const impl = schema.getType('Rectangle'); + const currentDepth = 3; + const fragment = getFragment(impl, schema, currentDepth, [], 'FooFragment', false, options); + expect(getFields).toHaveBeenCalledTimes(3); + expect(getFields.mock.calls[0][3]).toEqual(options); + }); +}); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/utils/getType.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/utils/getType.test.ts index da79c98d6e..2dc6348ba7 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/utils/getType.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/utils/getType.test.ts @@ -1,42 +1,34 @@ -import getType from "../../../src/generator/utils/getType"; -import { - GraphQLScalarType, - Kind, - GraphQLInt, - GraphQLObjectType, - GraphQLString, - GraphQLNonNull, - GraphQLList -} from "graphql"; -describe("getType", () => { +import getType from '../../../src/generator/utils/getType'; +import { GraphQLScalarType, Kind, GraphQLInt, GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLList } from 'graphql'; +describe('getType', () => { const testObj = new GraphQLObjectType({ - name: "Address", + name: 'Address', fields: { street: { type: GraphQLString }, number: { type: GraphQLInt }, requiredInt: { type: new GraphQLNonNull(GraphQLInt) }, listOfInt: { type: new GraphQLList(GraphQLInt) }, - listOfNonNullInt: { type: new GraphQLNonNull(new GraphQLList(GraphQLInt)) } - } + listOfNonNullInt: { type: new GraphQLNonNull(new GraphQLList(GraphQLInt)) }, + }, }); - it("should return string type for street", () => { + it('should return string type for street', () => { expect(getType(testObj.getFields().street.type)).toEqual(GraphQLString); }); - it("should return integer type for number", () => { + it('should return integer type for number', () => { expect(getType(testObj.getFields().number.type)).toEqual(GraphQLInt); }); - it("should return integer type for a Non-Null integer", () => { + it('should return integer type for a Non-Null integer', () => { expect(getType(testObj.getFields().requiredInt.type)).toEqual(GraphQLInt); }); - it("should return integer type for list of integer type", () => { + it('should return integer type for list of integer type', () => { expect(getType(testObj.getFields().listOfInt.type)).toEqual(GraphQLInt); }); - it("should return integer type for a list of non null integer type", () => { + it('should return integer type for a list of non null integer type', () => { expect(getType(testObj.getFields().listOfNonNullInt.type)).toEqual(GraphQLInt); }); }); diff --git a/packages/amplify-graphql-docs-generator/__tests__/generator/utils/isRequired.test.ts b/packages/amplify-graphql-docs-generator/__tests__/generator/utils/isRequired.test.ts index 1c45366562..64537366e8 100644 --- a/packages/amplify-graphql-docs-generator/__tests__/generator/utils/isRequired.test.ts +++ b/packages/amplify-graphql-docs-generator/__tests__/generator/utils/isRequired.test.ts @@ -1,4 +1,4 @@ -import isRequired from "../../../src/generator/utils/isRequired"; +import isRequired from '../../../src/generator/utils/isRequired'; import { GraphQLScalarType, Kind, @@ -7,22 +7,22 @@ import { GraphQLString, GraphQLNonNull, GraphQLList, - GraphQLInputObjectType -} from "graphql"; -describe("isRequired", () => { + GraphQLInputObjectType, +} from 'graphql'; +describe('isRequired', () => { const testObj = new GraphQLInputObjectType({ - name: "Address", + name: 'Address', fields: { street: { type: GraphQLString }, - requiredInt: { type: new GraphQLNonNull(GraphQLInt) } - } + requiredInt: { type: new GraphQLNonNull(GraphQLInt) }, + }, }); - it("should return false for null types", () => { + it('should return false for null types', () => { expect(isRequired(testObj.getFields().street.type)).toEqual(false); }); - it("should return true for non null types", () => { + it('should return true for non null types', () => { expect(isRequired(testObj.getFields().requiredInt.type)).toEqual(true); }); }); diff --git a/packages/amplify-graphql-docs-generator/src/cli.ts b/packages/amplify-graphql-docs-generator/src/cli.ts index 3b888e9b74..0ccc7fef64 100644 --- a/packages/amplify-graphql-docs-generator/src/cli.ts +++ b/packages/amplify-graphql-docs-generator/src/cli.ts @@ -1,23 +1,23 @@ -import * as yargs from 'yargs' -import * as path from 'path' +import * as yargs from 'yargs'; +import * as path from 'path'; -import { logError } from './logger' -import generateAllOps from './index' +import { logError } from './logger'; +import generateAllOps from './index'; // / Make sure unhandled errors in async code are propagated correctly process.on('unhandledRejection', error => { - throw error -}) + throw error; +}); -process.on('uncaughtException', handleError) +process.on('uncaughtException', handleError); function handleError(error: Error) { - logError(error) - process.exit(1) + logError(error); + process.exit(1); } export function run(argv: Array): void { - // tslint:disable + // tslint:disable yargs .command( '$0', @@ -40,25 +40,24 @@ export function run(argv: Array): void { demand: true, default: 'graphql', normalize: true, - choices: ['graphql', 'javascript', 'flow', 'typescript'] + choices: ['graphql', 'javascript', 'flow', 'typescript'], }, maxDepth: { demand: true, default: 2, normalize: true, - type: 'number' + type: 'number', }, separateFiles: { default: false, - type: 'boolean' - } + type: 'boolean', + }, }, async argv => { - generateAllOps(argv.schema, argv.output, { separateFiles: argv.separateFiles, language: argv.language, maxDepth: argv.maxDepth }) + generateAllOps(argv.schema, argv.output, { separateFiles: argv.separateFiles, language: argv.language, maxDepth: argv.maxDepth }); } ) .help() .version() - .strict() - .argv + .strict().argv; } diff --git a/packages/amplify-graphql-docs-generator/src/generator/generate.ts b/packages/amplify-graphql-docs-generator/src/generator/generate.ts index 7e9bae0e80..028bdd7c8b 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/generate.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/generate.ts @@ -1,22 +1,18 @@ -import { buildClientSchema, GraphQLObjectType, GraphQLSchema, IntrospectionQuery } from 'graphql' +import { buildClientSchema, GraphQLObjectType, GraphQLSchema, IntrospectionQuery } from 'graphql'; -import { generateQueries, generateMutations, generateSubscriptions, collectExternalFragments } from './generateAllOperations' -import { GQLDocsGenOptions, GQLAllOperations} from './types' -export default function generate( - schemaDoc: GraphQLSchema, - maxDepth: number, - options: GQLDocsGenOptions -): GQLAllOperations { +import { generateQueries, generateMutations, generateSubscriptions, collectExternalFragments } from './generateAllOperations'; +import { GQLDocsGenOptions, GQLAllOperations } from './types'; +export default function generate(schemaDoc: GraphQLSchema, maxDepth: number, options: GQLDocsGenOptions): GQLAllOperations { try { - const queryTypes: GraphQLObjectType = schemaDoc.getQueryType() - const mutationType: GraphQLObjectType = schemaDoc.getMutationType() - const subscriptionType: GraphQLObjectType = schemaDoc.getSubscriptionType() - const queries = generateQueries(queryTypes, schemaDoc, maxDepth, options) || [] - const mutations = generateMutations(mutationType, schemaDoc, maxDepth, options) || [] - const subscriptions = generateSubscriptions(subscriptionType, schemaDoc, maxDepth, options) || [] + const queryTypes: GraphQLObjectType = schemaDoc.getQueryType(); + const mutationType: GraphQLObjectType = schemaDoc.getMutationType(); + const subscriptionType: GraphQLObjectType = schemaDoc.getSubscriptionType(); + const queries = generateQueries(queryTypes, schemaDoc, maxDepth, options) || []; + const mutations = generateMutations(mutationType, schemaDoc, maxDepth, options) || []; + const subscriptions = generateSubscriptions(subscriptionType, schemaDoc, maxDepth, options) || []; const fragments = options.useExternalFragmentForS3Object ? collectExternalFragments([...queries, ...mutations, ...subscriptions]) : []; - return { queries, mutations, subscriptions, fragments } + return { queries, mutations, subscriptions, fragments }; } catch (e) { - throw new Error('GraphQL schema file should contain a valid GraphQL introspection query result') + throw new Error('GraphQL schema file should contain a valid GraphQL introspection query result'); } } diff --git a/packages/amplify-graphql-docs-generator/src/generator/generateAllOperations.ts b/packages/amplify-graphql-docs-generator/src/generator/generateAllOperations.ts index 4f0debf5f7..1e2d0eae4a 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/generateAllOperations.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/generateAllOperations.ts @@ -1,8 +1,15 @@ -import { GraphQLObjectType, GraphQLSchema } from 'graphql' -const pascalCase = require('pascal-case') +import { GraphQLObjectType, GraphQLSchema } from 'graphql'; +const pascalCase = require('pascal-case'); -import generateOperation from './generateOperation' -import { GQLTemplateOp, GQLOperationTypeEnum, GQLTemplateGenericOp, GQLTemplateField, GQLTemplateFragment, GQLDocsGenOptions } from './types' +import generateOperation from './generateOperation'; +import { + GQLTemplateOp, + GQLOperationTypeEnum, + GQLTemplateGenericOp, + GQLTemplateField, + GQLTemplateFragment, + GQLDocsGenOptions, +} from './types'; export function generateQueries( queries: GraphQLObjectType, @@ -11,14 +18,14 @@ export function generateQueries( options: GQLDocsGenOptions ): Array | undefined { if (queries) { - const allQueries = queries.getFields() - const processedQueries: Array = Object.keys(allQueries).map((queryName) => { - const type: GQLOperationTypeEnum = GQLOperationTypeEnum.QUERY - const op = generateOperation(allQueries[queryName], schema, maxDepth, options) - const name: string = pascalCase(queryName) - return { type, name, ...op } - }) - return processedQueries + const allQueries = queries.getFields(); + const processedQueries: Array = Object.keys(allQueries).map(queryName => { + const type: GQLOperationTypeEnum = GQLOperationTypeEnum.QUERY; + const op = generateOperation(allQueries[queryName], schema, maxDepth, options); + const name: string = pascalCase(queryName); + return { type, name, ...op }; + }); + return processedQueries; } } @@ -27,17 +34,16 @@ export function generateMutations( schema: GraphQLSchema, maxDepth: number, options: GQLDocsGenOptions - ): Array { if (mutations) { - const allMutations = mutations.getFields() - const processedMutations = Object.keys(allMutations).map((mutationName) => { - const type: GQLOperationTypeEnum = GQLOperationTypeEnum.MUTATION - const op = generateOperation(allMutations[mutationName], schema, maxDepth, options) - const name = pascalCase(mutationName) - return { type, name, ...op } - }) - return processedMutations + const allMutations = mutations.getFields(); + const processedMutations = Object.keys(allMutations).map(mutationName => { + const type: GQLOperationTypeEnum = GQLOperationTypeEnum.MUTATION; + const op = generateOperation(allMutations[mutationName], schema, maxDepth, options); + const name = pascalCase(mutationName); + return { type, name, ...op }; + }); + return processedMutations; } } @@ -48,35 +54,35 @@ export function generateSubscriptions( options: GQLDocsGenOptions ): Array { if (subscriptions) { - const allSubscriptions = subscriptions.getFields() - const processedMutations = Object.keys(allSubscriptions).map((subscriptionName) => { - const type: GQLOperationTypeEnum = GQLOperationTypeEnum.SUBSCRIPTION - const op = generateOperation(allSubscriptions[subscriptionName], schema, maxDepth, options) - const name = pascalCase(subscriptionName) - return { type, name, ...op } - }) - return processedMutations + const allSubscriptions = subscriptions.getFields(); + const processedMutations = Object.keys(allSubscriptions).map(subscriptionName => { + const type: GQLOperationTypeEnum = GQLOperationTypeEnum.SUBSCRIPTION; + const op = generateOperation(allSubscriptions[subscriptionName], schema, maxDepth, options); + const name = pascalCase(subscriptionName); + return { type, name, ...op }; + }); + return processedMutations; } } export function collectExternalFragments(operations: GQLTemplateOp[] = []): GQLTemplateFragment[] { const fragments = {}; - operations.forEach((op) => { + operations.forEach(op => { getExternalFragment(op.body, fragments); }); return Object.values(fragments); } function getExternalFragment(field: GQLTemplateField, externalFragments: object = {}) { - field.fragments.filter((fragment) => - fragment.external - ).reduce((acc, val) => { - acc[val.name] = val; - return acc; - }, externalFragments); - field.fields.forEach((f) => { + field.fragments + .filter(fragment => fragment.external) + .reduce((acc, val) => { + acc[val.name] = val; + return acc; + }, externalFragments); + field.fields.forEach(f => { getExternalFragment(f, externalFragments); }); return externalFragments; -} \ No newline at end of file +} diff --git a/packages/amplify-graphql-docs-generator/src/generator/generateOperation.ts b/packages/amplify-graphql-docs-generator/src/generator/generateOperation.ts index 83afcafad1..ec0ae8ddfe 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/generateOperation.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/generateOperation.ts @@ -1,8 +1,8 @@ -import { GraphQLField, GraphQLSchema } from 'graphql' +import { GraphQLField, GraphQLSchema } from 'graphql'; -import getArgs from './getArgs' -import getBody from './getBody' -import { GQLTemplateGenericOp, GQLTemplateArgDeclaration, GQLTemplateOpBody, GQLDocsGenOptions } from './types' +import getArgs from './getArgs'; +import getBody from './getBody'; +import { GQLTemplateGenericOp, GQLTemplateArgDeclaration, GQLTemplateOpBody, GQLDocsGenOptions } from './types'; export default function generateOperation( operation: GraphQLField, @@ -10,10 +10,10 @@ export default function generateOperation( maxDepth: number = 3, options: GQLDocsGenOptions ): GQLTemplateGenericOp { - const args: Array = getArgs(operation.args) - const body: GQLTemplateOpBody = getBody(operation, schema, maxDepth, options) + const args: Array = getArgs(operation.args); + const body: GQLTemplateOpBody = getBody(operation, schema, maxDepth, options); return { args, body, - } + }; } diff --git a/packages/amplify-graphql-docs-generator/src/generator/getBody.ts b/packages/amplify-graphql-docs-generator/src/generator/getBody.ts index 504bd1bca6..c231aee6f8 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/getBody.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/getBody.ts @@ -1,7 +1,7 @@ -import { GraphQLField, GraphQLSchema } from 'graphql' +import { GraphQLField, GraphQLSchema } from 'graphql'; -import getFields from './getFields' -import { GQLTemplateOpBody, GQLTemplateArgInvocation, GQLTemplateField, GQLDocsGenOptions } from './types' +import getFields from './getFields'; +import { GQLTemplateOpBody, GQLTemplateArgInvocation, GQLTemplateField, GQLDocsGenOptions } from './types'; export default function getBody( op: GraphQLField, @@ -9,13 +9,13 @@ export default function getBody( maxDepth: number = 3, options: GQLDocsGenOptions ): GQLTemplateOpBody { - const args: Array = op.args.map((arg) => ({ + const args: Array = op.args.map(arg => ({ name: arg.name, value: `\$${arg.name}`, - })) - const fields: GQLTemplateField = getFields(op, schema, maxDepth, options) + })); + const fields: GQLTemplateField = getFields(op, schema, maxDepth, options); return { args, ...fields, - } + }; } diff --git a/packages/amplify-graphql-docs-generator/src/generator/getFragment.ts b/packages/amplify-graphql-docs-generator/src/generator/getFragment.ts index e39cb97a24..d8a76efd04 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/getFragment.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/getFragment.ts @@ -1,7 +1,7 @@ -import { GraphQLObjectType, GraphQLSchema } from 'graphql' +import { GraphQLObjectType, GraphQLSchema } from 'graphql'; -import getFields from './getFields' -import { GQLTemplateField, GQLTemplateFragment, GQLDocsGenOptions } from './types' +import getFields from './getFields'; +import { GQLTemplateField, GQLTemplateFragment, GQLDocsGenOptions } from './types'; export default function getFragment( typeObj: GraphQLObjectType, @@ -12,17 +12,17 @@ export default function getFragment( external: boolean = false, options?: GQLDocsGenOptions ): GQLTemplateFragment { - const subFields = (typeObj && typeObj.getFields && typeObj.getFields()) || [] - const filterFieldNames = filterFields.map((f) => f.name) + const subFields = (typeObj && typeObj.getFields && typeObj.getFields()) || []; + const filterFieldNames = filterFields.map(f => f.name); const fields: Array = Object.keys(subFields) - .map((field) => getFields(subFields[field], schema, depth - 1, options)) - .filter((field) => field && !filterFieldNames.includes(field.name)) + .map(field => getFields(subFields[field], schema, depth - 1, options)) + .filter(field => field && !filterFieldNames.includes(field.name)); if (fields.length) { return { on: typeObj.name, fields, external, name: name || `${typeObj.name}Fragment`, - } + }; } } diff --git a/packages/amplify-graphql-docs-generator/src/generator/index.ts b/packages/amplify-graphql-docs-generator/src/generator/index.ts index db373d98c0..2b57d265d1 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/index.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/index.ts @@ -1,6 +1,6 @@ -import * as types from './types' +import * as types from './types'; -export * from './types' -import generate from './generate' -export { generateMutations, generateSubscriptions, generateQueries } from './generateAllOperations' -export default generate +export * from './types'; +import generate from './generate'; +export { generateMutations, generateSubscriptions, generateQueries } from './generateAllOperations'; +export default generate; diff --git a/packages/amplify-graphql-docs-generator/src/generator/types.ts b/packages/amplify-graphql-docs-generator/src/generator/types.ts index 4bbd3bbe3d..56fe775d18 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/types.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/types.ts @@ -5,7 +5,7 @@ import { GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, -} from 'graphql' +} from 'graphql'; export type GQLConcreteType = | GraphQLScalarType @@ -13,14 +13,14 @@ export type GQLConcreteType = | GraphQLInterfaceType | GraphQLUnionType | GraphQLEnumType - | GraphQLInputObjectType + | GraphQLInputObjectType; export type GQLTemplateFragment = { - on: string, - fields: Array, - external: boolean, - name: string, -} + on: string; + fields: Array; + external: boolean; + name: string; +}; export enum GQLOperationTypeEnum { QUERY = 'query', @@ -29,47 +29,47 @@ export enum GQLOperationTypeEnum { } export type GQLTemplateField = { - name: string - fields: Array - fragments: Array - hasBody: boolean -} + name: string; + fields: Array; + fragments: Array; + hasBody: boolean; +}; export type GQLTemplateArgDeclaration = { - name: string - type: string - isRequired: boolean - isList: boolean - isListRequired: boolean - defaultValue: string | null -} + name: string; + type: string; + isRequired: boolean; + isList: boolean; + isListRequired: boolean; + defaultValue: string | null; +}; export type GQLTemplateArgInvocation = { - name: string - value: string -} + name: string; + value: string; +}; export type GQLTemplateOpBody = GQLTemplateField & { - args: Array -} + args: Array; +}; export type GQLTemplateGenericOp = { - args: Array - body: GQLTemplateOpBody -} + args: Array; + body: GQLTemplateOpBody; +}; export type GQLTemplateOp = GQLTemplateGenericOp & { - type: GQLOperationTypeEnum - name: string -} + type: GQLOperationTypeEnum; + name: string; +}; export type GQLAllOperations = { - queries: Array - mutations: Array - subscriptions: Array - fragments: Array -} + queries: Array; + mutations: Array; + subscriptions: Array; + fragments: Array; +}; export type GQLDocsGenOptions = { - useExternalFragmentForS3Object: boolean -} + useExternalFragmentForS3Object: boolean; +}; diff --git a/packages/amplify-graphql-docs-generator/src/generator/utils/getType.ts b/packages/amplify-graphql-docs-generator/src/generator/utils/getType.ts index f160da8c48..425b689185 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/utils/getType.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/utils/getType.ts @@ -1,6 +1,6 @@ -import { GraphQLList, GraphQLNonNull, GraphQLType } from "graphql"; +import { GraphQLList, GraphQLNonNull, GraphQLType } from 'graphql'; -import { GQLConcreteType } from "../types"; +import { GQLConcreteType } from '../types'; export default function getType(typeObj: GraphQLType): GQLConcreteType { if (typeObj instanceof GraphQLList || typeObj instanceof GraphQLNonNull) { diff --git a/packages/amplify-graphql-docs-generator/src/generator/utils/isList.ts b/packages/amplify-graphql-docs-generator/src/generator/utils/isList.ts index f0826d5f71..139e384201 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/utils/isList.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/utils/isList.ts @@ -1,4 +1,4 @@ -import { GraphQLType, isNonNullType, isListType } from "graphql"; +import { GraphQLType, isNonNullType, isListType } from 'graphql'; export default function isList(typeObj: GraphQLType): boolean { if (isNonNullType(typeObj)) { diff --git a/packages/amplify-graphql-docs-generator/src/generator/utils/isRequiredList.ts b/packages/amplify-graphql-docs-generator/src/generator/utils/isRequiredList.ts index ec80272571..2d98155cc7 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/utils/isRequiredList.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/utils/isRequiredList.ts @@ -1,4 +1,4 @@ -import { GraphQLType, isNonNullType, isListType } from 'graphql' +import { GraphQLType, isNonNullType, isListType } from 'graphql'; export default function isRequired(typeObj: GraphQLType): boolean { - return isNonNullType(typeObj) && isListType(typeObj.ofType) + return isNonNullType(typeObj) && isListType(typeObj.ofType); } diff --git a/packages/amplify-graphql-docs-generator/src/generator/utils/isS3Object.ts b/packages/amplify-graphql-docs-generator/src/generator/utils/isS3Object.ts index 44db87ec23..d2ae53955b 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/utils/isS3Object.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/utils/isS3Object.ts @@ -1,14 +1,14 @@ -import { GraphQLType, isObjectType, isScalarType } from "graphql"; -import getType from "./getType"; -const S3_FIELD_NAMES = ['bucket', 'key', 'region' ]; +import { GraphQLType, isObjectType, isScalarType } from 'graphql'; +import getType from './getType'; +const S3_FIELD_NAMES = ['bucket', 'key', 'region']; export default function isS3Object(typeObj: GraphQLType): boolean { if (isObjectType(typeObj)) { const fields = typeObj.getFields(); const fieldName = typeObj.name; - const hasS3Fields = S3_FIELD_NAMES.every((s3Field) => { + const hasS3Fields = S3_FIELD_NAMES.every(s3Field => { const field = fields[s3Field]; try { - const type = getType(field.type) + const type = getType(field.type); return field && isScalarType(type) && type.name === 'String'; } catch (e) { return false; diff --git a/packages/amplify-graphql-docs-generator/src/generator/utils/loading.ts b/packages/amplify-graphql-docs-generator/src/generator/utils/loading.ts index bf4be0c111..bc0e606fd7 100644 --- a/packages/amplify-graphql-docs-generator/src/generator/utils/loading.ts +++ b/packages/amplify-graphql-docs-generator/src/generator/utils/loading.ts @@ -1,26 +1,17 @@ -import * as fs from 'fs' - -import { - buildClientSchema, - Source, - concatAST, - parse, - DocumentNode, - GraphQLSchema, - buildASTSchema -} from 'graphql'; +import * as fs from 'fs'; -import { extname, join, normalize } from 'path'; +import { buildClientSchema, Source, concatAST, parse, DocumentNode, GraphQLSchema, buildASTSchema } from 'graphql'; +import { extname, join, normalize } from 'path'; export function loadSchema(schemaPath: string): GraphQLSchema { if (extname(schemaPath) === '.json') { - return loadIntrospectionSchema(schemaPath) + return loadIntrospectionSchema(schemaPath); } - return loadSDLSchema(schemaPath) + return loadSDLSchema(schemaPath); } -function loadIntrospectionSchema(schemaPath: string): GraphQLSchema { +function loadIntrospectionSchema(schemaPath: string): GraphQLSchema { if (!fs.existsSync(schemaPath)) { throw new Error(`Cannot find GraphQL schema file: ${schemaPath}`); } @@ -29,23 +20,25 @@ function loadIntrospectionSchema(schemaPath: string): GraphQLSchema { if (!schemaData.data && !schemaData.__schema) { throw new Error('GraphQL schema file should contain a valid GraphQL introspection query result'); } - return buildClientSchema((schemaData.data) ? schemaData.data : schemaData); + return buildClientSchema(schemaData.data ? schemaData.data : schemaData); } -function loadSDLSchema(schemaPath: string): GraphQLSchema { +function loadSDLSchema(schemaPath: string): GraphQLSchema { const authDirectivePath = normalize(join(__dirname, '../../..', 'awsApppSyncDirectives.graphql')); const doc = loadAndMergeQueryDocuments([authDirectivePath, schemaPath]); return buildASTSchema(doc); } export function loadAndMergeQueryDocuments(inputPaths: string[], tagName: string = 'gql'): DocumentNode { - const sources = inputPaths.map(inputPath => { - const body = fs.readFileSync(inputPath, 'utf8'); - if (!body) { - return null; - } - return new Source(body, inputPath); - }).filter(source => source); + const sources = inputPaths + .map(inputPath => { + const body = fs.readFileSync(inputPath, 'utf8'); + if (!body) { + return null; + } + return new Source(body, inputPath); + }) + .filter(source => source); return concatAST((sources as Source[]).map(source => parse(source))); -} \ No newline at end of file +} diff --git a/packages/amplify-graphql-docs-generator/src/logger.ts b/packages/amplify-graphql-docs-generator/src/logger.ts index cf7fe29164..449be3e40d 100644 --- a/packages/amplify-graphql-docs-generator/src/logger.ts +++ b/packages/amplify-graphql-docs-generator/src/logger.ts @@ -1,7 +1,7 @@ export function logError(error: Error): void { - console.error(error.message) + console.error(error.message); } export function logMessage(message: String): void { - console.log(message) + console.log(message); } diff --git a/packages/amplify-graphql-types-generator/fixtures/angular.ts b/packages/amplify-graphql-types-generator/fixtures/angular.ts index 649e04b7ba..b774c23f90 100644 --- a/packages/amplify-graphql-types-generator/fixtures/angular.ts +++ b/packages/amplify-graphql-types-generator/fixtures/angular.ts @@ -1,15 +1,15 @@ /* tslint:disable */ // This file was automatically generated and should not be edited. -import { Injectable } from "@angular/core"; -import { graphqlOperation } from "aws-amplify"; -import { AmplifyService } from "aws-amplify-angular"; +import { Injectable } from '@angular/core'; +import { graphqlOperation } from 'aws-amplify'; +import { AmplifyService } from 'aws-amplify-angular'; @Injectable({ - providedIn: "root" + providedIn: 'root', }) export enum Episode { - NEWHOPE = "NEWHOPE", - EMPIRE = "EMPIRE", - JEDI = "JEDI" + NEWHOPE = 'NEWHOPE', + EMPIRE = 'EMPIRE', + JEDI = 'JEDI', } export type ReviewInput = { @@ -2425,9 +2425,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Hero.items; } async Reviews(input: ReviewsQueryVariables): Promise { @@ -2438,9 +2436,7 @@ export class AppSyncService { commentary } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Reviews.items; } async Search(input: SearchQueryVariables): Promise { @@ -2582,9 +2578,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Search.items; } async Character(input: CharacterQueryVariables): Promise { @@ -2700,9 +2694,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Character.items; } async Droid(input: DroidQueryVariables): Promise { @@ -2854,9 +2846,7 @@ export class AppSyncService { primaryFunction } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Droid.items; } async Human(input: HumanQueryVariables): Promise { @@ -3016,9 +3006,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Human.items; } async Starship(input: StarshipQueryVariables): Promise { @@ -3030,14 +3018,10 @@ export class AppSyncService { coordinates } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Starship.items; } - async CreateReview( - input: CreateReviewMutationVariables - ): Promise { + async CreateReview(input: CreateReviewMutationVariables): Promise { const statement = `mutation CreateReview($episode: Episode, $review: ReviewInput!) { createReview(episode: $episode, review: $review) { episode @@ -3045,14 +3029,10 @@ export class AppSyncService { commentary } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.CreateReview.items; } - async ReviewAdded( - input: ReviewAddedSubscriptionVariables - ): Promise { + async ReviewAdded(input: ReviewAddedSubscriptionVariables): Promise { const statement = `subscription ReviewAdded($episode: Episode) { reviewAdded(episode: $episode) { episode @@ -3060,9 +3040,7 @@ export class AppSyncService { commentary } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.ReviewAdded.items; } } diff --git a/packages/amplify-graphql-types-generator/fixtures/output.ts b/packages/amplify-graphql-types-generator/fixtures/output.ts index bca03129b0..b8beffb6b3 100644 --- a/packages/amplify-graphql-types-generator/fixtures/output.ts +++ b/packages/amplify-graphql-types-generator/fixtures/output.ts @@ -1,15 +1,15 @@ /* tslint:disable */ // This file was automatically generated and should not be edited. -import { Injectable } from "@angular/core"; -import { graphqlOperation } from "aws-amplify"; -import { AmplifyService } from "aws-amplify-angular"; +import { Injectable } from '@angular/core'; +import { graphqlOperation } from 'aws-amplify'; +import { AmplifyService } from 'aws-amplify-angular'; @Injectable({ - providedIn: "root" + providedIn: 'root', }) export enum Episode { - NEWHOPE = "NEWHOPE", - EMPIRE = "EMPIRE", - JEDI = "JEDI" + NEWHOPE = 'NEWHOPE', + EMPIRE = 'EMPIRE', + JEDI = 'JEDI', } export type ReviewInput = { @@ -2425,9 +2425,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Hero.items; } async Reviews(input: ReviewsQueryVariables): ReviewsQuery { @@ -2438,9 +2436,7 @@ export class AppSyncService { commentary } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Reviews.items; } async Search(input: SearchQueryVariables): SearchQuery { @@ -2582,9 +2578,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Search.items; } async Character(input: CharacterQueryVariables): CharacterQuery { @@ -2700,9 +2694,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Character.items; } async Droid(input: DroidQueryVariables): DroidQuery { @@ -2854,9 +2846,7 @@ export class AppSyncService { primaryFunction } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Droid.items; } async Human(input: HumanQueryVariables): HumanQuery { @@ -3016,9 +3006,7 @@ export class AppSyncService { } } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Human.items; } async Starship(input: StarshipQueryVariables): StarshipQuery { @@ -3030,14 +3018,10 @@ export class AppSyncService { coordinates } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.Starship.items; } - async CreateReview( - input: CreateReviewMutationVariables - ): CreateReviewMutation { + async CreateReview(input: CreateReviewMutationVariables): CreateReviewMutation { const statement = `mutation CreateReview($episode: Episode, $review: ReviewInput!) { createReview(episode: $episode, review: $review) { episode @@ -3045,14 +3029,10 @@ export class AppSyncService { commentary } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.CreateReview.items; } - async ReviewAdded( - input: ReviewAddedSubscriptionVariables - ): ReviewAddedSubscription { + async ReviewAdded(input: ReviewAddedSubscriptionVariables): ReviewAddedSubscription { const statement = `subscription ReviewAdded($episode: Episode) { reviewAdded(episode: $episode) { episode @@ -3060,9 +3040,7 @@ export class AppSyncService { commentary } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data.ReviewAdded.items; } } diff --git a/packages/amplify-graphql-types-generator/fixtures/starwars.service.ts b/packages/amplify-graphql-types-generator/fixtures/starwars.service.ts index 36144fa665..6ae95c532b 100644 --- a/packages/amplify-graphql-types-generator/fixtures/starwars.service.ts +++ b/packages/amplify-graphql-types-generator/fixtures/starwars.service.ts @@ -1,14 +1,14 @@ /* tslint:disable */ // This file was automatically generated and should not be edited. -import { Injectable } from "@angular/core"; -import API, { graphqlOperation } from "@aws-amplify/api"; -import { GraphQLResult } from "@aws-amplify/api/lib/types"; -import * as Observable from "zen-observable"; +import { Injectable } from '@angular/core'; +import API, { graphqlOperation } from '@aws-amplify/api'; +import { GraphQLResult } from '@aws-amplify/api/lib/types'; +import * as Observable from 'zen-observable'; export enum Episode { - NEWHOPE = "NEWHOPE", - EMPIRE = "EMPIRE", - JEDI = "JEDI" + NEWHOPE = 'NEWHOPE', + EMPIRE = 'EMPIRE', + JEDI = 'JEDI', } export type ReviewInput = { @@ -1549,7 +1549,7 @@ export type ReviewAddedSubscription = { }; @Injectable({ - providedIn: "root" + providedIn: 'root', }) export class APIService { async Hero(episode?: Episode): Promise { @@ -1669,9 +1669,7 @@ export class APIService { if (episode) { gqlAPIServiceArguments.episode = episode; } - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.hero; } async Reviews(episode: Episode): Promise { @@ -1683,11 +1681,9 @@ export class APIService { } }`; const gqlAPIServiceArguments: any = { - episode + episode, }; - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.reviews; } async Search(text?: string): Promise { @@ -1833,9 +1829,7 @@ export class APIService { if (text) { gqlAPIServiceArguments.text = text; } - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.search; } async Character(id: string): Promise { @@ -1952,11 +1946,9 @@ export class APIService { } }`; const gqlAPIServiceArguments: any = { - id + id, }; - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.character; } async Droid(id: string): Promise { @@ -2109,11 +2101,9 @@ export class APIService { } }`; const gqlAPIServiceArguments: any = { - id + id, }; - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.droid; } async Human(id: string): Promise { @@ -2274,11 +2264,9 @@ export class APIService { } }`; const gqlAPIServiceArguments: any = { - id + id, }; - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.human; } async Starship(id: string): Promise { @@ -2291,17 +2279,12 @@ export class APIService { } }`; const gqlAPIServiceArguments: any = { - id + id, }; - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.starship; } - async CreateReview( - review: ReviewInput, - episode?: Episode - ): Promise { + async CreateReview(review: ReviewInput, episode?: Episode): Promise { const statement = `mutation CreateReview($episode: Episode, $review: ReviewInput!) { createReview(episode: $episode, review: $review) { episode @@ -2310,14 +2293,12 @@ export class APIService { } }`; const gqlAPIServiceArguments: any = { - review + review, }; if (episode) { gqlAPIServiceArguments.episode = episode; } - const response = (await API.graphql( - graphqlOperation(statement, gqlAPIServiceArguments) - )) as any; + const response = (await API.graphql(graphqlOperation(statement, gqlAPIServiceArguments))) as any; return response.data.createReview; } ReviewAddedListener: Observable = API.graphql( diff --git a/packages/amplify-graphql-types-generator/fixtures/test.ts b/packages/amplify-graphql-types-generator/fixtures/test.ts index 9930703a3b..a52ae50484 100644 --- a/packages/amplify-graphql-types-generator/fixtures/test.ts +++ b/packages/amplify-graphql-types-generator/fixtures/test.ts @@ -2,1830 +2,2304 @@ // This file was automatically generated and should not be edited. export enum Episode { - NEWHOPE = "NEWHOPE", - EMPIRE = "EMPIRE", - JEDI = "JEDI", + NEWHOPE = 'NEWHOPE', + EMPIRE = 'EMPIRE', + JEDI = 'JEDI', } - export type ReviewInput = { - stars: number, - commentary?: string | null, - favorite_color?: ColorInput | null, + stars: number; + commentary?: string | null; + favorite_color?: ColorInput | null; }; export type ColorInput = { - red: number, - green: number, - blue: number, + red: number; + green: number; + blue: number; }; export type HeroQueryVariables = { - episode?: Episode | null, + episode?: Episode | null; }; export type HeroQuery = { - hero: ( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, + hero: + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null, + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null; }; export type ReviewsQueryVariables = { - episode: Episode, + episode: Episode; }; export type ReviewsQuery = { - reviews: Array< { - episode: Episode | null, - stars: number, - commentary: string | null, - } | null > | null, + reviews: Array<{ + episode: Episode | null; + stars: number; + commentary: string | null; + } | null> | null; }; export type SearchQueryVariables = { - text?: string | null, + text?: string | null; }; export type SearchQuery = { - search: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, + search: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } | { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } - ) | null > | null, + | { + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + }) + | null + > | null; }; export type CharacterQueryVariables = { - id: string, + id: string; }; export type CharacterQuery = { - character: ( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, + character: + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null, + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null; }; export type DroidQueryVariables = { - id: string, + id: string; }; export type DroidQuery = { - droid: { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, + droid: { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } | null, + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + } | null; }; export type HumanQueryVariables = { - id: string, + id: string; }; export type HumanQuery = { - human: { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, + human: { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + homePlanet: string | null; + height: number | null; + mass: number | null; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + primaryFunction: string | null; + }) + | null + > | null; + friendsConnection: { + totalCount: number | null; + edges: Array<{ + cursor: string; + } | null> | null; + friends: Array< + | ( + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + homePlanet: string | null; + height: number | null; + mass: number | null; } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - homePlanet: string | null, - height: number | null, - mass: number | null, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - primaryFunction: string | null, - } - ) | null > | null, - friendsConnection: { - totalCount: number | null, - edges: Array< { - cursor: string, - } | null > | null, - friends: Array<( { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - friends: Array<( { - id: string, - name: string, - homePlanet: string | null, - height: number | null, - mass: number | null, - } | { - id: string, - name: string, - primaryFunction: string | null, - } - ) | null > | null, - primaryFunction: string | null, - } - ) | null > | null, - }, - appearsIn: Array< Episode | null >, - starships: Array< { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null > | null, - } | null, + | { + id: string; + name: string; + friends: Array< + | ( + | { + id: string; + name: string; + homePlanet: string | null; + height: number | null; + mass: number | null; + } + | { + id: string; + name: string; + primaryFunction: string | null; + }) + | null + > | null; + primaryFunction: string | null; + }) + | null + > | null; + }; + appearsIn: Array; + starships: Array<{ + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null> | null; + } | null; }; export type StarshipQueryVariables = { - id: string, + id: string; }; export type StarshipQuery = { - starship: { - id: string, - name: string, - length: number | null, - coordinates: Array< Array< number > > | null, - } | null, + starship: { + id: string; + name: string; + length: number | null; + coordinates: Array> | null; + } | null; }; export type CreateReviewMutationVariables = { - episode?: Episode | null, - review: ReviewInput, + episode?: Episode | null; + review: ReviewInput; }; export type CreateReviewMutation = { - createReview: { - episode: Episode | null, - stars: number, - commentary: string | null, - } | null, + createReview: { + episode: Episode | null; + stars: number; + commentary: string | null; + } | null; }; export type ReviewAddedSubscriptionVariables = { - episode?: Episode | null, + episode?: Episode | null; }; export type ReviewAddedSubscription = { - reviewAdded: { - episode: Episode | null, - stars: number, - commentary: string | null, - } | null, + reviewAdded: { + episode: Episode | null; + stars: number; + commentary: string | null; + } | null; }; diff --git a/packages/amplify-graphql-types-generator/fixtures/todo.service.ts b/packages/amplify-graphql-types-generator/fixtures/todo.service.ts index c8d0073f04..fab8f8abfd 100644 --- a/packages/amplify-graphql-types-generator/fixtures/todo.service.ts +++ b/packages/amplify-graphql-types-generator/fixtures/todo.service.ts @@ -1,9 +1,9 @@ /* tslint:disable */ // This file was automatically generated and should not be edited. -import { Injectable } from "@angular/core"; -import { graphqlOperation } from "aws-amplify"; -import { AmplifyService } from "aws-amplify-angular"; -import * as Observable from "zen-observable"; +import { Injectable } from '@angular/core'; +import { graphqlOperation } from 'aws-amplify'; +import { AmplifyService } from 'aws-amplify-angular'; +import * as Observable from 'zen-observable'; export type ModelTodoFilterInput = { id?: ModelIDFilterInput | null; @@ -145,7 +145,7 @@ export type OnDeleteTodoSubscription = { }; @Injectable({ - providedIn: "root" + providedIn: 'root', }) export class AppSyncService { constructor(private amplifyService: AmplifyService) {} @@ -157,9 +157,7 @@ export class AppSyncService { description } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data; } async ListTodos(input: ListTodosQueryVariables): Promise { @@ -173,14 +171,10 @@ export class AppSyncService { nextToken } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data; } - async CreateTodo( - input: CreateTodoMutationVariables - ): Promise { + async CreateTodo(input: CreateTodoMutationVariables): Promise { const statement = `mutation CreateTodo($input: CreateTodoInput!) { createTodo(input: $input) { id @@ -188,14 +182,10 @@ export class AppSyncService { description } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data; } - async UpdateTodo( - input: UpdateTodoMutationVariables - ): Promise { + async UpdateTodo(input: UpdateTodoMutationVariables): Promise { const statement = `mutation UpdateTodo($input: UpdateTodoInput!) { updateTodo(input: $input) { id @@ -203,14 +193,10 @@ export class AppSyncService { description } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data; } - async DeleteTodo( - input: DeleteTodoMutationVariables - ): Promise { + async DeleteTodo(input: DeleteTodoMutationVariables): Promise { const statement = `mutation DeleteTodo($input: DeleteTodoInput!) { deleteTodo(input: $input) { id @@ -218,14 +204,10 @@ export class AppSyncService { description } }`; - const response = await this.amplifyService - .api() - .graphql(graphqlOperation(statement, input)); + const response = await this.amplifyService.api().graphql(graphqlOperation(statement, input)); return response.data; } - OnCreateTodoListener: Observable< - OnCreateTodoSubscription - > = this.amplifyService.api().graphql( + OnCreateTodoListener: Observable = this.amplifyService.api().graphql( graphqlOperation( `subscription OnCreateTodo { onCreateTodo { @@ -237,9 +219,7 @@ export class AppSyncService { ) ) as Observable; - OnUpdateTodoListener: Observable< - OnUpdateTodoSubscription - > = this.amplifyService.api().graphql( + OnUpdateTodoListener: Observable = this.amplifyService.api().graphql( graphqlOperation( `subscription OnUpdateTodo { onUpdateTodo { @@ -251,9 +231,7 @@ export class AppSyncService { ) ) as Observable; - OnDeleteTodoListener: Observable< - OnDeleteTodoSubscription - > = this.amplifyService.api().graphql( + OnDeleteTodoListener: Observable = this.amplifyService.api().graphql( graphqlOperation( `subscription OnDeleteTodo { onDeleteTodo { diff --git a/packages/amplify-graphql-types-generator/src/angular/index.ts b/packages/amplify-graphql-types-generator/src/angular/index.ts index 05c64ce758..6089342271 100644 --- a/packages/amplify-graphql-types-generator/src/angular/index.ts +++ b/packages/amplify-graphql-types-generator/src/angular/index.ts @@ -1,11 +1,6 @@ import { GraphQLNonNull, GraphQLType, isScalarType } from 'graphql'; import * as prettier from 'prettier'; -import { - LegacyCompilerContext, - LegacyOperation, - LegacyInlineFragment, - LegacyField -} from '../compiler/legacyIR'; +import { LegacyCompilerContext, LegacyOperation, LegacyInlineFragment, LegacyField } from '../compiler/legacyIR'; import CodeGenerator from '../utilities/CodeGenerator'; import { @@ -14,7 +9,7 @@ import { propertiesFromFields, updateTypeNameField, propertyDeclarations, - interfaceNameFromOperation + interfaceNameFromOperation, } from '../typescript/codeGeneration'; import { typeNameFromGraphQLType } from '../typescript/types'; import { Property, interfaceDeclaration } from '../typescript/language'; @@ -46,15 +41,10 @@ function generateTypes(generator: CodeGenerator, context: LegacyCompilerContext) interfaceDeclarationForOperation(generator, operation); }); - Object.values(context.fragments).forEach(operation => - interfaceDeclarationForFragment(generator, operation) - ); + Object.values(context.fragments).forEach(operation => interfaceDeclarationForFragment(generator, operation)); } -function interfaceDeclarationForOperation( - generator: CodeGenerator, - { operationName, operationType, fields }: LegacyOperation -) { +function interfaceDeclarationForOperation(generator: CodeGenerator, { operationName, operationType, fields }: LegacyOperation) { const interfaceName = interfaceNameFromOperation({ operationName, operationType }); fields = fields.map(field => updateTypeNameField(field)); @@ -74,19 +64,19 @@ function interfaceDeclarationForOperation( // } // } // but the interface is needed only for the result value of getAudioAlbum - if (fields[0].fields) { // execute only if there are sub fields + if (fields[0].fields) { + // execute only if there are sub fields const properties = propertiesFromFields(generator.context, fields[0].fields as LegacyField[]); interfaceDeclaration( generator, { - interfaceName + interfaceName, }, () => { propertyDeclarations(generator, properties); } ); } - } function getOperationResultField(operation: LegacyOperation): LegacyField | void { @@ -98,7 +88,7 @@ function getOperationResultField(operation: LegacyOperation): LegacyField | void function getReturnTypeName(generator: CodeGenerator, op: LegacyOperation): String { const { operationName, operationType } = op; if (isScalarType(op.fields[0].type)) { - return typeNameFromGraphQLType(generator.context, op.fields[0].type) + return typeNameFromGraphQLType(generator.context, op.fields[0].type); } else { return interfaceNameFromOperation({ operationName, operationType }); } @@ -155,9 +145,7 @@ function generateQueryOrMutationOperation(generator: CodeGenerator, op: LegacyOp variableAssignmentToInput(generator, vars); params.push('gqlAPIServiceArguments'); } - generator.printOnNewline( - `const response = await API.graphql(graphqlOperation(${params.join(', ')})) as any;` - ); + generator.printOnNewline(`const response = await API.graphql(graphqlOperation(${params.join(', ')})) as any;`); generator.printOnNewline(`return (<${returnType}>response.data${resultProp})`); }); generator.printOnNewline('}'); @@ -267,9 +255,7 @@ function variableAssignmentToInput(generator: CodeGenerator, vars: Property[]) { } function formatTemplateString(generator: CodeGenerator, str: string): string { - const indentation = ' '.repeat( - generator.currentFile.indentWidth * (generator.currentFile.indentLevel + 2) - ); + const indentation = ' '.repeat(generator.currentFile.indentWidth * (generator.currentFile.indentLevel + 2)); return str .split('\n') .map((line, idx) => (idx > 0 ? indentation + line : line)) diff --git a/packages/amplify-graphql-types-generator/src/cli.js b/packages/amplify-graphql-types-generator/src/cli.js index 20ff197010..9c473c54a9 100755 --- a/packages/amplify-graphql-types-generator/src/cli.js +++ b/packages/amplify-graphql-types-generator/src/cli.js @@ -6,12 +6,14 @@ import * as path from 'path'; import * as yargs from 'yargs'; import { downloadSchema, introspectSchema, printSchema, generate } from '.'; -import { ToolError, logError } from './errors' +import { ToolError, logError } from './errors'; -import 'source-map-support/register' +import 'source-map-support/register'; // Make sure unhandled errors in async code are propagated correctly -process.on('unhandledRejection', (error) => { throw error }); +process.on('unhandledRejection', error => { + throw error; +}); process.on('uncaughtException', handleError); @@ -40,7 +42,7 @@ yargs demand: false, describe: 'Code generation target language', choices: ['swift', 'scala', 'json', 'ts', 'typescript', 'flow', 'flow-modern', 'angular'], - default: 'angular' + default: 'angular', }, only: { describe: 'Parse all input files, but only output generated code for the specified file [Swift only]', @@ -50,51 +52,52 @@ yargs namespace: { demand: false, describe: 'Optional namespace for generated types [currently Swift and Scala-only]', - type: 'string' + type: 'string', }, - "passthrough-custom-scalars": { + 'passthrough-custom-scalars': { demand: false, describe: "Don't attempt to map custom scalars [temporary option]", - default: false + default: false, }, - "custom-scalars-prefix": { + 'custom-scalars-prefix': { demand: false, - describe: "Prefix for custom scalars. (Implies that passthrough-custom-scalars is true if set)", + describe: 'Prefix for custom scalars. (Implies that passthrough-custom-scalars is true if set)', default: '', - normalize: true + normalize: true, }, - "add-typename": { + 'add-typename': { demand: false, - describe: "For non-swift targets, always add the __typename GraphQL introspection type when generating target types", - default: false + describe: 'For non-swift targets, always add the __typename GraphQL introspection type when generating target types', + default: false, }, - "use-flow-exact-objects": { + 'use-flow-exact-objects': { demand: false, - describe: "Use Flow exact objects for generated types [flow-modern only]", + describe: 'Use Flow exact objects for generated types [flow-modern only]', default: false, - type: 'boolean' + type: 'boolean', }, - "tag-name": { + 'tag-name': { demand: false, - describe: "Name of the template literal tag used to identify template literals containing GraphQL queries in Javascript/Typescript code", - default: 'gql' + describe: + 'Name of the template literal tag used to identify template literals containing GraphQL queries in Javascript/Typescript code', + default: 'gql', }, - "project-name": { + 'project-name': { demand: false, - describe: "Name of the project to use in a multi-project .graphqlconfig file", + describe: 'Name of the project to use in a multi-project .graphqlconfig file', }, - "merge-in-fields-from-fragment-spreads": { + 'merge-in-fields-from-fragment-spreads': { demand: false, - describe: "Merge fragment fields onto its enclosing type", + describe: 'Merge fragment fields onto its enclosing type', default: true, - type: 'boolean' + type: 'boolean', }, - "complex-object-support": { + 'complex-object-support': { demand: false, - describe: "Adds S3 wrapper code to the output. [Swift only]", + describe: 'Adds S3 wrapper code to the output. [Swift only]', default: 'auto', choices: ['yes', 'no', 'auto'], - } + }, }, argv => { let { input } = argv; @@ -110,22 +113,21 @@ yargs .sort(); const options = { - passthroughCustomScalars: argv["passthrough-custom-scalars"] || argv["custom-scalars-prefix"] !== '', - customScalarsPrefix: argv["custom-scalars-prefix"] || '', - addTypename: argv["add-typename"], + passthroughCustomScalars: argv['passthrough-custom-scalars'] || argv['custom-scalars-prefix'] !== '', + customScalarsPrefix: argv['custom-scalars-prefix'] || '', + addTypename: argv['add-typename'], namespace: argv.namespace, - mergeInFieldsFromFragmentSpreads: argv["merge-in-fields-from-fragment-spreads"], + mergeInFieldsFromFragmentSpreads: argv['merge-in-fields-from-fragment-spreads'], useFlowExactObjects: argv['use-flow-exact-objects'], - complexObjectSupport: argv["complex-object-support"], + complexObjectSupport: argv['complex-object-support'], }; generate(inputPaths, argv.schema, argv.output, argv.only, argv.target, argv.tagName, options); - }, + } ) .fail(function(message, error) { handleError(error ? error : new ToolError(message)); }) .help() .version() - .strict() - .argv + .strict().argv; diff --git a/packages/amplify-graphql-types-generator/src/compiler/index.ts b/packages/amplify-graphql-types-generator/src/compiler/index.ts index be75c95c19..f80ce7c4aa 100644 --- a/packages/amplify-graphql-types-generator/src/compiler/index.ts +++ b/packages/amplify-graphql-types-generator/src/compiler/index.ts @@ -122,11 +122,7 @@ export interface FragmentSpread { selectionSet: SelectionSet; } -export function compileToIR( - schema: GraphQLSchema, - document: DocumentNode, - options: CompilerOptions = {} -): CompilerContext { +export function compileToIR(schema: GraphQLSchema, document: DocumentNode, options: CompilerOptions = {}): CompilerContext { if (options.addTypename) { document = withTypenameFieldAddedWhereNeeded(document); } @@ -151,12 +147,8 @@ export function compileToIR( } catch (e) { if (e instanceof GraphQLError) { if (definition.kind === 'OperationDefinition' && definition.name) { - const locInfo = definition.name.loc - ? `in ${definition.name.loc.source.name}` - : ''; - console.log( - `${e.message} but found ${definition.operation}: ${definition.name.value} ${locInfo} ` - ); + const locInfo = definition.name.loc ? `in ${definition.name.loc.source.name}` : ''; + console.log(`${e.message} but found ${definition.operation}: ${definition.name.value} ${locInfo} `); } else { console.log(e.message); } @@ -171,9 +163,7 @@ export function compileToIR( } // Compute the intersection between the possible,types of the fragment spread and the fragment. - const possibleTypes = fragment.selectionSet.possibleTypes.filter(type => - fragmentSpread.selectionSet.possibleTypes.includes(type) - ); + const possibleTypes = fragment.selectionSet.possibleTypes.filter(type => fragmentSpread.selectionSet.possibleTypes.includes(type)); fragmentSpread.isConditional = fragment.selectionSet.possibleTypes.some( type => !fragmentSpread.selectionSet.possibleTypes.includes(type) @@ -305,9 +295,7 @@ class Compiler { const fieldDef = getFieldDef(this.schema, parentType, selectionNode); if (!fieldDef) { - throw new GraphQLError(`Cannot query field "${name}" on type "${String(parentType)}"`, [ - selectionNode, - ]); + throw new GraphQLError(`Cannot query field "${name}" on type "${String(parentType)}"`, [selectionNode]); } const fieldType = fieldDef.type; @@ -347,35 +335,21 @@ class Compiler { if (isCompositeType(unmodifiedFieldType)) { const selectionSetNode = selectionNode.selectionSet; if (!selectionSetNode) { - throw new GraphQLError( - `Composite field "${name}" on type "${String(parentType)}" requires selection set`, - [selectionNode] - ); + throw new GraphQLError(`Composite field "${name}" on type "${String(parentType)}" requires selection set`, [selectionNode]); } - field.selectionSet = this.compileSelectionSet( - selectionNode.selectionSet as SelectionSetNode, - unmodifiedFieldType - ); + field.selectionSet = this.compileSelectionSet(selectionNode.selectionSet as SelectionSetNode, unmodifiedFieldType); } return field; } case Kind.INLINE_FRAGMENT: { const typeNode = selectionNode.typeCondition; - const type = typeNode - ? (typeFromAST(this.schema, typeNode) as GraphQLCompositeType) - : parentType; - const possibleTypesForTypeCondition = this.possibleTypesForType(type).filter(type => - possibleTypes.includes(type) - ); + const type = typeNode ? (typeFromAST(this.schema, typeNode) as GraphQLCompositeType) : parentType; + const possibleTypesForTypeCondition = this.possibleTypesForType(type).filter(type => possibleTypes.includes(type)); return { kind: 'TypeCondition', type, - selectionSet: this.compileSelectionSet( - selectionNode.selectionSet, - type, - possibleTypesForTypeCondition - ), + selectionSet: this.compileSelectionSet(selectionNode.selectionSet, type, possibleTypesForTypeCondition), }; } case Kind.FRAGMENT_SPREAD: { diff --git a/packages/amplify-graphql-types-generator/src/compiler/legacyIR.ts b/packages/amplify-graphql-types-generator/src/compiler/legacyIR.ts index 6bd8acfa7c..a4fc139839 100644 --- a/packages/amplify-graphql-types-generator/src/compiler/legacyIR.ts +++ b/packages/amplify-graphql-types-generator/src/compiler/legacyIR.ts @@ -99,10 +99,7 @@ export function compileToLegacyIR( } class LegacyIRTransformer { - constructor( - public context: CompilerContext, - public options: CompilerOptions = { mergeInFieldsFromFragmentSpreads: true } - ) {} + constructor(public context: CompilerContext, public options: CompilerOptions = { mergeInFieldsFromFragmentSpreads: true }) {} transformIR(): LegacyCompilerContext { const operations: { [operationName: string]: LegacyOperation } = Object.create({}); @@ -111,11 +108,7 @@ class LegacyIRTransformer { const { filePath, operationType, rootType, variables, source, selectionSet } = operation; const fragmentsReferenced = collectFragmentsReferenced(selectionSet, this.context.fragments); - const { sourceWithFragments, operationId } = generateOperationId( - operation, - this.context.fragments, - fragmentsReferenced - ); + const { sourceWithFragments, operationId } = generateOperationId(operation, this.context.fragments, fragmentsReferenced); operations[operationName] = { filePath, @@ -127,7 +120,7 @@ class LegacyIRTransformer { ...this.transformSelectionSetToLegacyIR(selectionSet), fragmentsReferenced: Array.from(fragmentsReferenced), sourceWithFragments, - operationId + operationId, }; } @@ -139,7 +132,7 @@ class LegacyIRTransformer { typeCondition: type, possibleTypes: selectionSet.possibleTypes, ...fragmentWithoutSelectionSet, - ...this.transformSelectionSetToLegacyIR(selectionSet) + ...this.transformSelectionSetToLegacyIR(selectionSet), }; } @@ -148,7 +141,7 @@ class LegacyIRTransformer { operations, fragments, typesUsed: this.context.typesUsed, - options: this.options + options: this.options, }; return legacyContext; @@ -178,7 +171,7 @@ class LegacyIRTransformer { typeCondition: possibleType, possibleTypes: [possibleType], fields, - fragmentSpreads + fragmentSpreads, } as LegacyInlineFragment; }); }); @@ -194,7 +187,7 @@ class LegacyIRTransformer { return { fields, fragmentSpreads, - inlineFragments + inlineFragments, }; } @@ -207,7 +200,7 @@ class LegacyIRTransformer { return { kind, variableName, - inverted + inverted, }; }) : undefined; @@ -221,15 +214,12 @@ class LegacyIRTransformer { description, isDeprecated, deprecationReason, - ...selectionSet ? this.transformSelectionSetToLegacyIR(selectionSet) : {} + ...(selectionSet ? this.transformSelectionSetToLegacyIR(selectionSet) : {}), } as LegacyField; }); } - collectFragmentSpreads( - selectionSet: SelectionSet, - possibleTypes: GraphQLObjectType[] = selectionSet.possibleTypes - ): FragmentSpread[] { + collectFragmentSpreads(selectionSet: SelectionSet, possibleTypes: GraphQLObjectType[] = selectionSet.possibleTypes): FragmentSpread[] { const fragmentSpreads: FragmentSpread[] = []; for (const selection of selectionSet.selections) { diff --git a/packages/amplify-graphql-types-generator/src/compiler/visitors/collectAndMergeFields.ts b/packages/amplify-graphql-types-generator/src/compiler/visitors/collectAndMergeFields.ts index b6a854740a..de61c07c50 100644 --- a/packages/amplify-graphql-types-generator/src/compiler/visitors/collectAndMergeFields.ts +++ b/packages/amplify-graphql-types-generator/src/compiler/visitors/collectAndMergeFields.ts @@ -9,17 +9,10 @@ declare module '../' { } } -export function collectAndMergeFields( - selectionSet: SelectionSet, - mergeInFragmentSpreads: Boolean = true -): Field[] { +export function collectAndMergeFields(selectionSet: SelectionSet, mergeInFragmentSpreads: Boolean = true): Field[] { const groupedFields: Map = new Map(); - function visitSelectionSet( - selections: Selection[], - possibleTypes: GraphQLObjectType[], - conditions: BooleanCondition[] = [] - ) { + function visitSelectionSet(selections: Selection[], possibleTypes: GraphQLObjectType[], conditions: BooleanCondition[] = []) { if (possibleTypes.length < 1) return; for (const selection of selections) { @@ -39,9 +32,9 @@ export function collectAndMergeFields( selectionSet: selection.selectionSet ? { possibleTypes: selection.selectionSet.possibleTypes, - selections: [...selection.selectionSet.selections] + selections: [...selection.selectionSet.selections], } - : undefined + : undefined, }); break; case 'FragmentSpread': @@ -70,10 +63,7 @@ export function collectAndMergeFields( return fields .map(field => { if (isFieldIncludedUnconditionally && field.isConditional && field.selectionSet) { - field.selectionSet.selections = wrapInBooleanConditionsIfNeeded( - field.selectionSet.selections, - field.conditions - ); + field.selectionSet.selections = wrapInBooleanConditionsIfNeeded(field.selectionSet.selections, field.conditions); } return field; }) @@ -114,10 +104,7 @@ export function collectAndMergeFields( return fields; } -export function wrapInBooleanConditionsIfNeeded( - selections: Selection[], - conditions?: BooleanCondition[] -): Selection[] { +export function wrapInBooleanConditionsIfNeeded(selections: Selection[], conditions?: BooleanCondition[]): Selection[] { if (!conditions || conditions.length == 0) return selections; const [condition, ...rest] = conditions; @@ -126,8 +113,8 @@ export function wrapInBooleanConditionsIfNeeded( ...condition, selectionSet: { possibleTypes: condition.selectionSet.possibleTypes, - selections: wrapInBooleanConditionsIfNeeded(selections, rest) - } - } + selections: wrapInBooleanConditionsIfNeeded(selections, rest), + }, + }, ]; } diff --git a/packages/amplify-graphql-types-generator/src/compiler/visitors/generateOperationId.ts b/packages/amplify-graphql-types-generator/src/compiler/visitors/generateOperationId.ts index f7198340cf..1a76fcbc24 100644 --- a/packages/amplify-graphql-types-generator/src/compiler/visitors/generateOperationId.ts +++ b/packages/amplify-graphql-types-generator/src/compiler/visitors/generateOperationId.ts @@ -19,7 +19,7 @@ export function generateOperationId( throw new Error(`Cannot find fragment "${fragmentName}"`); } return fragment.source; - }) + }), ].join('\n'); const hash = createHash('sha256'); diff --git a/packages/amplify-graphql-types-generator/src/compiler/visitors/inlineRedundantTypeConditions.ts b/packages/amplify-graphql-types-generator/src/compiler/visitors/inlineRedundantTypeConditions.ts index 9f7ac9e2ae..a3a5331db2 100644 --- a/packages/amplify-graphql-types-generator/src/compiler/visitors/inlineRedundantTypeConditions.ts +++ b/packages/amplify-graphql-types-generator/src/compiler/visitors/inlineRedundantTypeConditions.ts @@ -16,6 +16,6 @@ export function inlineRedundantTypeConditions(selectionSet: SelectionSet): Selec return { possibleTypes: selectionSet.possibleTypes, - selections + selections, }; } diff --git a/packages/amplify-graphql-types-generator/src/compiler/visitors/typeCase.ts b/packages/amplify-graphql-types-generator/src/compiler/visitors/typeCase.ts index 94d6f9ad94..7284824a3c 100644 --- a/packages/amplify-graphql-types-generator/src/compiler/visitors/typeCase.ts +++ b/packages/amplify-graphql-types-generator/src/compiler/visitors/typeCase.ts @@ -17,16 +17,13 @@ export class Variant implements SelectionSet { } inspect() { - return `${inspect(this.possibleTypes)} -> ${inspect( - collectAndMergeFields(this, false).map(field => field.responseKey) - )} ${inspect(this.fragmentSpreads.map(fragmentSpread => fragmentSpread.fragmentName))}\n`; + return `${inspect(this.possibleTypes)} -> ${inspect(collectAndMergeFields(this, false).map(field => field.responseKey))} ${inspect( + this.fragmentSpreads.map(fragmentSpread => fragmentSpread.fragmentName) + )}\n`; } } -export function typeCaseForSelectionSet( - selectionSet: SelectionSet, - mergeInFragmentSpreads: boolean = true -): TypeCase { +export function typeCaseForSelectionSet(selectionSet: SelectionSet, mergeInFragmentSpreads: boolean = true): TypeCase { const typeCase = new TypeCase(selectionSet.possibleTypes); for (const selection of selectionSet.selections) { @@ -37,12 +34,7 @@ export function typeCaseForSelectionSet( } break; case 'FragmentSpread': - if ( - typeCase.default.fragmentSpreads.some( - fragmentSpread => fragmentSpread.fragmentName === selection.fragmentName - ) - ) - continue; + if (typeCase.default.fragmentSpreads.some(fragmentSpread => fragmentSpread.fragmentName === selection.fragmentName)) continue; for (const variant of typeCase.disjointVariantsFor(selectionSet.possibleTypes)) { variant.fragmentSpreads.push(selection); @@ -55,10 +47,8 @@ export function typeCaseForSelectionSet( typeCase.merge( typeCaseForSelectionSet( { - possibleTypes: selectionSet.possibleTypes.filter(type => - selection.selectionSet.possibleTypes.includes(type) - ), - selections: selection.selectionSet.selections + possibleTypes: selectionSet.possibleTypes.filter(type => selection.selectionSet.possibleTypes.includes(type)), + selections: selection.selectionSet.selections, }, mergeInFragmentSpreads ) @@ -69,25 +59,20 @@ export function typeCaseForSelectionSet( typeCase.merge( typeCaseForSelectionSet( { - possibleTypes: selectionSet.possibleTypes.filter(type => - selection.selectionSet.possibleTypes.includes(type) - ), - selections: selection.selectionSet.selections + possibleTypes: selectionSet.possibleTypes.filter(type => selection.selectionSet.possibleTypes.includes(type)), + selections: selection.selectionSet.selections, }, mergeInFragmentSpreads ) ); break; case 'BooleanCondition': - typeCase.merge( - typeCaseForSelectionSet(selection.selectionSet, mergeInFragmentSpreads), - selectionSet => [ - { - ...selection, - selectionSet - } - ] - ); + typeCase.merge(typeCaseForSelectionSet(selection.selectionSet, mergeInFragmentSpreads), selectionSet => [ + { + ...selection, + selectionSet, + }, + ]); break; } } @@ -197,10 +182,6 @@ export class TypeCase { } inspect() { - return ( - `TypeCase\n` + - ` default -> ${inspect(this.default)}\n` + - this.variants.map(variant => ` ${inspect(variant)}\n`).join('') - ); + return `TypeCase\n` + ` default -> ${inspect(this.default)}\n` + this.variants.map(variant => ` ${inspect(variant)}\n`).join(''); } } diff --git a/packages/amplify-graphql-types-generator/src/errors.ts b/packages/amplify-graphql-types-generator/src/errors.ts index a5ec6e231c..61af30115f 100644 --- a/packages/amplify-graphql-types-generator/src/errors.ts +++ b/packages/amplify-graphql-types-generator/src/errors.ts @@ -44,7 +44,12 @@ export function logErrorMessage(message: string, fileName?: string, lineNumber?: } } else { if (fileName) { - const truncatedFileName = '/' + fileName.split(path.sep).slice(-4).join(path.sep); + const truncatedFileName = + '/' + + fileName + .split(path.sep) + .slice(-4) + .join(path.sep); console.log(`...${truncatedFileName}: ${message}`); } else { console.log(`error: ${message}`); diff --git a/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/codeGeneration.ts b/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/codeGeneration.ts index 2c796015f2..ce56efc648 100644 --- a/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/codeGeneration.ts +++ b/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/codeGeneration.ts @@ -3,11 +3,7 @@ import { parse } from 'graphql'; import { loadSchema } from '../../loading'; const schema = loadSchema(require.resolve('../../../test/fixtures/starwars/schema.json')); -import { - compileToIR, - CompilerOptions, - CompilerContext, -} from '../../compiler'; +import { compileToIR, CompilerOptions, CompilerContext } from '../../compiler'; import { generateSource } from '../codeGeneration'; @@ -15,7 +11,7 @@ function compile( source: string, options: CompilerOptions = { mergeInFieldsFromFragmentSpreads: true, - addTypename: true + addTypename: true, } ): CompilerContext { const document = parse(source); @@ -50,16 +46,15 @@ describe('Flow codeGeneration', () => { } } `); - context.operations["HeroName"].filePath = '/some/file/ComponentA.js'; - context.operations["SomeOther"].filePath = '/some/file/ComponentB.js'; + context.operations['HeroName'].filePath = '/some/file/ComponentA.js'; + context.operations['SomeOther'].filePath = '/some/file/ComponentB.js'; context.fragments['someFragment'].filePath = '/some/file/ComponentB.js'; const output = generateSource(context); expect(output).toBeInstanceOf(Object); - Object.keys(output) - .forEach((filePath) => { - expect(filePath).toMatchSnapshot(); - expect(output[filePath]).toMatchSnapshot(); - }); + Object.keys(output).forEach(filePath => { + expect(filePath).toMatchSnapshot(); + expect(output[filePath]).toMatchSnapshot(); + }); }); test('simple hero query', () => { @@ -169,7 +164,7 @@ describe('Flow codeGeneration', () => { const output = generateSource(context); expect(output).toMatchSnapshot(); - }) + }); test('inline fragment on type conditions', () => { const context = compile(` diff --git a/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/helpers.ts b/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/helpers.ts index 6f8e757b3a..f524b9ba87 100644 --- a/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/helpers.ts +++ b/packages/amplify-graphql-types-generator/src/flow-modern/__tests__/helpers.ts @@ -14,375 +14,194 @@ import * as t from 'babel-types'; import { createTypeAnnotationFromGraphQLTypeFunction } from '../helpers'; const typeAnnotationFromGraphQLType = createTypeAnnotationFromGraphQLTypeFunction({ - passthroughCustomScalars: false + passthroughCustomScalars: false, }); describe('Flow typeAnnotationFromGraphQLType', () => { test('String', () => { - expect(typeAnnotationFromGraphQLType(GraphQLString)) - .toMatchObject( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ); + expect(typeAnnotationFromGraphQLType(GraphQLString)).toMatchObject(t.nullableTypeAnnotation(t.stringTypeAnnotation())); }); test('Int', () => { - expect(typeAnnotationFromGraphQLType(GraphQLInt)) - .toMatchObject( - t.nullableTypeAnnotation( - t.numberTypeAnnotation() - ) - ); + expect(typeAnnotationFromGraphQLType(GraphQLInt)).toMatchObject(t.nullableTypeAnnotation(t.numberTypeAnnotation())); }); test('Float', () => { - expect(typeAnnotationFromGraphQLType(GraphQLFloat)) - .toMatchObject( - t.nullableTypeAnnotation( - t.numberTypeAnnotation() - ) - ); + expect(typeAnnotationFromGraphQLType(GraphQLFloat)).toMatchObject(t.nullableTypeAnnotation(t.numberTypeAnnotation())); }); test('Boolean', () => { - expect(typeAnnotationFromGraphQLType(GraphQLBoolean)) - .toMatchObject( - t.nullableTypeAnnotation( - t.booleanTypeAnnotation() - ) - ); + expect(typeAnnotationFromGraphQLType(GraphQLBoolean)).toMatchObject(t.nullableTypeAnnotation(t.booleanTypeAnnotation())); }); test('ID', () => { - expect(typeAnnotationFromGraphQLType(GraphQLID)) - .toMatchObject( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ); + expect(typeAnnotationFromGraphQLType(GraphQLID)).toMatchObject(t.nullableTypeAnnotation(t.stringTypeAnnotation())); }); test('String!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLString)) - ).toMatchObject(t.stringTypeAnnotation()); + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLString))).toMatchObject(t.stringTypeAnnotation()); }); test('Int!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLInt)) - ).toMatchObject(t.numberTypeAnnotation()); + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLInt))).toMatchObject(t.numberTypeAnnotation()); }); test('Float!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLFloat)) - ).toMatchObject(t.numberTypeAnnotation()); + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLFloat))).toMatchObject(t.numberTypeAnnotation()); }); test('Boolean!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLBoolean)) - ).toMatchObject(t.booleanTypeAnnotation()); + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLBoolean))).toMatchObject(t.booleanTypeAnnotation()); }); test('ID!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLID)) - ).toMatchObject(t.stringTypeAnnotation()); + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(GraphQLID))).toMatchObject(t.stringTypeAnnotation()); }); // TODO: Test GenericTypeAnnotation test('[String]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(GraphQLString)) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(GraphQLString))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.stringTypeAnnotation()))) + ); }); test('[Int]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(GraphQLInt)) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.numberTypeAnnotation() - ) - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(GraphQLInt))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.numberTypeAnnotation()))) + ); }); test('[Float]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(GraphQLFloat)) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.numberTypeAnnotation() - ) - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(GraphQLFloat))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.numberTypeAnnotation()))) + ); }); test('[Boolean]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(GraphQLBoolean)) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.booleanTypeAnnotation() - ) - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(GraphQLBoolean))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.booleanTypeAnnotation()))) + ); }); test('[ID]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(GraphQLID)) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) - ) - }) + expect(typeAnnotationFromGraphQLType(new GraphQLList(GraphQLID))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.stringTypeAnnotation()))) + ); + }); test('[String]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLString))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLString)))).toMatchObject( + t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.stringTypeAnnotation())) + ); }); test('[Int]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLInt))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.numberTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLInt)))).toMatchObject( + t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.numberTypeAnnotation())) + ); }); test('[Float]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLFloat))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.numberTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLFloat)))).toMatchObject( + t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.numberTypeAnnotation())) + ); }); test('[Boolean]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLBoolean))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.booleanTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLBoolean)))).toMatchObject( + t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.booleanTypeAnnotation())) + ); }); test('[ID]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLID))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLID)))).toMatchObject( + t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.stringTypeAnnotation())) + ); }); test('[String!]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLString))) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLString)))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.stringTypeAnnotation())) + ); }); test('[Int!]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull((GraphQLInt)))) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.numberTypeAnnotation() - ) - ) - ) - }) + expect(typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLInt)))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.numberTypeAnnotation())) + ); + }); test('[Float!]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLFloat))) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.numberTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLFloat)))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.numberTypeAnnotation())) + ); }); test('[Boolean!]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLBoolean))) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.booleanTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLBoolean)))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.booleanTypeAnnotation())) + ); }); test('[ID!]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLID))) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLID)))).toMatchObject( + t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.stringTypeAnnotation())) + ); }); test('[String!]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString)))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.stringTypeAnnotation() - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))))).toMatchObject( + t.arrayTypeAnnotation(t.stringTypeAnnotation()) + ); }); test('[Int!]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLInt)))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.numberTypeAnnotation() - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLInt))))).toMatchObject( + t.arrayTypeAnnotation(t.numberTypeAnnotation()) + ); }); test('[Float!]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLFloat)))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.numberTypeAnnotation() - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLFloat))))).toMatchObject( + t.arrayTypeAnnotation(t.numberTypeAnnotation()) + ); }); test('[Boolean!]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLBoolean)))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.booleanTypeAnnotation() - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLBoolean))))).toMatchObject( + t.arrayTypeAnnotation(t.booleanTypeAnnotation()) + ); }); test('[ID!]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID)))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.stringTypeAnnotation() - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))))).toMatchObject( + t.arrayTypeAnnotation(t.stringTypeAnnotation()) + ); }); test('[[String]]', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLList(GraphQLString))) - ).toMatchObject( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLList(new GraphQLList(GraphQLString)))).toMatchObject( + t.nullableTypeAnnotation( + t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.stringTypeAnnotation())))) ) + ); }); test('[[String]]!', () => { - expect( - typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLList(GraphQLString)))) - ).toMatchObject( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.arrayTypeAnnotation( - t.nullableTypeAnnotation( - t.stringTypeAnnotation() - ) - ) - ) - ) - ) + expect(typeAnnotationFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLList(GraphQLString))))).toMatchObject( + t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.arrayTypeAnnotation(t.nullableTypeAnnotation(t.stringTypeAnnotation())))) + ); }); test('Custom Scalar', () => { const OddType = new GraphQLScalarType({ name: 'Odd', serialize(value) { - return value % 2 === 1 ? value : null - } + return value % 2 === 1 ? value : null; + }, }); - expect( - typeAnnotationFromGraphQLType(OddType) - ).toMatchObject( - t.nullableTypeAnnotation( - t.genericTypeAnnotation(t.identifier('Odd')) - ) - ) + expect(typeAnnotationFromGraphQLType(OddType)).toMatchObject(t.nullableTypeAnnotation(t.genericTypeAnnotation(t.identifier('Odd')))); }); }); @@ -391,7 +210,7 @@ describe('passthrough custom scalars', () => { beforeAll(() => { getTypeAnnotation = createTypeAnnotationFromGraphQLTypeFunction({ - passthroughCustomScalars: true + passthroughCustomScalars: true, }); }); @@ -399,16 +218,10 @@ describe('passthrough custom scalars', () => { const OddType = new GraphQLScalarType({ name: 'Odd', serialize(value) { - return value % 2 === 1 ? value : null - } + return value % 2 === 1 ? value : null; + }, }); - expect( - getTypeAnnotation(OddType) - ).toMatchObject( - t.nullableTypeAnnotation( - t.anyTypeAnnotation() - ) - ) + expect(getTypeAnnotation(OddType)).toMatchObject(t.nullableTypeAnnotation(t.anyTypeAnnotation())); }); }); diff --git a/packages/amplify-graphql-types-generator/src/flow-modern/codeGeneration.ts b/packages/amplify-graphql-types-generator/src/flow-modern/codeGeneration.ts index 8e73fd4387..2ff46befdf 100644 --- a/packages/amplify-graphql-types-generator/src/flow-modern/codeGeneration.ts +++ b/packages/amplify-graphql-types-generator/src/flow-modern/codeGeneration.ts @@ -1,31 +1,16 @@ import * as t from '@babel/types'; import { stripIndent } from 'common-tags'; -import { - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLNonNull, -} from 'graphql'; +import { GraphQLEnumType, GraphQLInputObjectType, GraphQLNonNull } from 'graphql'; import * as path from 'path'; -import { - CompilerContext, - Operation, - Fragment, - SelectionSet, - Field, -} from '../compiler'; +import { CompilerContext, Operation, Fragment, SelectionSet, Field } from '../compiler'; -import { - typeCaseForSelectionSet, - Variant -} from '../compiler/visitors/typeCase'; +import { typeCaseForSelectionSet, Variant } from '../compiler/visitors/typeCase'; -import { - collectAndMergeFields -} from '../compiler/visitors/collectAndMergeFields'; +import { collectAndMergeFields } from '../compiler/visitors/collectAndMergeFields'; import { BasicGeneratedFile } from '../utilities/CodeGenerator'; -import FlowGenerator, { ObjectProperty, FlowCompilerOptions, } from './language'; +import FlowGenerator, { ObjectProperty, FlowCompilerOptions } from './language'; import Printer from './printer'; class FlowGeneratedFile implements BasicGeneratedFile { @@ -35,7 +20,7 @@ class FlowGeneratedFile implements BasicGeneratedFile { this.fileContents = fileContents; } get output() { - return this.fileContents + return this.fileContents; } } @@ -50,14 +35,14 @@ function printEnumsAndInputObjects(generator: FlowAPIGenerator, context: Compile `); context.typesUsed - .filter(type => (type instanceof GraphQLEnumType)) - .forEach((enumType) => { + .filter(type => type instanceof GraphQLEnumType) + .forEach(enumType => { generator.typeAliasForEnumType(enumType as GraphQLEnumType); }); context.typesUsed .filter(type => type instanceof GraphQLInputObjectType) - .forEach((inputObjectType) => { + .forEach(inputObjectType => { generator.typeAliasForInputObjectType(inputObjectType as GraphQLInputObjectType); }); @@ -65,56 +50,44 @@ function printEnumsAndInputObjects(generator: FlowAPIGenerator, context: Compile //============================================================== // END Enums and Input Objects //============================================================== - `) + `); } -export function generateSource( - context: CompilerContext, -) { +export function generateSource(context: CompilerContext) { const generator = new FlowAPIGenerator(context); const generatedFiles: { [filePath: string]: FlowGeneratedFile } = {}; - Object.values(context.operations) - .forEach((operation) => { - generator.fileHeader(); - generator.typeAliasesForOperation(operation); - printEnumsAndInputObjects(generator, context); + Object.values(context.operations).forEach(operation => { + generator.fileHeader(); + generator.typeAliasesForOperation(operation); + printEnumsAndInputObjects(generator, context); - const output = generator.printer.printAndClear(); + const output = generator.printer.printAndClear(); - const outputFilePath = path.join( - path.dirname(operation.filePath), - '__generated__', - `${operation.operationName}.js` - ); + const outputFilePath = path.join(path.dirname(operation.filePath), '__generated__', `${operation.operationName}.js`); - generatedFiles[outputFilePath] = new FlowGeneratedFile(output); - }); + generatedFiles[outputFilePath] = new FlowGeneratedFile(output); + }); - Object.values(context.fragments) - .forEach((fragment) => { - generator.fileHeader(); - generator.typeAliasesForFragment(fragment); - printEnumsAndInputObjects(generator, context); + Object.values(context.fragments).forEach(fragment => { + generator.fileHeader(); + generator.typeAliasesForFragment(fragment); + printEnumsAndInputObjects(generator, context); - const output = generator.printer.printAndClear(); + const output = generator.printer.printAndClear(); - const outputFilePath = path.join( - path.dirname(fragment.filePath), - '__generated__', - `${fragment.fragmentName}.js` - ); + const outputFilePath = path.join(path.dirname(fragment.filePath), '__generated__', `${fragment.fragmentName}.js`); - generatedFiles[outputFilePath] = new FlowGeneratedFile(output); - }); + generatedFiles[outputFilePath] = new FlowGeneratedFile(output); + }); return generatedFiles; } export class FlowAPIGenerator extends FlowGenerator { - context: CompilerContext - printer: Printer - scopeStack: string[] + context: CompilerContext; + printer: Printer; + scopeStack: string[]; constructor(context: CompilerContext) { super(context.options as FlowCompilerOptions); @@ -142,11 +115,7 @@ export class FlowAPIGenerator extends FlowGenerator { } public typeAliasesForOperation(operation: Operation) { - const { - operationType, - operationName, - selectionSet - } = operation; + const { operationType, operationName, selectionSet } = operation; this.scopeStackPush(operationName); @@ -154,7 +123,7 @@ export class FlowAPIGenerator extends FlowGenerator { // ==================================================== // GraphQL ${operationType} operation: ${operationName} // ==================================================== - `) + `); // The root operation only has one variant // Do we need to get exhaustive variants anyway? @@ -163,19 +132,14 @@ export class FlowAPIGenerator extends FlowGenerator { const variant = variants[0]; const properties = this.getPropertiesForVariant(variant); - const exportedTypeAlias = this.exportDeclaration( - this.typeAliasObject(operationName, properties) - ); + const exportedTypeAlias = this.exportDeclaration(this.typeAliasObject(operationName, properties)); this.printer.enqueue(exportedTypeAlias); this.scopeStackPop(); } public typeAliasesForFragment(fragment: Fragment) { - const { - fragmentName, - selectionSet - } = fragment; + const { fragmentName, selectionSet } = fragment; this.scopeStackPush(fragmentName); @@ -191,12 +155,7 @@ export class FlowAPIGenerator extends FlowGenerator { const properties = this.getPropertiesForVariant(variants[0]); const name = this.annotationFromScopeStack(this.scopeStack).id.name; - const exportedTypeAlias = this.exportDeclaration( - this.typeAliasObject( - name, - properties - ) - ); + const exportedTypeAlias = this.exportDeclaration(this.typeAliasObject(name, properties)); this.printer.enqueue(exportedTypeAlias); } else { @@ -206,12 +165,7 @@ export class FlowAPIGenerator extends FlowGenerator { const properties = this.getPropertiesForVariant(variant); const name = this.annotationFromScopeStack(this.scopeStack).id.name; - const exportedTypeAlias = this.exportDeclaration( - this.typeAliasObject( - name, - properties - ) - ); + const exportedTypeAlias = this.exportDeclaration(this.typeAliasObject(name, properties)); this.printer.enqueue(exportedTypeAlias); @@ -221,12 +175,7 @@ export class FlowAPIGenerator extends FlowGenerator { }); this.printer.enqueue( - this.exportDeclaration( - this.typeAliasGenericUnion( - this.annotationFromScopeStack(this.scopeStack).id.name, - unionMembers - ) - ) + this.exportDeclaration(this.typeAliasGenericUnion(this.annotationFromScopeStack(this.scopeStack).id.name, unionMembers)) ); } @@ -238,17 +187,11 @@ export class FlowAPIGenerator extends FlowGenerator { } private getTypeCasesForSelectionSet(selectionSet: SelectionSet) { - return typeCaseForSelectionSet( - selectionSet, - this.context.options.mergeInFieldsFromFragmentSpreads - ); + return typeCaseForSelectionSet(selectionSet, this.context.options.mergeInFieldsFromFragmentSpreads); } private getPropertiesForVariant(variant: Variant): ObjectProperty[] { - const fields = collectAndMergeFields( - variant, - this.context.options.mergeInFieldsFromFragmentSpreads - ); + const fields = collectAndMergeFields(variant, this.context.options.mergeInFieldsFromFragmentSpreads); return fields.map(field => { const fieldName = field.alias !== undefined ? field.alias : field.name; @@ -263,15 +206,9 @@ export class FlowAPIGenerator extends FlowGenerator { genericAnnotation.id.name = '?' + genericAnnotation.id.name; } - res = this.handleFieldSelectionSetValue( - genericAnnotation, - field - ); + res = this.handleFieldSelectionSetValue(genericAnnotation, field); } else { - res = this.handleFieldValue( - field, - variant - ); + res = this.handleFieldValue(field, variant); } this.scopeStackPop(); @@ -289,26 +226,16 @@ export class FlowAPIGenerator extends FlowGenerator { if (variants.length === 1) { const variant = variants[0]; const properties = this.getPropertiesForVariant(variant); - exportedTypeAlias = this.exportDeclaration( - this.typeAliasObject( - this.annotationFromScopeStack(this.scopeStack).id.name, - properties - ) - ); + exportedTypeAlias = this.exportDeclaration(this.typeAliasObject(this.annotationFromScopeStack(this.scopeStack).id.name, properties)); } else { const propertySets = variants.map(variant => { - this.scopeStackPush(variant.possibleTypes[0].toString()) + this.scopeStackPush(variant.possibleTypes[0].toString()); const properties = this.getPropertiesForVariant(variant); this.scopeStackPop(); return properties; - }) + }); - exportedTypeAlias = this.exportDeclaration( - this.typeAliasObjectUnion( - genericAnnotation.id.name, - propertySets - ) - ); + exportedTypeAlias = this.exportDeclaration(this.typeAliasObjectUnion(genericAnnotation.id.name, propertySets)); } this.printer.enqueue(exportedTypeAlias); @@ -316,31 +243,30 @@ export class FlowAPIGenerator extends FlowGenerator { return { name: field.alias ? field.alias : field.name, description: field.description, - annotation: genericAnnotation + annotation: genericAnnotation, }; } private handleFieldValue(field: Field, variant: Variant) { let res; if (field.name === '__typename') { - const annotations = variant.possibleTypes - .map(type => { - const annotation = t.stringLiteralTypeAnnotation(); - annotation.value = type.toString(); - return annotation; - }); + const annotations = variant.possibleTypes.map(type => { + const annotation = t.stringLiteralTypeAnnotation(); + annotation.value = type.toString(); + return annotation; + }); res = { name: field.alias ? field.alias : field.name, description: field.description, - annotation: t.unionTypeAnnotation(annotations) + annotation: t.unionTypeAnnotation(annotations), }; } else { // TODO: Double check that this works res = { name: field.alias ? field.alias : field.name, description: field.description, - annotation: this.typeAnnotationFromGraphQLType(field.type) + annotation: this.typeAnnotationFromGraphQLType(field.type), }; } @@ -356,8 +282,7 @@ export class FlowAPIGenerator extends FlowGenerator { } scopeStackPop() { - const popped = this.scopeStack.pop() + const popped = this.scopeStack.pop(); return popped; } - } diff --git a/packages/amplify-graphql-types-generator/src/flow-modern/helpers.ts b/packages/amplify-graphql-types-generator/src/flow-modern/helpers.ts index c62f4bbef9..12e5e8df0f 100644 --- a/packages/amplify-graphql-types-generator/src/flow-modern/helpers.ts +++ b/packages/amplify-graphql-types-generator/src/flow-modern/helpers.ts @@ -8,7 +8,7 @@ import { GraphQLType, isNonNullType, isListType, -} from 'graphql' +} from 'graphql'; import * as t from 'babel-types'; @@ -20,27 +20,21 @@ const builtInScalarMap = { [GraphQLFloat.name]: t.numberTypeAnnotation(), [GraphQLBoolean.name]: t.booleanTypeAnnotation(), [GraphQLID.name]: t.stringTypeAnnotation(), -} +}; -export function createTypeAnnotationFromGraphQLTypeFunction( - compilerOptions: CompilerOptions -): Function { - return function typeAnnotationFromGraphQLType(type: GraphQLType, { - nullable - }: { nullable: boolean } = { - nullable: true - }): t.FlowTypeAnnotation { +export function createTypeAnnotationFromGraphQLTypeFunction(compilerOptions: CompilerOptions): Function { + return function typeAnnotationFromGraphQLType( + type: GraphQLType, + { nullable }: { nullable: boolean } = { + nullable: true, + } + ): t.FlowTypeAnnotation { if (isNonNullType(type)) { - return typeAnnotationFromGraphQLType( - type.ofType, - { nullable: false } - ); + return typeAnnotationFromGraphQLType(type.ofType, { nullable: false }); } if (isListType(type)) { - const typeAnnotation = t.arrayTypeAnnotation( - typeAnnotationFromGraphQLType(type.ofType) - ); + const typeAnnotation = t.arrayTypeAnnotation(typeAnnotationFromGraphQLType(type.ofType)); if (nullable) { return t.nullableTypeAnnotation(typeAnnotation); @@ -51,22 +45,18 @@ export function createTypeAnnotationFromGraphQLTypeFunction( let typeAnnotation; if (type instanceof GraphQLScalarType) { - const builtIn = builtInScalarMap[type.name] + const builtIn = builtInScalarMap[type.name]; if (builtIn) { typeAnnotation = builtIn; } else { if (compilerOptions.passthroughCustomScalars) { typeAnnotation = t.anyTypeAnnotation(); } else { - typeAnnotation = t.genericTypeAnnotation( - t.identifier(type.name) - ); + typeAnnotation = t.genericTypeAnnotation(t.identifier(type.name)); } } } else { - typeAnnotation = t.genericTypeAnnotation( - t.identifier(type.name) - ); + typeAnnotation = t.genericTypeAnnotation(t.identifier(type.name)); } if (nullable) { @@ -74,5 +64,5 @@ export function createTypeAnnotationFromGraphQLTypeFunction( } else { return typeAnnotation; } - } + }; } diff --git a/packages/amplify-graphql-types-generator/src/flow-modern/language.ts b/packages/amplify-graphql-types-generator/src/flow-modern/language.ts index d2672073cb..729e011440 100644 --- a/packages/amplify-graphql-types-generator/src/flow-modern/language.ts +++ b/packages/amplify-graphql-types-generator/src/flow-modern/language.ts @@ -1,29 +1,24 @@ -import { - GraphQLEnumType, - GraphQLInputObjectType, -} from 'graphql'; +import { GraphQLEnumType, GraphQLInputObjectType } from 'graphql'; -import { - CompilerOptions -} from '../compiler'; +import { CompilerOptions } from '../compiler'; import { createTypeAnnotationFromGraphQLTypeFunction } from './helpers'; import * as t from '@babel/types'; export type ObjectProperty = { - name: string, - description?: string | null | undefined, - annotation: t.FlowTypeAnnotation -} + name: string; + description?: string | null | undefined; + annotation: t.FlowTypeAnnotation; +}; export interface FlowCompilerOptions extends CompilerOptions { - useFlowExactObjects: boolean + useFlowExactObjects: boolean; } export default class FlowGenerator { - options: FlowCompilerOptions - typeAnnotationFromGraphQLType: Function + options: FlowCompilerOptions; + typeAnnotationFromGraphQLType: Function; constructor(compilerOptions: FlowCompilerOptions) { this.options = compilerOptions; @@ -40,19 +35,14 @@ export default class FlowGenerator { return type; }); - const typeAlias = t.exportNamedDeclaration( - t.typeAlias( - t.identifier(name), - undefined, - t.unionTypeAnnotation(unionValues) - ), - [] - ); + const typeAlias = t.exportNamedDeclaration(t.typeAlias(t.identifier(name), undefined, t.unionTypeAnnotation(unionValues)), []); - typeAlias.leadingComments = [{ - type: 'CommentLine', - value: ` ${description}` - } as t.CommentLine]; + typeAlias.leadingComments = [ + { + type: 'CommentLine', + value: ` ${description}`, + } as t.CommentLine, + ]; return typeAlias; } @@ -61,48 +51,49 @@ export default class FlowGenerator { const { name, description } = inputObjectType; const fieldMap = inputObjectType.getFields(); - const fields: ObjectProperty[] = Object.keys(inputObjectType.getFields()) - .map((fieldName: string) => { - const field = fieldMap[fieldName]; - return { - name: fieldName, - annotation: this.typeAnnotationFromGraphQLType(field.type) - } - }); + const fields: ObjectProperty[] = Object.keys(inputObjectType.getFields()).map((fieldName: string) => { + const field = fieldMap[fieldName]; + return { + name: fieldName, + annotation: this.typeAnnotationFromGraphQLType(field.type), + }; + }); const typeAlias = this.typeAliasObject(name, fields); - typeAlias.leadingComments = [{ - type: 'CommentLine', - value: ` ${description}` - } as t.CommentLine] + typeAlias.leadingComments = [ + { + type: 'CommentLine', + value: ` ${description}`, + } as t.CommentLine, + ]; return typeAlias; } public objectTypeAnnotation(fields: ObjectProperty[], isInputObject: boolean = false) { const objectTypeAnnotation = t.objectTypeAnnotation( - fields.map(({name, description, annotation}) => { - if (annotation.type === "NullableTypeAnnotation") { - t.identifier(name + '?') + fields.map(({ name, description, annotation }) => { + if (annotation.type === 'NullableTypeAnnotation') { + t.identifier(name + '?'); } const objectTypeProperty = t.objectTypeProperty( t.identifier( // Nullable fields on input objects do not have to be defined // as well, so allow these fields to be "undefined" - (isInputObject && annotation.type === "NullableTypeAnnotation") - ? name + '?' - : name + isInputObject && annotation.type === 'NullableTypeAnnotation' ? name + '?' : name ), annotation ); if (description) { - objectTypeProperty.trailingComments = [{ - type: 'CommentLine', - value: ` ${description}` - } as t.CommentLine] + objectTypeProperty.trailingComments = [ + { + type: 'CommentLine', + value: ` ${description}`, + } as t.CommentLine, + ]; } return objectTypeProperty; @@ -117,11 +108,7 @@ export default class FlowGenerator { } public typeAliasObject(name: string, fields: ObjectProperty[]) { - return t.typeAlias( - t.identifier(name), - undefined, - this.objectTypeAnnotation(fields) - ); + return t.typeAlias(t.identifier(name), undefined, this.objectTypeAnnotation(fields)); } public typeAliasObjectUnion(name: string, members: ObjectProperty[][]) { @@ -130,18 +117,14 @@ export default class FlowGenerator { undefined, t.unionTypeAnnotation( members.map(member => { - return this.objectTypeAnnotation(member) + return this.objectTypeAnnotation(member); }) ) - ) + ); } public typeAliasGenericUnion(name: string, members: t.FlowTypeAnnotation[]) { - return t.typeAlias( - t.identifier(name), - undefined, - t.unionTypeAnnotation(members) - ); + return t.typeAlias(t.identifier(name), undefined, t.unionTypeAnnotation(members)); } public exportDeclaration(declaration: t.Declaration) { @@ -149,10 +132,6 @@ export default class FlowGenerator { } public annotationFromScopeStack(scope: string[]) { - return t.genericTypeAnnotation( - t.identifier( - scope.join('_') - ) - ); + return t.genericTypeAnnotation(t.identifier(scope.join('_'))); } } diff --git a/packages/amplify-graphql-types-generator/src/flow-modern/printer.ts b/packages/amplify-graphql-types-generator/src/flow-modern/printer.ts index 0da25775a2..9308e32162 100644 --- a/packages/amplify-graphql-types-generator/src/flow-modern/printer.ts +++ b/packages/amplify-graphql-types-generator/src/flow-modern/printer.ts @@ -6,30 +6,21 @@ import { stripIndent } from 'common-tags'; type Printable = t.Node | string; export default class Printer { - private printQueue: Printable[] = [] + private printQueue: Printable[] = []; public print(): string { - return this.printQueue - .reduce( - (document: string, printable) => { - if (typeof printable === 'string') { - return document + printable; - } else { - const documentPart = generate(printable).code; - return document + this.fixCommas(documentPart); - } - }, - '' - ) as string; + return this.printQueue.reduce((document: string, printable) => { + if (typeof printable === 'string') { + return document + printable; + } else { + const documentPart = generate(printable).code; + return document + this.fixCommas(documentPart); + } + }, '') as string; } public enqueue(printable: Printable) { - this.printQueue = [ - ...this.printQueue, - '\n', - '\n', - printable - ]; + this.printQueue = [...this.printQueue, '\n', '\n', printable]; } public printAndClear() { @@ -78,37 +69,33 @@ export default class Printer { const [contents, comment] = currentLineContents.split('//'); newDocumentParts.push({ main: contents.replace(/\s+$/g, '') + ',', - comment: comment.trim() + comment: comment.trim(), }); currentLine++; } else { newDocumentParts.push({ main: lines[currentLine], - comment: null + comment: null, }); } currentLine++; } - return newDocumentParts.reduce((memo: string[], part) => { - const { - main, - comment - } = part; + return newDocumentParts + .reduce((memo: string[], part) => { + const { main, comment } = part; - let line; - if (comment !== null) { - const spacesBetween = maxCommentColumn - main.length; - line = `${main}${' '.repeat(spacesBetween)} // ${comment}` - } else { - line = main; - } + let line; + if (comment !== null) { + const spacesBetween = maxCommentColumn - main.length; + line = `${main}${' '.repeat(spacesBetween)} // ${comment}`; + } else { + line = main; + } - return [ - ...memo, - line - ]; - }, []).join('\n'); + return [...memo, line]; + }, []) + .join('\n'); } } diff --git a/packages/amplify-graphql-types-generator/src/flow-modern/types/augment-babel-types.ts b/packages/amplify-graphql-types-generator/src/flow-modern/types/augment-babel-types.ts index f6cc11c0cf..6538cf8dff 100644 --- a/packages/amplify-graphql-types-generator/src/flow-modern/types/augment-babel-types.ts +++ b/packages/amplify-graphql-types-generator/src/flow-modern/types/augment-babel-types.ts @@ -5,10 +5,10 @@ import 'babel-types'; declare module 'babel-types' { interface StringLiteralTypeAnnotation { - value: string + value: string; } interface ObjectTypeAnnotation { - exact: boolean + exact: boolean; } } diff --git a/packages/amplify-graphql-types-generator/src/flow/codeGeneration.js b/packages/amplify-graphql-types-generator/src/flow/codeGeneration.js index 2c11f6fd51..1e8c239ff7 100644 --- a/packages/amplify-graphql-types-generator/src/flow/codeGeneration.js +++ b/packages/amplify-graphql-types-generator/src/flow/codeGeneration.js @@ -11,29 +11,20 @@ import { GraphQLID, GraphQLInputObjectType, GraphQLObjectType, - GraphQLUnionType -} from 'graphql' + GraphQLUnionType, +} from 'graphql'; -import { isTypeProperSuperTypeOf } from '../utilities/graphql'; +import { isTypeProperSuperTypeOf } from '../utilities/graphql'; import * as Inflector from 'inflected'; -import { - join, - wrap, -} from '../utilities/printing'; +import { join, wrap } from '../utilities/printing'; import CodeGenerator from '../utilities/CodeGenerator'; -import { - typeDeclaration, - propertyDeclaration, - propertySetsDeclaration -} from './language'; +import { typeDeclaration, propertyDeclaration, propertySetsDeclaration } from './language'; -import { - typeNameFromGraphQLType, -} from './types'; +import { typeNameFromGraphQLType } from './types'; export function generateSource(context) { const generator = new CodeGenerator(context); @@ -41,15 +32,13 @@ export function generateSource(context) { generator.printOnNewline('/* @flow */'); generator.printOnNewline('/* eslint-disable */'); generator.printOnNewline('// This file was automatically generated and should not be edited.'); - typeDeclarationForGraphQLType(context.typesUsed.forEach(type => - typeDeclarationForGraphQLType(generator, type) - )); + typeDeclarationForGraphQLType(context.typesUsed.forEach(type => typeDeclarationForGraphQLType(generator, type))); Object.values(context.operations).forEach(operation => { interfaceVariablesDeclarationForOperation(generator, operation); typeDeclarationForOperation(generator, operation); }); Object.values(context.fragments).forEach(fragment => { - typeDeclarationForFragment(generator, fragment) + typeDeclarationForFragment(generator, fragment); }); return generator.output; @@ -69,43 +58,42 @@ function enumerationDeclaration(generator, type) { generator.printNewlineIfNeeded(); if (description) { - description.split('\n') - .forEach(line => { - generator.printOnNewline(`// ${line.trim()}`); - }) + description.split('\n').forEach(line => { + generator.printOnNewline(`// ${line.trim()}`); + }); } generator.printOnNewline(`export type ${name} =`); const nValues = values.length; values.forEach((value, i) => { if (!value.description || value.description.indexOf('\n') === -1) { - generator.printOnNewline(` "${value.value}"${i === nValues - 1 ? ';' : ' |'}${wrap(' // ', value.description)}`) + generator.printOnNewline(` "${value.value}"${i === nValues - 1 ? ';' : ' |'}${wrap(' // ', value.description)}`); } else { if (value.description) { - value.description.split('\n') - .forEach(line => { - generator.printOnNewline(` // ${line.trim()}`); - }) + value.description.split('\n').forEach(line => { + generator.printOnNewline(` // ${line.trim()}`); + }); } - generator.printOnNewline(` "${value.value}"${i === nValues - 1 ? ';' : ' |'}`) + generator.printOnNewline(` "${value.value}"${i === nValues - 1 ? ';' : ' |'}`); } }); generator.printNewline(); } -function structDeclarationForInputObjectType( - generator, - type -) { +function structDeclarationForInputObjectType(generator, type) { const interfaceName = type.name; - typeDeclaration(generator, { - interfaceName, - }, () => { - const properties = propertiesFromFields(generator.context, Object.values(type.getFields())); - propertyDeclarations(generator, properties, true); - }); + typeDeclaration( + generator, + { + interfaceName, + }, + () => { + const properties = propertiesFromFields(generator.context, Object.values(type.getFields())); + propertyDeclarations(generator, properties, true); + } + ); } -function interfaceNameFromOperation({operationName, operationType}) { +function interfaceNameFromOperation({ operationName, operationType }) { switch (operationType) { case 'query': return `${operationName}Query`; @@ -123,26 +111,23 @@ function interfaceNameFromOperation({operationName, operationType}) { export function interfaceVariablesDeclarationForOperation( generator, - { - operationName, - operationType, - variables, - fields, - fragmentsReferenced, - source, - } + { operationName, operationType, variables, fields, fragmentsReferenced, source } ) { if (!variables || variables.length < 1) { return null; } - const interfaceName = `${interfaceNameFromOperation({operationName, operationType})}Variables`; - - typeDeclaration(generator, { - interfaceName, - }, () => { - const properties = propertiesFromFields(generator.context, variables); - propertyDeclarations(generator, properties, true); - }); + const interfaceName = `${interfaceNameFromOperation({ operationName, operationType })}Variables`; + + typeDeclaration( + generator, + { + interfaceName, + }, + () => { + const properties = propertiesFromFields(generator.context, variables); + propertyDeclarations(generator, properties, true); + } + ); } function getObjectTypeName(type) { @@ -156,76 +141,69 @@ function getObjectTypeName(type) { return `"${type.name}"`; } if (type instanceof GraphQLUnionType) { - return type.getTypes().map(type => getObjectTypeName(type)).join(" | "); + return type + .getTypes() + .map(type => getObjectTypeName(type)) + .join(' | '); } return `"${type.name}"`; } export function typeDeclarationForOperation( generator, - { - operationName, - operationType, - variables, - fields, - fragmentSpreads, - fragmentsReferenced, - source, - } + { operationName, operationType, variables, fields, fragmentSpreads, fragmentsReferenced, source } ) { - const interfaceName = interfaceNameFromOperation({operationName, operationType}); + const interfaceName = interfaceNameFromOperation({ operationName, operationType }); fields = fields.map(rootField => { - const fields = rootField.fields && rootField.fields.map(field => { - if (field.fieldName === '__typename') { - const objectTypeName = getObjectTypeName(rootField.type); - return { - ...field, - typeName: objectTypeName, - type: { name: objectTypeName }, - }; - } - return field; - }); + const fields = + rootField.fields && + rootField.fields.map(field => { + if (field.fieldName === '__typename') { + const objectTypeName = getObjectTypeName(rootField.type); + return { + ...field, + typeName: objectTypeName, + type: { name: objectTypeName }, + }; + } + return field; + }); return { ...rootField, fields, }; }); const properties = propertiesFromFields(generator.context, fields); - typeDeclaration(generator, { - interfaceName, - }, () => { - propertyDeclarations(generator, properties); - }); + typeDeclaration( + generator, + { + interfaceName, + }, + () => { + propertyDeclarations(generator, properties); + } + ); } -export function typeDeclarationForFragment( - generator, - fragment -) { - const { - fragmentName, - typeCondition, - fields, - inlineFragments, - fragmentSpreads, - source, - } = fragment; +export function typeDeclarationForFragment(generator, fragment) { + const { fragmentName, typeCondition, fields, inlineFragments, fragmentSpreads, source } = fragment; const interfaceName = `${fragmentName}Fragment`; - typeDeclaration(generator, { - interfaceName, - noBrackets: isAbstractType(typeCondition) - }, () => { - if (isAbstractType(typeCondition)) { - const propertySets = fragment.possibleTypes - .map(type => { + typeDeclaration( + generator, + { + interfaceName, + noBrackets: isAbstractType(typeCondition), + }, + () => { + if (isAbstractType(typeCondition)) { + const propertySets = fragment.possibleTypes.map(type => { // NOTE: inlineFragment currently consists of the merged fields // from both inline fragments and fragment spreads. // TODO: Rename inlineFragments in the IR. const inlineFragment = inlineFragments.find(inlineFragment => { - return inlineFragment.typeCondition.toString() == type + return inlineFragment.typeCondition.toString() == type; }); if (inlineFragment) { @@ -234,8 +212,8 @@ export function typeDeclarationForFragment( return { ...field, typeName: `"${inlineFragment.typeCondition}"`, - type: { name: `"${inlineFragment.typeCondition}"` } - } + type: { name: `"${inlineFragment.typeCondition}"` }, + }; } else { return field; } @@ -248,8 +226,8 @@ export function typeDeclarationForFragment( return { ...field, typeName: `"${type}"`, - type: { name: `"${type}"` } - } + type: { name: `"${type}"` }, + }; } else { return field; } @@ -259,23 +237,24 @@ export function typeDeclarationForFragment( } }); - propertySetsDeclaration(generator, fragment, propertySets, true); - } else { - const fragmentFields = fields.map(field => { - if (field.fieldName === '__typename') { - return { - ...field, - typeName: `"${fragment.typeCondition}"`, - type: { name: `"${fragment.typeCondition}"` } + propertySetsDeclaration(generator, fragment, propertySets, true); + } else { + const fragmentFields = fields.map(field => { + if (field.fieldName === '__typename') { + return { + ...field, + typeName: `"${fragment.typeCondition}"`, + type: { name: `"${fragment.typeCondition}"` }, + }; + } else { + return field; } - } else { - return field; - } - }); - const properties = propertiesFromFields(generator.context, fragmentFields) - propertyDeclarations(generator, properties); + }); + const properties = propertiesFromFields(generator.context, fragmentFields); + propertyDeclarations(generator, properties); + } } - }); + ); } export function propertiesFromFields(context, fields) { @@ -308,8 +287,15 @@ export function propertyFromField(context, field) { } return { ...property, - typeName, fields: field.fields, isComposite: true, fragmentSpreads, inlineFragments, fieldType, - isArray, isNullable, isArrayElementNullable, + typeName, + fields: field.fields, + isComposite: true, + fragmentSpreads, + inlineFragments, + fieldType, + isArray, + isNullable, + isArrayElementNullable, }; } else { if (field.fieldName === '__typename') { @@ -326,55 +312,55 @@ export function propertyDeclarations(generator, properties, isInput) { if (!properties) return; properties.forEach(property => { if (isAbstractType(getNamedType(property.type || property.fieldType))) { - const propertySets = getPossibleTypeNames(generator, property) - .map(type => { - const inlineFragment = property.inlineFragments.find(inlineFragment => { - return inlineFragment.typeCondition.toString() == type - }); + const propertySets = getPossibleTypeNames(generator, property).map(type => { + const inlineFragment = property.inlineFragments.find(inlineFragment => { + return inlineFragment.typeCondition.toString() == type; + }); - if (inlineFragment) { - const fields = inlineFragment.fields.map(field => { - if (field.fieldName === '__typename') { - return { - ...field, - typeName: `"${inlineFragment.typeCondition}"`, - type: { name: `"${inlineFragment.typeCondition}"` } - } - } else { - return field; - } - }); + if (inlineFragment) { + const fields = inlineFragment.fields.map(field => { + if (field.fieldName === '__typename') { + return { + ...field, + typeName: `"${inlineFragment.typeCondition}"`, + type: { name: `"${inlineFragment.typeCondition}"` }, + }; + } else { + return field; + } + }); - return propertiesFromFields(generator, fields); - } else { - const fields = property.fields.map(field => { - if (field.fieldName === '__typename') { - return { - ...field, - typeName: `"${type}"`, - type: { name: `"${type}"` } - } - } else { - return field; - } - }); + return propertiesFromFields(generator, fields); + } else { + const fields = property.fields.map(field => { + if (field.fieldName === '__typename') { + return { + ...field, + typeName: `"${type}"`, + type: { name: `"${type}"` }, + }; + } else { + return field; + } + }); - return propertiesFromFields(generator, fields); - } - }); + return propertiesFromFields(generator, fields); + } + }); propertySetsDeclaration(generator, property, propertySets); } else { - if (property.fields && property.fields.length > 0 - || property.inlineFragments && property.inlineFragments.length > 0 - || property.fragmentSpreads && property.fragmentSpreads.length > 0 + if ( + (property.fields && property.fields.length > 0) || + (property.inlineFragments && property.inlineFragments.length > 0) || + (property.fragmentSpreads && property.fragmentSpreads.length > 0) ) { propertyDeclaration(generator, property, () => { const properties = propertiesFromFields(generator.context, property.fields); propertyDeclarations(generator, properties, isInput); }); } else { - propertyDeclaration(generator, {...property, isInput}); + propertyDeclaration(generator, { ...property, isInput }); } } }); diff --git a/packages/amplify-graphql-types-generator/src/flow/language.js b/packages/amplify-graphql-types-generator/src/flow/language.js index 0173c6da8c..5938de4722 100644 --- a/packages/amplify-graphql-types-generator/src/flow/language.js +++ b/packages/amplify-graphql-types-generator/src/flow/language.js @@ -1,7 +1,4 @@ -import { - join, - wrap, -} from '../utilities/printing'; +import { join, wrap } from '../utilities/printing'; import { propertyDeclarations } from './codeGeneration'; import { typeNameFromGraphQLType } from './types'; @@ -11,7 +8,7 @@ import { pascalCase } from 'change-case'; export function typeDeclaration(generator, { interfaceName, noBrackets }, closure) { generator.printNewlineIfNeeded(); generator.printNewline(); - generator.print(`export type ${ interfaceName } = `); + generator.print(`export type ${interfaceName} = `); generator.pushScope({ typeName: interfaceName }); if (noBrackets) { generator.withinBlock(closure, '', ''); @@ -22,33 +19,27 @@ export function typeDeclaration(generator, { interfaceName, noBrackets }, closur generator.print(';'); } -export function propertyDeclaration(generator, { - fieldName, - type, - propertyName, - typeName, - description, - isArray, - isNullable, - isArrayElementNullable, - fragmentSpreads, - isInput -}, closure, open = ' {|', close = '|}') { +export function propertyDeclaration( + generator, + { fieldName, type, propertyName, typeName, description, isArray, isNullable, isArrayElementNullable, fragmentSpreads, isInput }, + closure, + open = ' {|', + close = '|}' +) { const name = fieldName || propertyName; if (description) { - description.split('\n') - .forEach(line => { - generator.printOnNewline(`// ${line.trim()}`); - }) + description.split('\n').forEach(line => { + generator.printOnNewline(`// ${line.trim()}`); + }); } if (closure) { - generator.printOnNewline(name) + generator.printOnNewline(name); if (isInput && isNullable) { - generator.print('?') + generator.print('?'); } - generator.print(':') + generator.print(':'); if (isNullable) { generator.print(' ?'); } @@ -71,11 +62,10 @@ export function propertyDeclaration(generator, { if (isArray) { generator.print(' >'); } - } else { - generator.printOnNewline(name) + generator.printOnNewline(name); if (isInput && isNullable) { - generator.print('?') + generator.print('?'); } generator.print(`: ${typeName || typeNameFromGraphQLType(generator.context, type)}`); } @@ -83,17 +73,13 @@ export function propertyDeclaration(generator, { } export function propertySetsDeclaration(generator, property, propertySets, standalone = false) { - const { - description, fieldName, propertyName, typeName, - isNullable, isArray, isArrayElementNullable - } = property; + const { description, fieldName, propertyName, typeName, isNullable, isArray, isArrayElementNullable } = property; const name = fieldName || propertyName; if (description) { - description.split('\n') - .forEach(line => { - generator.printOnNewline(`// ${line.trim()}`); - }) + description.split('\n').forEach(line => { + generator.printOnNewline(`// ${line.trim()}`); + }); } if (!standalone) { generator.printOnNewline(`${name}:`); @@ -112,16 +98,20 @@ export function propertySetsDeclaration(generator, property, propertySets, stand generator.pushScope({ typeName: name }); - generator.withinBlock(() => { - propertySets.forEach((propertySet, index, propertySets) => { - generator.withinBlock(() => { - propertyDeclarations(generator, propertySet); + generator.withinBlock( + () => { + propertySets.forEach((propertySet, index, propertySets) => { + generator.withinBlock(() => { + propertyDeclarations(generator, propertySet); + }); + if (index !== propertySets.length - 1) { + generator.print(' |'); + } }); - if (index !== propertySets.length - 1) { - generator.print(' |'); - } - }) - }, '(', ')'); + }, + '(', + ')' + ); generator.popScope(); diff --git a/packages/amplify-graphql-types-generator/src/flow/types.js b/packages/amplify-graphql-types-generator/src/flow/types.js index 8b2fa04084..d9c6e6cf97 100644 --- a/packages/amplify-graphql-types-generator/src/flow/types.js +++ b/packages/amplify-graphql-types-generator/src/flow/types.js @@ -1,9 +1,4 @@ -import { - join, - block, - wrap, - indent -} from '../utilities/printing'; +import { join, block, wrap, indent } from '../utilities/printing'; import { camelCase } from 'change-case'; @@ -16,7 +11,7 @@ import { GraphQLList, GraphQLNonNull, GraphQLScalarType, - GraphQLEnumType + GraphQLEnumType, } from 'graphql'; const builtInScalarMap = { @@ -25,7 +20,7 @@ const builtInScalarMap = { [GraphQLFloat.name]: 'number', [GraphQLBoolean.name]: 'boolean', [GraphQLID.name]: 'string', -} +}; const appSyncScalars = { AWSTimestamp: 'number', @@ -33,14 +28,17 @@ const appSyncScalars = { export function typeNameFromGraphQLType(context, type, bareTypeName, nullable = true) { if (type instanceof GraphQLNonNull) { - return typeNameFromGraphQLType(context, type.ofType, bareTypeName, false) + return typeNameFromGraphQLType(context, type.ofType, bareTypeName, false); } let typeName; if (type instanceof GraphQLList) { typeName = `Array< ${typeNameFromGraphQLType(context, type.ofType, bareTypeName)} >`; } else if (type instanceof GraphQLScalarType) { - typeName = builtInScalarMap[type.name] || appSyncScalars[type.name] || (context.passthroughCustomScalars ? context.customScalarsPrefix + type.name : 'any'); + typeName = + builtInScalarMap[type.name] || + appSyncScalars[type.name] || + (context.passthroughCustomScalars ? context.customScalarsPrefix + type.name : 'any'); } else { typeName = bareTypeName || type.name; } diff --git a/packages/amplify-graphql-types-generator/src/generate.ts b/packages/amplify-graphql-types-generator/src/generate.ts index 6ab10eacb4..56e43b37cc 100644 --- a/packages/amplify-graphql-types-generator/src/generate.ts +++ b/packages/amplify-graphql-types-generator/src/generate.ts @@ -7,7 +7,7 @@ import { validateQueryDocument } from './validation'; import { compileToIR } from './compiler'; import { compileToLegacyIR } from './compiler/legacyIR'; import serializeToJSON from './serializeToJSON'; -import { BasicGeneratedFile } from './utilities/CodeGenerator' +import { BasicGeneratedFile } from './utilities/CodeGenerator'; import { generateSource as generateSwiftSource } from './swift'; import { generateSource as generateTypescriptSource } from './typescript'; import { generateSource as generateFlowSource } from './flow'; @@ -53,39 +53,32 @@ export default function generate( } else { fs.writeFileSync(outputPath, generator.output); } - } - else if (target === 'flow-modern') { + } else if (target === 'flow-modern') { const context = compileToIR(schema, document, options); const generatedFiles = generateFlowModernSource(context); // Group by output directory const filesByOutputDirectory: { [outputDirectory: string]: { - [fileName: string]: BasicGeneratedFile - } + [fileName: string]: BasicGeneratedFile; + }; } = {}; - Object.keys(generatedFiles) - .forEach((filePath: string) => { - const outputDirectory = path.dirname(filePath); - if (!filesByOutputDirectory[outputDirectory]) { - filesByOutputDirectory[outputDirectory] = { - [path.basename(filePath)]: generatedFiles[filePath] - }; - } else { - filesByOutputDirectory[outputDirectory][path.basename(filePath)] = generatedFiles[filePath]; - } - }) + Object.keys(generatedFiles).forEach((filePath: string) => { + const outputDirectory = path.dirname(filePath); + if (!filesByOutputDirectory[outputDirectory]) { + filesByOutputDirectory[outputDirectory] = { + [path.basename(filePath)]: generatedFiles[filePath], + }; + } else { + filesByOutputDirectory[outputDirectory][path.basename(filePath)] = generatedFiles[filePath]; + } + }); - Object.keys(filesByOutputDirectory) - .forEach((outputDirectory) => { - writeGeneratedFiles( - filesByOutputDirectory[outputDirectory], - outputDirectory - ); - }); - } - else { + Object.keys(filesByOutputDirectory).forEach(outputDirectory => { + writeGeneratedFiles(filesByOutputDirectory[outputDirectory], outputDirectory); + }); + } else { let output; const context = compileToLegacyIR(schema, document, options); switch (target) { @@ -114,10 +107,7 @@ export default function generate( } } -function writeGeneratedFiles( - generatedFiles: { [fileName: string]: BasicGeneratedFile }, - outputDirectory: string -) { +function writeGeneratedFiles(generatedFiles: { [fileName: string]: BasicGeneratedFile }, outputDirectory: string) { // Clear all generated stuff to make sure there isn't anything // unnecessary lying around. rimraf.sync(outputDirectory); @@ -128,4 +118,3 @@ function writeGeneratedFiles( fs.writeFileSync(path.join(outputDirectory, fileName), generatedFile.output); } } - diff --git a/packages/amplify-graphql-types-generator/src/loading.ts b/packages/amplify-graphql-types-generator/src/loading.ts index be756eb1f9..0e5c5df577 100644 --- a/packages/amplify-graphql-types-generator/src/loading.ts +++ b/packages/amplify-graphql-types-generator/src/loading.ts @@ -1,26 +1,18 @@ -import * as fs from 'fs' +import * as fs from 'fs'; -import { - buildClientSchema, - Source, - concatAST, - parse, - DocumentNode, - GraphQLSchema, - buildASTSchema -} from 'graphql'; +import { buildClientSchema, Source, concatAST, parse, DocumentNode, GraphQLSchema, buildASTSchema } from 'graphql'; -import { ToolError } from './errors' +import { ToolError } from './errors'; import { extname, join, normalize, relative } from 'path'; export function loadSchema(schemaPath: string): GraphQLSchema { if (extname(schemaPath) === '.json') { - return loadIntrospectionSchema(schemaPath) + return loadIntrospectionSchema(schemaPath); } - return loadSDLSchema(schemaPath) + return loadSDLSchema(schemaPath); } -function loadIntrospectionSchema(schemaPath: string): GraphQLSchema { +function loadIntrospectionSchema(schemaPath: string): GraphQLSchema { if (!fs.existsSync(schemaPath)) { throw new ToolError(`Cannot find GraphQL schema file: ${schemaPath}`); } @@ -29,10 +21,10 @@ function loadIntrospectionSchema(schemaPath: string): GraphQLSchema { if (!schemaData.data && !schemaData.__schema) { throw new ToolError('GraphQL schema file should contain a valid GraphQL introspection query result'); } - return buildClientSchema((schemaData.data) ? schemaData.data : schemaData); + return buildClientSchema(schemaData.data ? schemaData.data : schemaData); } -function loadSDLSchema(schemaPath: string): GraphQLSchema { +function loadSDLSchema(schemaPath: string): GraphQLSchema { const authDirectivePath = normalize(join(__dirname, '..', 'awsApppSyncDirectives.graphql')); const doc = loadAndMergeQueryDocuments([authDirectivePath, schemaPath]); return buildASTSchema(doc); @@ -40,44 +32,43 @@ function loadSDLSchema(schemaPath: string): GraphQLSchema { function extractDocumentFromJavascript(content: string, tagName: string = 'gql'): string | null { const re = new RegExp(tagName + '\\s*`([^`/]*)`', 'g'); - let match - const matches = [] + let match; + const matches = []; - while (match = re.exec(content)) { - const doc = match[1] - .replace(/\${[^}]*}/g, '') + while ((match = re.exec(content))) { + const doc = match[1].replace(/\${[^}]*}/g, ''); - matches.push(doc) + matches.push(doc); } - const doc = matches.join('\n') + const doc = matches.join('\n'); return doc.length ? doc : null; } export function loadAndMergeQueryDocuments(inputPaths: string[], tagName: string = 'gql'): DocumentNode { - const sources = inputPaths.map(inputPath => { - const body = fs.readFileSync(inputPath, 'utf8'); - if (!body) { - return null; - } + const sources = inputPaths + .map(inputPath => { + const body = fs.readFileSync(inputPath, 'utf8'); + if (!body) { + return null; + } - if (inputPath.endsWith('.jsx') || inputPath.endsWith('.js') - || inputPath.endsWith('.tsx') || inputPath.endsWith('.ts') - ) { - const doc = extractDocumentFromJavascript(body.toString(), tagName); - return doc ? new Source(doc, inputPath) : null; - } + if (inputPath.endsWith('.jsx') || inputPath.endsWith('.js') || inputPath.endsWith('.tsx') || inputPath.endsWith('.ts')) { + const doc = extractDocumentFromJavascript(body.toString(), tagName); + return doc ? new Source(doc, inputPath) : null; + } - return new Source(body, inputPath); - }).filter((source): source is Source => Boolean(source)); + return new Source(body, inputPath); + }) + .filter((source): source is Source => Boolean(source)); const parsedSources = sources.map(source => { try { return parse(source); } catch (err) { const relativePathToInput = relative(process.cwd(), source.name); - throw new ToolError(`Could not parse graphql operations in ${relativePathToInput}\n Failed on : ${source.body}`) + throw new ToolError(`Could not parse graphql operations in ${relativePathToInput}\n Failed on : ${source.body}`); } - }) + }); return concatAST(parsedSources); } diff --git a/packages/amplify-graphql-types-generator/src/scala/codeGeneration.js b/packages/amplify-graphql-types-generator/src/scala/codeGeneration.js index 0dcc4803a7..1081f1869e 100644 --- a/packages/amplify-graphql-types-generator/src/scala/codeGeneration.js +++ b/packages/amplify-graphql-types-generator/src/scala/codeGeneration.js @@ -9,15 +9,12 @@ import { GraphQLList, GraphQLNonNull, GraphQLID, - GraphQLInputObjectType -} from 'graphql' + GraphQLInputObjectType, +} from 'graphql'; -import { isTypeProperSuperTypeOf } from '../utilities/graphql' +import { isTypeProperSuperTypeOf } from '../utilities/graphql'; -import { - join, - wrap, -} from '../utilities/printing'; +import { join, wrap } from '../utilities/printing'; import { packageDeclaration, @@ -26,7 +23,7 @@ import { propertyDeclaration, propertyDeclarations, escapeIdentifierIfNeeded, - comment + comment, } from './language'; import { @@ -38,19 +35,12 @@ import { propertiesFromSelectionSet, propertyFromField, propertyFromInlineFragment, - propertyFromFragmentSpread + propertyFromFragmentSpread, } from './naming'; -import { - escapedString, - multilineString, - dictionaryLiteralForFieldArguments, -} from './values'; +import { escapedString, multilineString, dictionaryLiteralForFieldArguments } from './values'; -import { - possibleTypesForType, - typeNameFromGraphQLType, -} from './types'; +import { possibleTypesForType, typeNameFromGraphQLType } from './types'; import CodeGenerator from '../utilities/CodeGenerator'; @@ -92,7 +82,7 @@ export function classDeclarationForOperation( fragmentsReferenced, source, sourceWithFragments, - operationId + operationId, } ) { let objectName; @@ -111,178 +101,190 @@ export function classDeclarationForOperation( throw new GraphQLError(`Unsupported operation type "${operationType}"`); } - objectDeclaration(generator, { - objectName, - modifiers: [], - superclass: protocol - }, () => { - if (source) { - generator.printOnNewline('val operationString ='); - generator.withIndent(() => { - multilineString(generator, source); - }); - } + objectDeclaration( + generator, + { + objectName, + modifiers: [], + superclass: protocol, + }, + () => { + if (source) { + generator.printOnNewline('val operationString ='); + generator.withIndent(() => { + multilineString(generator, source); + }); + } - operationIdentifier(generator, { operationName, sourceWithFragments, operationId }); + operationIdentifier(generator, { operationName, sourceWithFragments, operationId }); - if (fragmentsReferenced && fragmentsReferenced.length > 0) { - generator.printNewlineIfNeeded(); - generator.printOnNewline('val requestString: String = { operationString'); - fragmentsReferenced.forEach(fragment => { - generator.print(` + ${caseClassNameForFragmentName(fragment)}.fragmentString`) - }); - generator.print(' }'); + if (fragmentsReferenced && fragmentsReferenced.length > 0) { + generator.printNewlineIfNeeded(); + generator.printOnNewline('val requestString: String = { operationString'); + fragmentsReferenced.forEach(fragment => { + generator.print(` + ${caseClassNameForFragmentName(fragment)}.fragmentString`); + }); + generator.print(' }'); - generator.printOnNewline('val operation = com.apollographql.scalajs.gql(requestString)'); - } else { - generator.printOnNewline('val operation = com.apollographql.scalajs.gql(operationString)'); - } + generator.printOnNewline('val operation = com.apollographql.scalajs.gql(requestString)'); + } else { + generator.printOnNewline('val operation = com.apollographql.scalajs.gql(operationString)'); + } - generator.printNewlineIfNeeded(); + generator.printNewlineIfNeeded(); - if (variables && variables.length > 0) { - const properties = variables.map(({ name, type }) => { - const propertyName = escapeIdentifierIfNeeded(name); - const typeName = typeNameFromGraphQLType(generator.context, type); - const isOptional = !(type instanceof GraphQLNonNull || type.ofType instanceof GraphQLNonNull); - return { name, propertyName, type, typeName, isOptional }; - }); + if (variables && variables.length > 0) { + const properties = variables.map(({ name, type }) => { + const propertyName = escapeIdentifierIfNeeded(name); + const typeName = typeNameFromGraphQLType(generator.context, type); + const isOptional = !(type instanceof GraphQLNonNull || type.ofType instanceof GraphQLNonNull); + return { name, propertyName, type, typeName, isOptional }; + }); - caseClassDeclaration(generator, { caseClassName: 'Variables', description: '', params: properties.map(p => { - return { - name: p.propertyName, - type: p.typeName - }; - })}, () => {}); - } else { - generator.printOnNewline('type Variables = Unit'); - } + caseClassDeclaration( + generator, + { + caseClassName: 'Variables', + description: '', + params: properties.map(p => { + return { + name: p.propertyName, + type: p.typeName, + }; + }), + }, + () => {} + ); + } else { + generator.printOnNewline('type Variables = Unit'); + } - caseClassDeclarationForSelectionSet( - generator, - { - caseClassName: "Data", + caseClassDeclarationForSelectionSet(generator, { + caseClassName: 'Data', parentType: rootType, fields, inlineFragments, - fragmentSpreads - } - ); - }); + fragmentSpreads, + }); + } + ); } export function caseClassDeclarationForFragment( generator, - { - fragmentName, - typeCondition, - fields, - inlineFragments, - fragmentSpreads, - source - } + { fragmentName, typeCondition, fields, inlineFragments, fragmentSpreads, source } ) { const caseClassName = caseClassNameForFragmentName(fragmentName); - caseClassDeclarationForSelectionSet(generator, { - caseClassName, - parentType: typeCondition, - fields, - inlineFragments, - fragmentSpreads - }, () => {}, () => { - if (source) { - generator.printOnNewline('val fragmentString ='); - generator.withIndent(() => { - multilineString(generator, source); - }); + caseClassDeclarationForSelectionSet( + generator, + { + caseClassName, + parentType: typeCondition, + fields, + inlineFragments, + fragmentSpreads, + }, + () => {}, + () => { + if (source) { + generator.printOnNewline('val fragmentString ='); + generator.withIndent(() => { + multilineString(generator, source); + }); + } } - }); + ); } export function caseClassDeclarationForSelectionSet( generator, - { - caseClassName, - parentType, - fields, - inlineFragments, - fragmentSpreads, - viewableAs - }, + { caseClassName, parentType, fields, inlineFragments, fragmentSpreads, viewableAs }, beforeClosure, - objectClosure, + objectClosure ) { const possibleTypes = parentType ? possibleTypesForType(generator.context, parentType) : null; let properties; if (!possibleTypes || possibleTypes.length == 1) { - properties = fields - .map(field => propertyFromField(generator.context, field)) - .filter(field => field.propertyName != "__typename"); - - caseClassDeclaration(generator, { caseClassName, params: properties.map(p => { - return { - name: p.responseName, - type: p.typeName, - }; - }) }, () => {}); + properties = fields.map(field => propertyFromField(generator.context, field)).filter(field => field.propertyName != '__typename'); + + caseClassDeclaration( + generator, + { + caseClassName, + params: properties.map(p => { + return { + name: p.responseName, + type: p.typeName, + }; + }), + }, + () => {} + ); } else { generator.printNewlineIfNeeded(); - const properties = fields - .map(field => propertyFromField(generator.context, field)) - .filter(field => field.propertyName != "__typename"); - - caseClassDeclaration(generator, { caseClassName, params: properties.map(p => { - return { - name: p.responseName, - type: p.typeName, - }; - }), superclass: 'me.shadaj.slinky.core.WithRaw'}, () => { - if (inlineFragments && inlineFragments.length > 0) { - inlineFragments.forEach((inlineFragment) => { - const fragClass = caseClassNameForInlineFragment(inlineFragment); - generator.printOnNewline(`def as${inlineFragment.typeCondition}`); - generator.print(`: Option[${fragClass}] =`); - generator.withinBlock(() => { - generator.printOnNewline(`if (${fragClass}.possibleTypes.contains(this.raw.__typename.asInstanceOf[String])) Some(implicitly[me.shadaj.slinky.core.Reader[${fragClass}]].read(this.raw)) else None`); - }); - }); - } + const properties = fields.map(field => propertyFromField(generator.context, field)).filter(field => field.propertyName != '__typename'); - if (fragmentSpreads) { - fragmentSpreads.forEach(s => { - const fragment = generator.context.fragments[s]; - const alwaysDefined = isTypeProperSuperTypeOf(generator.context.schema, fragment.typeCondition, parentType); - if (!alwaysDefined) { - generator.printOnNewline(`def as${s}`); - generator.print(`: Option[${s}] =`); + caseClassDeclaration( + generator, + { + caseClassName, + params: properties.map(p => { + return { + name: p.responseName, + type: p.typeName, + }; + }), + superclass: 'me.shadaj.slinky.core.WithRaw', + }, + () => { + if (inlineFragments && inlineFragments.length > 0) { + inlineFragments.forEach(inlineFragment => { + const fragClass = caseClassNameForInlineFragment(inlineFragment); + generator.printOnNewline(`def as${inlineFragment.typeCondition}`); + generator.print(`: Option[${fragClass}] =`); generator.withinBlock(() => { - generator.printOnNewline(`if (${s}.possibleTypes.contains(this.raw.__typename.asInstanceOf[String])) Some(implicitly[me.shadaj.slinky.core.Reader[${s}]].read(this.raw)) else None`); + generator.printOnNewline( + `if (${fragClass}.possibleTypes.contains(this.raw.__typename.asInstanceOf[String])) Some(implicitly[me.shadaj.slinky.core.Reader[${fragClass}]].read(this.raw)) else None` + ); }); - } - }) + }); + } + + if (fragmentSpreads) { + fragmentSpreads.forEach(s => { + const fragment = generator.context.fragments[s]; + const alwaysDefined = isTypeProperSuperTypeOf(generator.context.schema, fragment.typeCondition, parentType); + if (!alwaysDefined) { + generator.printOnNewline(`def as${s}`); + generator.print(`: Option[${s}] =`); + generator.withinBlock(() => { + generator.printOnNewline( + `if (${s}.possibleTypes.contains(this.raw.__typename.asInstanceOf[String])) Some(implicitly[me.shadaj.slinky.core.Reader[${s}]].read(this.raw)) else None` + ); + }); + } + }); + } } - }); + ); // add types and implicit conversions if (inlineFragments && inlineFragments.length > 0) { - inlineFragments.forEach((inlineFragment) => { - caseClassDeclarationForSelectionSet( - generator, - { - caseClassName: caseClassNameForInlineFragment(inlineFragment), - parentType: inlineFragment.typeCondition, - fields: inlineFragment.fields, - inlineFragments: inlineFragment.inlineFragments, - fragmentSpreads: inlineFragment.fragmentSpreads, - viewableAs: { - caseClassName, - properties, - }, - } - ); + inlineFragments.forEach(inlineFragment => { + caseClassDeclarationForSelectionSet(generator, { + caseClassName: caseClassNameForInlineFragment(inlineFragment), + parentType: inlineFragment.typeCondition, + fields: inlineFragment.fields, + inlineFragments: inlineFragment.inlineFragments, + fragmentSpreads: inlineFragment.fragmentSpreads, + viewableAs: { + caseClassName, + properties, + }, + }); }); } } @@ -296,7 +298,11 @@ export function caseClassDeclarationForSelectionSet( } if (viewableAs) { - generator.printOnNewline(`implicit def to${viewableAs.caseClassName}(a: ${caseClassName}): ${viewableAs.caseClassName} = ${viewableAs.caseClassName}(${viewableAs.properties.map(p => "a." + p.responseName).join(', ')})`); + generator.printOnNewline( + `implicit def to${viewableAs.caseClassName}(a: ${caseClassName}): ${viewableAs.caseClassName} = ${ + viewableAs.caseClassName + }(${viewableAs.properties.map(p => 'a.' + p.responseName).join(', ')})` + ); } if (fragmentSpreads) { @@ -304,9 +310,11 @@ export function caseClassDeclarationForSelectionSet( const fragment = generator.context.fragments[s]; const alwaysDefined = isTypeProperSuperTypeOf(generator.context.schema, fragment.typeCondition, parentType); if (alwaysDefined) { - generator.printOnNewline(`implicit def to${s}(a: ${caseClassName}): ${s} = ${s}(${(fragment.fields || []).map(p => "a." + p.responseName).join(', ')})`); + generator.printOnNewline( + `implicit def to${s}(a: ${caseClassName}): ${s} = ${s}(${(fragment.fields || []).map(p => 'a.' + p.responseName).join(', ')})` + ); } - }) + }); } if (objectClosure) { @@ -314,23 +322,22 @@ export function caseClassDeclarationForSelectionSet( } }); - fields.filter(field => isCompositeType(getNamedType(field.type))).forEach(field => { - caseClassDeclarationForSelectionSet( - generator, - { + fields + .filter(field => isCompositeType(getNamedType(field.type))) + .forEach(field => { + caseClassDeclarationForSelectionSet(generator, { caseClassName: caseClassNameForPropertyName(field.responseName), parentType: getNamedType(field.type), fields: field.fields, inlineFragments: field.inlineFragments, - fragmentSpreads: field.fragmentSpreads - } - ); - }); + fragmentSpreads: field.fragmentSpreads, + }); + }); } -function operationIdentifier(generator, { operationName, sourceWithFragments, operationId }) { +function operationIdentifier(generator, { operationName, sourceWithFragments, operationId }) { if (!generator.context.generateOperationIds) { - return + return; } generator.printNewlineIfNeeded(); @@ -366,10 +373,18 @@ function caseClassDeclarationForInputObjectType(generator, type) { const fields = Object.values(type.getFields()); const properties = fields.map(field => propertyFromField(generator.context, field)); - caseClassDeclaration(generator, { caseClassName, description, params: properties.map(p => { - return { - name: p.propertyName, - type: p.typeName - }; - })}, () => {}); + caseClassDeclaration( + generator, + { + caseClassName, + description, + params: properties.map(p => { + return { + name: p.propertyName, + type: p.typeName, + }; + }), + }, + () => {} + ); } diff --git a/packages/amplify-graphql-types-generator/src/scala/language.js b/packages/amplify-graphql-types-generator/src/scala/language.js index 23fc67dc02..b5803b5572 100644 --- a/packages/amplify-graphql-types-generator/src/scala/language.js +++ b/packages/amplify-graphql-types-generator/src/scala/language.js @@ -1,12 +1,9 @@ -import { - join, - wrap, -} from '../utilities/printing'; +import { join, wrap } from '../utilities/printing'; export function comment(generator, comment) { const split = comment ? comment.split('\n') : []; if (split.length > 0) { - generator.printOnNewline('/**') + generator.printOnNewline('/**'); split.forEach(line => { generator.printOnNewline(` * ${line.trim()}`); }); @@ -32,13 +29,16 @@ export function objectDeclaration(generator, { objectName, superclass, propertie export function caseClassDeclaration(generator, { caseClassName, description, superclass, params }, closure) { generator.printNewlineIfNeeded(); comment(generator, description); - generator.printOnNewline(`case class ${caseClassName}(${(params || []).map(v => v.name + ": " + v.type).join(', ')})` + (superclass ? ` extends ${superclass}` : '')); + generator.printOnNewline( + `case class ${caseClassName}(${(params || []).map(v => v.name + ': ' + v.type).join(', ')})` + + (superclass ? ` extends ${superclass}` : '') + ); generator.pushScope({ typeName: caseClassName }); generator.withinBlock(closure); generator.popScope(); } -export function propertyDeclaration(generator, { propertyName, typeName, description}, closure) { +export function propertyDeclaration(generator, { propertyName, typeName, description }, closure) { comment(generator, description); generator.printOnNewline(`val ${propertyName}: ${typeName} =`); generator.withinBlock(closure); @@ -51,10 +51,29 @@ export function propertyDeclarations(generator, declarations) { } const reservedKeywords = new Set( - 'case', 'catch', 'class', 'def', 'do', 'else', - 'extends', 'false', 'final', 'for', 'if', 'match', - 'new', 'null', 'throw', 'trait', 'true', 'try', 'until', - 'val', 'var', 'while', 'with' + 'case', + 'catch', + 'class', + 'def', + 'do', + 'else', + 'extends', + 'false', + 'final', + 'for', + 'if', + 'match', + 'new', + 'null', + 'throw', + 'trait', + 'true', + 'try', + 'until', + 'val', + 'var', + 'while', + 'with' ); export function escapeIdentifierIfNeeded(identifier) { diff --git a/packages/amplify-graphql-types-generator/src/scala/naming.js b/packages/amplify-graphql-types-generator/src/scala/naming.js index 6399c7a090..1c412e878e 100644 --- a/packages/amplify-graphql-types-generator/src/scala/naming.js +++ b/packages/amplify-graphql-types-generator/src/scala/naming.js @@ -1,25 +1,13 @@ import { camelCase, pascalCase } from 'change-case'; import * as Inflector from 'inflected'; -import { - join -} from '../utilities/printing'; +import { join } from '../utilities/printing'; -import { - escapeIdentifierIfNeeded -} from './language'; +import { escapeIdentifierIfNeeded } from './language'; -import { - typeNameFromGraphQLType -} from './types'; +import { typeNameFromGraphQLType } from './types'; -import { - GraphQLError, - GraphQLList, - GraphQLNonNull, - getNamedType, - isCompositeType, -} from 'graphql'; +import { GraphQLError, GraphQLList, GraphQLNonNull, getNamedType, isCompositeType } from 'graphql'; export function enumCaseName(name) { return camelCase(name); @@ -43,19 +31,16 @@ export function caseClassNameForInlineFragment(inlineFragment) { export function propertyFromField(context, field, namespace) { const name = field.name || field.responseName; - const unescapedPropertyName = isMetaFieldName(name) ? name : camelCase(name) + const unescapedPropertyName = isMetaFieldName(name) ? name : camelCase(name); const propertyName = escapeIdentifierIfNeeded(unescapedPropertyName); const type = field.type; - const isList = type instanceof GraphQLList || type.ofType instanceof GraphQLList + const isList = type instanceof GraphQLList || type.ofType instanceof GraphQLList; const isOptional = field.isConditional || !(type instanceof GraphQLNonNull); const bareType = getNamedType(type); if (isCompositeType(bareType)) { - const bareTypeName = join([ - namespace, - escapeIdentifierIfNeeded(pascalCase(Inflector.singularize(name))) - ], '.'); + const bareTypeName = join([namespace, escapeIdentifierIfNeeded(pascalCase(Inflector.singularize(name)))], '.'); const typeName = typeNameFromGraphQLType(context, type, bareTypeName, isOptional); return { ...field, propertyName, typeName, bareTypeName, isOptional, isList, isComposite: true }; } else { @@ -67,7 +52,7 @@ export function propertyFromField(context, field, namespace) { export function propertyFromInlineFragment(context, inlineFragment) { const structName = caseClassNameForInlineFragment(inlineFragment); const propertyName = camelCase(structName); - const typeName = structName + '?' + const typeName = structName + '?'; return { propertyName, typeName, structName, isComposite: true, ...inlineFragment }; } @@ -83,5 +68,5 @@ export function propertyFromFragmentSpread(context, fragmentSpread) { } function isMetaFieldName(name) { - return name.startsWith("__"); -} \ No newline at end of file + return name.startsWith('__'); +} diff --git a/packages/amplify-graphql-types-generator/src/scala/types.js b/packages/amplify-graphql-types-generator/src/scala/types.js index ee182f8588..9c7a7ea883 100644 --- a/packages/amplify-graphql-types-generator/src/scala/types.js +++ b/packages/amplify-graphql-types-generator/src/scala/types.js @@ -1,9 +1,4 @@ -import { - join, - block, - wrap, - indent -} from '../utilities/printing'; +import { join, block, wrap, indent } from '../utilities/printing'; import { camelCase } from 'change-case'; @@ -18,7 +13,7 @@ import { GraphQLScalarType, GraphQLEnumType, isCompositeType, - isAbstractType + isAbstractType, } from 'graphql'; const builtInScalarMap = { @@ -27,7 +22,7 @@ const builtInScalarMap = { [GraphQLFloat.name]: 'Double', [GraphQLBoolean.name]: 'Boolean', [GraphQLID.name]: 'String', -} +}; export function possibleTypesForType(context, type) { if (isAbstractType(type)) { @@ -39,7 +34,7 @@ export function possibleTypesForType(context, type) { export function typeNameFromGraphQLType(context, type, bareTypeName, isOptional) { if (type instanceof GraphQLNonNull) { - return typeNameFromGraphQLType(context, type.ofType, bareTypeName, isOptional || false) + return typeNameFromGraphQLType(context, type.ofType, bareTypeName, isOptional || false); } else if (isOptional === undefined) { isOptional = true; } @@ -50,7 +45,7 @@ export function typeNameFromGraphQLType(context, type, bareTypeName, isOptional) } else if (type instanceof GraphQLScalarType) { typeName = typeNameForScalarType(context, type); } else if (type instanceof GraphQLEnumType) { - typeName = "String"; + typeName = 'String'; } else { typeName = bareTypeName || type.name; } @@ -59,5 +54,5 @@ export function typeNameFromGraphQLType(context, type, bareTypeName, isOptional) } function typeNameForScalarType(context, type) { - return builtInScalarMap[type.name] || (context.passthroughCustomScalars ? context.customScalarsPrefix + type.name: GraphQLString) + return builtInScalarMap[type.name] || (context.passthroughCustomScalars ? context.customScalarsPrefix + type.name : GraphQLString); } diff --git a/packages/amplify-graphql-types-generator/src/scala/values.js b/packages/amplify-graphql-types-generator/src/scala/values.js index 0e24f9f424..dbf637a8c1 100644 --- a/packages/amplify-graphql-types-generator/src/scala/values.js +++ b/packages/amplify-graphql-types-generator/src/scala/values.js @@ -1,7 +1,4 @@ -import { - join, - wrap, -} from '../utilities/printing'; +import { join, wrap } from '../utilities/printing'; export function escapedString(string) { return string.replace(/"/g, '\\"'); @@ -22,15 +19,29 @@ export function dictionaryLiteralForFieldArguments(args) { } else if (Array.isArray(value)) { return wrap('[', join(value.map(expressionFromValue), ', '), ']'); } else if (typeof value === 'object') { - return wrap('[', join(Object.entries(value).map(([key, value]) => { - return `"${key}": ${expressionFromValue(value)}`; - }), ', ') || ':', ']'); + return wrap( + '[', + join( + Object.entries(value).map(([key, value]) => { + return `"${key}": ${expressionFromValue(value)}`; + }), + ', ' + ) || ':', + ']' + ); } else { return JSON.stringify(value); } } - return wrap('[', join(args.map(arg => { - return `"${arg.name}": ${expressionFromValue(arg.value)}`; - }), ', ') || ':', ']'); + return wrap( + '[', + join( + args.map(arg => { + return `"${arg.name}": ${expressionFromValue(arg.value)}`; + }), + ', ' + ) || ':', + ']' + ); } diff --git a/packages/amplify-graphql-types-generator/src/serializeToJSON.ts b/packages/amplify-graphql-types-generator/src/serializeToJSON.ts index f84a5f29c0..b40afd09b2 100644 --- a/packages/amplify-graphql-types-generator/src/serializeToJSON.ts +++ b/packages/amplify-graphql-types-generator/src/serializeToJSON.ts @@ -1,29 +1,30 @@ -import { - isType, - GraphQLType, - GraphQLScalarType, - GraphQLEnumType, - GraphQLInputObjectType, -} from 'graphql'; +import { isType, GraphQLType, GraphQLScalarType, GraphQLEnumType, GraphQLInputObjectType } from 'graphql'; import { LegacyCompilerContext } from './compiler/legacyIR'; export default function serializeToJSON(context: LegacyCompilerContext) { - return serializeAST({ - operations: Object.values(context.operations), - fragments: Object.values(context.fragments), - typesUsed: context.typesUsed.map(serializeType), - }, '\t'); + return serializeAST( + { + operations: Object.values(context.operations), + fragments: Object.values(context.fragments), + typesUsed: context.typesUsed.map(serializeType), + }, + '\t' + ); } export function serializeAST(ast: any, space?: string) { - return JSON.stringify(ast, function(_, value) { - if (isType(value)) { - return String(value); - } else { - return value; - } - }, space); + return JSON.stringify( + ast, + function(_, value) { + if (isType(value)) { + return String(value); + } else { + return value; + } + }, + space + ); } function serializeType(type: GraphQLType) { @@ -46,15 +47,13 @@ function serializeEnumType(type: GraphQLEnumType) { kind: 'EnumType', name, description, - values: values.map(value => ( - { - name: value.name, - description: value.description, - isDeprecated: value.isDeprecated, - deprecationReason: value.deprecationReason - } - )) - } + values: values.map(value => ({ + name: value.name, + description: value.description, + isDeprecated: value.isDeprecated, + deprecationReason: value.deprecationReason, + })), + }; } function serializeInputObjectType(type: GraphQLInputObjectType) { @@ -69,9 +68,9 @@ function serializeInputObjectType(type: GraphQLInputObjectType) { name: field.name, type: String(field.type), description: field.description, - defaultValue: field.defaultValue - })) - } + defaultValue: field.defaultValue, + })), + }; } function serializeScalarType(type: GraphQLScalarType) { @@ -80,6 +79,6 @@ function serializeScalarType(type: GraphQLScalarType) { return { kind: 'ScalarType', name, - description - } + description, + }; } diff --git a/packages/amplify-graphql-types-generator/src/swift/aws-scalar-helper.ts b/packages/amplify-graphql-types-generator/src/swift/aws-scalar-helper.ts index 952f3fad47..187f801add 100644 --- a/packages/amplify-graphql-types-generator/src/swift/aws-scalar-helper.ts +++ b/packages/amplify-graphql-types-generator/src/swift/aws-scalar-helper.ts @@ -1,23 +1,21 @@ -import { - GraphQLScalarType, - } from 'graphql'; +import { GraphQLScalarType } from 'graphql'; interface INameToValueMap { - [key: string]: any; + [key: string]: any; } export const awsScalarMap: INameToValueMap = { - AWSDate: 'String', - AWSTime: 'String', - AWSDateTime: 'String', - AWSTimestamp: 'Int', - AWSEmail: 'String', - AWSJSON: 'String', - AWSURL: 'String', - AWSPhone: 'String', - AWSIPAddress: 'String', + AWSDate: 'String', + AWSTime: 'String', + AWSDateTime: 'String', + AWSTimestamp: 'Int', + AWSEmail: 'String', + AWSJSON: 'String', + AWSURL: 'String', + AWSPhone: 'String', + AWSIPAddress: 'String', }; export function getTypeForAWSScalar(type: GraphQLScalarType): string { - return awsScalarMap[type.name]; -} \ No newline at end of file + return awsScalarMap[type.name]; +} diff --git a/packages/amplify-graphql-types-generator/src/swift/codeGeneration.ts b/packages/amplify-graphql-types-generator/src/swift/codeGeneration.ts index 8cc5a21ed0..e9937552fa 100644 --- a/packages/amplify-graphql-types-generator/src/swift/codeGeneration.ts +++ b/packages/amplify-graphql-types-generator/src/swift/codeGeneration.ts @@ -34,11 +34,7 @@ export interface Options { customScalarsPrefix?: string; } -export function generateSource( - context: CompilerContext, - outputIndividualFiles: boolean, - only?: string -): SwiftAPIGenerator { +export function generateSource(context: CompilerContext, outputIndividualFiles: boolean, only?: string): SwiftAPIGenerator { const generator = new SwiftAPIGenerator(context); if (outputIndividualFiles) { @@ -164,17 +160,10 @@ export class SwiftAPIGenerator extends SwiftGenerator { }); } - const fragmentsReferenced = collectFragmentsReferenced( - operation.selectionSet, - this.context.fragments - ); + const fragmentsReferenced = collectFragmentsReferenced(operation.selectionSet, this.context.fragments); if (this.context.options.generateOperationIds) { - const { operationId } = generateOperationId( - operation, - this.context.fragments, - fragmentsReferenced - ); + const { operationId } = generateOperationId(operation, this.context.fragments, fragmentsReferenced); operation.operationId = operationId; this.printNewlineIfNeeded(); this.printOnNewline(`public static let operationIdentifier: String? = "${operationId}"`); @@ -184,9 +173,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printNewlineIfNeeded(); this.printOnNewline('public static var requestString: String { return operationString'); fragmentsReferenced.forEach(fragmentName => { - this.print( - `.appending(${this.helpers.structNameForFragmentName(fragmentName)}.fragmentString)` - ); + this.print(`.appending(${this.helpers.structNameForFragmentName(fragmentName)}.fragmentString)`); }); this.print(' }'); } @@ -196,10 +183,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { if (variables && variables.length > 0) { const properties = variables.map(({ name, type }) => { const typeName = this.helpers.typeNameFromGraphQLType(type); - const isOptional = !( - isNonNullType(type) || - (isListType(type) && isNonNullType(type.ofType)) - ); + const isOptional = !(isNonNullType(type) || (isListType(type) && isNonNullType(type.ofType))); return { name, propertyName: name, type, typeName, isOptional }; }); @@ -214,13 +198,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline( wrap( `return [`, - join( - properties.map( - ({ name, propertyName }) => - `"${name}": ${escapeIdentifierIfNeeded(propertyName)}` - ), - ', ' - ) || ':', + join(properties.map(({ name, propertyName }) => `"${name}": ${escapeIdentifierIfNeeded(propertyName)}`), ', ') || ':', `]` ) ); @@ -269,10 +247,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { }, before?: Function ) { - const typeCase = typeCaseForSelectionSet( - selectionSet, - this.context.options.mergeInFieldsFromFragmentSpreads - ); + const typeCase = typeCaseForSelectionSet(selectionSet, this.context.options.mergeInFieldsFromFragmentSpreads); this.structDeclarationForVariant( { @@ -349,15 +324,12 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.initializersForVariant(variant); } - const fields = collectAndMergeFields( - variant, - this.context.options.mergeInFieldsFromFragmentSpreads - ).map(field => this.helpers.propertyFromField(field as Field)); + const fields = collectAndMergeFields(variant, this.context.options.mergeInFieldsFromFragmentSpreads).map(field => + this.helpers.propertyFromField(field as Field) + ); const fragmentSpreads = variant.fragmentSpreads.map(fragmentSpread => { - const isConditional = variant.possibleTypes.some( - type => !fragmentSpread.selectionSet.possibleTypes.includes(type) - ); + const isConditional = variant.possibleTypes.some(type => !fragmentSpread.selectionSet.possibleTypes.includes(type)); return this.helpers.propertyFromFragmentSpread(fragmentSpread, isConditional); }); @@ -392,16 +364,12 @@ export class SwiftAPIGenerator extends SwiftGenerator { const { propertyName, typeName, structName, isConditional } = fragmentSpread; this.printNewlineIfNeeded(); - this.printOnNewline( - `public var ${escapeIdentifierIfNeeded(propertyName)}: ${typeName}` - ); + this.printOnNewline(`public var ${escapeIdentifierIfNeeded(propertyName)}: ${typeName}`); this.withinBlock(() => { this.printOnNewline('get'); this.withinBlock(() => { if (isConditional) { - this.printOnNewline( - `if !${structName}.possibleTypes.contains(snapshot["__typename"]! as! String) { return nil }` - ); + this.printOnNewline(`if !${structName}.possibleTypes.contains(snapshot["__typename"]! as! String) { return nil }`); } this.printOnNewline(`return ${structName}(snapshot: snapshot)`); }); @@ -443,20 +411,12 @@ export class SwiftAPIGenerator extends SwiftGenerator { } else { const remainder = typeCase.remainder; for (const variant of remainder ? [remainder, ...variants] : variants) { - this.initializersForVariant( - variant, - variant === remainder ? undefined : this.helpers.structNameForVariant(variant), - false - ); + this.initializersForVariant(variant, variant === remainder ? undefined : this.helpers.structNameForVariant(variant), false); } } } - initializersForVariant( - variant: Variant, - namespace?: string, - useInitializerIfPossible: boolean = true - ) { + initializersForVariant(variant: Variant, namespace?: string, useInitializerIfPossible: boolean = true) { if (useInitializerIfPossible && variant.possibleTypes.length == 1) { const properties = this.helpers.propertiesForSelectionSet(variant); if (!properties) return; @@ -470,13 +430,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline( wrap( `self.init(snapshot: [`, - join( - [ - `"__typename": "${variant.possibleTypes[0]}"`, - ...properties.map(this.propertyAssignmentForField, this), - ], - ', ' - ) || ':', + join([`"__typename": "${variant.possibleTypes[0]}"`, ...properties.map(this.propertyAssignmentForField, this)], ', ') || ':', `])` ) ); @@ -506,13 +460,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline( wrap( `return ${structName}(snapshot: [`, - join( - [ - `"__typename": "${possibleType}"`, - ...properties.map(this.propertyAssignmentForField, this), - ], - ', ' - ) || ':', + join([`"__typename": "${possibleType}"`, ...properties.map(this.propertyAssignmentForField, this)], ', ') || ':', `])` ) ); @@ -521,18 +469,10 @@ export class SwiftAPIGenerator extends SwiftGenerator { } } - propertyAssignmentForField(field: { - responseKey: string; - propertyName: string; - type: GraphQLType; - }) { + propertyAssignmentForField(field: { responseKey: string; propertyName: string; type: GraphQLType }) { const { responseKey, propertyName, type } = field; const valueExpression = isCompositeType(getNamedType(type)) - ? this.helpers.mapExpressionForType( - type, - identifier => `${identifier}.snapshot`, - escapeIdentifierIfNeeded(propertyName) - ) + ? this.helpers.mapExpressionForType(type, identifier => `${identifier}.snapshot`, escapeIdentifierIfNeeded(propertyName)) : escapeIdentifierIfNeeded(propertyName); return `"${responseKey}": ${valueExpression}`; } @@ -550,9 +490,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline(`public var ${escapeIdentifierIfNeeded(propertyName)}: ${typeName}`); this.withinBlock(() => { if (isCompositeType(unmodifiedFieldType)) { - const structName = escapeIdentifierIfNeeded( - this.helpers.structNameForPropertyName(propertyName) - ); + const structName = escapeIdentifierIfNeeded(this.helpers.structNameForPropertyName(propertyName)); if (isList(type)) { this.printOnNewline('get'); @@ -564,34 +502,21 @@ export class SwiftAPIGenerator extends SwiftGenerator { } else { getter = `return (snapshot["${responseKey}"] as! ${snapshotTypeName})`; } - getter += this.helpers.mapExpressionForType( - type, - identifier => `${structName}(snapshot: ${identifier})` - ); + getter += this.helpers.mapExpressionForType(type, identifier => `${structName}(snapshot: ${identifier})`); this.printOnNewline(getter); }); this.printOnNewline('set'); this.withinBlock(() => { - let newValueExpression = this.helpers.mapExpressionForType( - type, - identifier => `${identifier}.snapshot`, - 'newValue' - ); - this.printOnNewline( - `snapshot.updateValue(${newValueExpression}, forKey: "${responseKey}")` - ); + let newValueExpression = this.helpers.mapExpressionForType(type, identifier => `${identifier}.snapshot`, 'newValue'); + this.printOnNewline(`snapshot.updateValue(${newValueExpression}, forKey: "${responseKey}")`); }); } else { this.printOnNewline('get'); this.withinBlock(() => { if (isOptional) { - this.printOnNewline( - `return (snapshot["${responseKey}"] as? Snapshot).flatMap { ${structName}(snapshot: $0) }` - ); + this.printOnNewline(`return (snapshot["${responseKey}"] as? Snapshot).flatMap { ${structName}(snapshot: $0) }`); } else { - this.printOnNewline( - `return ${structName}(snapshot: snapshot["${responseKey}"]! as! Snapshot)` - ); + this.printOnNewline(`return ${structName}(snapshot: snapshot["${responseKey}"]! as! Snapshot)`); } }); this.printOnNewline('set'); @@ -602,9 +527,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { } else { newValueExpression = 'newValue.snapshot'; } - this.printOnNewline( - `snapshot.updateValue(${newValueExpression}, forKey: "${responseKey}")` - ); + this.printOnNewline(`snapshot.updateValue(${newValueExpression}, forKey: "${responseKey}")`); }); } } else { @@ -711,9 +634,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { [ `"${name}"`, alias ? `alias: "${alias}"` : null, - args && - args.length && - `arguments: ${this.helpers.dictionaryLiteralForFieldArguments(args)}`, + args && args.length && `arguments: ${this.helpers.dictionaryLiteralForFieldArguments(args)}`, `type: ${this.helpers.fieldTypeEnum(type, structName)}`, ], ', ' @@ -724,16 +645,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { } case 'BooleanCondition': this.printOnNewline(`GraphQLBooleanCondition(`); - this.print( - join( - [ - `variableName: "${selection.variableName}"`, - `inverted: ${selection.inverted}`, - 'selections: ', - ], - ', ' - ) - ); + this.print(join([`variableName: "${selection.variableName}"`, `inverted: ${selection.inverted}`, 'selections: '], ', ')); this.selectionSetInitialization(selection.selectionSet); this.print('),'); break; @@ -741,13 +653,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline(`GraphQLTypeCondition(`); this.print( join( - [ - `possibleTypes: [${join( - selection.selectionSet.possibleTypes.map(type => `"${type.name}"`), - ', ' - )}]`, - 'selections: ', - ], + [`possibleTypes: [${join(selection.selectionSet.possibleTypes.map(type => `"${type.name}"`), ', ')}]`, 'selections: '], ', ' ) ); @@ -780,18 +686,14 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printNewlineIfNeeded(); this.comment(description || undefined); - this.printOnNewline( - `public enum ${name}: RawRepresentable, Equatable, JSONDecodable, JSONEncodable` - ); + this.printOnNewline(`public enum ${name}: RawRepresentable, Equatable, JSONDecodable, JSONEncodable`); this.withinBlock(() => { this.printOnNewline('public typealias RawValue = String'); values.forEach(value => { this.comment(value.description || undefined); this.deprecationAttributes(value.isDeprecated, value.deprecationReason || undefined); - this.printOnNewline( - `case ${escapeIdentifierIfNeeded(this.helpers.enumCaseName(value.name))}` - ); + this.printOnNewline(`case ${escapeIdentifierIfNeeded(this.helpers.enumCaseName(value.name))}`); }); this.comment('Auto generated constant for unknown enum values'); this.printOnNewline('case unknown(RawValue)'); @@ -802,11 +704,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline('switch rawValue'); this.withinBlock(() => { values.forEach(value => { - this.printOnNewline( - `case "${value.value}": self = ${escapeIdentifierIfNeeded( - this.helpers.enumDotCaseName(value.name) - )}` - ); + this.printOnNewline(`case "${value.value}": self = ${escapeIdentifierIfNeeded(this.helpers.enumDotCaseName(value.name))}`); }); this.printOnNewline(`default: self = .unknown(rawValue)`); }); @@ -818,11 +716,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline('switch self'); this.withinBlock(() => { values.forEach(value => { - this.printOnNewline( - `case ${escapeIdentifierIfNeeded( - this.helpers.enumDotCaseName(value.name) - )}: return "${value.value}"` - ); + this.printOnNewline(`case ${escapeIdentifierIfNeeded(this.helpers.enumDotCaseName(value.name))}: return "${value.value}"`); }); this.printOnNewline(`case .unknown(let value): return value`); }); @@ -834,15 +728,11 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline('switch (lhs, rhs)'); this.withinBlock(() => { values.forEach(value => { - const enumDotCaseName = escapeIdentifierIfNeeded( - this.helpers.enumDotCaseName(value.name) - ); + const enumDotCaseName = escapeIdentifierIfNeeded(this.helpers.enumDotCaseName(value.name)); const tuple = `(${enumDotCaseName}, ${enumDotCaseName})`; this.printOnNewline(`case ${tuple}: return true`); }); - this.printOnNewline( - `case (.unknown(let lhsValue), .unknown(let rhsValue)): return lhsValue == rhsValue` - ); + this.printOnNewline(`case (.unknown(let lhsValue), .unknown(let rhsValue)): return lhsValue == rhsValue`); this.printOnNewline(`default: return false`); }); }); @@ -885,58 +775,47 @@ export class SwiftAPIGenerator extends SwiftGenerator { ]; } - this.structDeclaration( - { structName, description: description || undefined, adoptedProtocols }, - () => { - this.printOnNewline(`public var graphQLMap: GraphQLMap`); + this.structDeclaration({ structName, description: description || undefined, adoptedProtocols }, () => { + this.printOnNewline(`public var graphQLMap: GraphQLMap`); - this.printNewlineIfNeeded(); - this.printOnNewline(`public init`); - this.print('('); - this.print( - join( - properties.map(({ propertyName, typeName, isOptional }) => - join([ - `${escapeIdentifierIfNeeded(propertyName)}: ${typeName}`, - isOptional && ' = nil', - ]) - ), - ', ' + this.printNewlineIfNeeded(); + this.printOnNewline(`public init`); + this.print('('); + this.print( + join( + properties.map(({ propertyName, typeName, isOptional }) => + join([`${escapeIdentifierIfNeeded(propertyName)}: ${typeName}`, isOptional && ' = nil']) + ), + ', ' + ) + ); + this.print(')'); + + this.withinBlock(() => { + this.printOnNewline( + wrap( + `graphQLMap = [`, + join(properties.map(({ name, propertyName }) => `"${name}": ${escapeIdentifierIfNeeded(propertyName)}`), ', ') || ':', + `]` ) ); - this.print(')'); + }); + for (const { name, propertyName, typeName, description } of properties) { + this.printNewlineIfNeeded(); + this.comment(description || undefined); + this.printOnNewline(`public var ${escapeIdentifierIfNeeded(propertyName)}: ${typeName}`); this.withinBlock(() => { - this.printOnNewline( - wrap( - `graphQLMap = [`, - join( - properties.map( - ({ name, propertyName }) => `"${name}": ${escapeIdentifierIfNeeded(propertyName)}` - ), - ', ' - ) || ':', - `]` - ) - ); - }); - - for (const { name, propertyName, typeName, description } of properties) { - this.printNewlineIfNeeded(); - this.comment(description || undefined); - this.printOnNewline(`public var ${escapeIdentifierIfNeeded(propertyName)}: ${typeName}`); + this.printOnNewline('get'); this.withinBlock(() => { - this.printOnNewline('get'); - this.withinBlock(() => { - this.printOnNewline(`return graphQLMap["${name}"] as! ${typeName}`); - }); - this.printOnNewline('set'); - this.withinBlock(() => { - this.printOnNewline(`graphQLMap.updateValue(newValue, forKey: "${name}")`); - }); + this.printOnNewline(`return graphQLMap["${name}"] as! ${typeName}`); }); - } + this.printOnNewline('set'); + this.withinBlock(() => { + this.printOnNewline(`graphQLMap.updateValue(newValue, forKey: "${name}")`); + }); + }); } - ); + }); } } diff --git a/packages/amplify-graphql-types-generator/src/swift/helpers.ts b/packages/amplify-graphql-types-generator/src/swift/helpers.ts index 96fbfe572c..06894897fd 100644 --- a/packages/amplify-graphql-types-generator/src/swift/helpers.ts +++ b/packages/amplify-graphql-types-generator/src/swift/helpers.ts @@ -12,7 +12,7 @@ import { getNamedType, GraphQLInputField, isNonNullType, - isListType + isListType, } from 'graphql'; import { camelCase, pascalCase } from 'change-case'; @@ -32,7 +32,7 @@ const builtInScalarMap = { [GraphQLInt.name]: 'Int', [GraphQLFloat.name]: 'Double', [GraphQLBoolean.name]: 'Bool', - [GraphQLID.name]: 'GraphQLID' + [GraphQLID.name]: 'GraphQLID', }; const INFLECTOR_BLACK_LIST = ['delta']; @@ -48,11 +48,7 @@ export class Helpers { // Types - typeNameFromGraphQLType( - type: GraphQLType, - unmodifiedTypeName?: string, - isOptional?: boolean - ): string { + typeNameFromGraphQLType(type: GraphQLType, unmodifiedTypeName?: string, isOptional?: boolean): string { if (isNonNullType(type)) { return this.typeNameFromGraphQLType(type.ofType, unmodifiedTypeName, false); } else if (isOptional === undefined) { @@ -152,7 +148,7 @@ export class Helpers { propertyName, typeName, structName, - isOptional + isOptional, }); } @@ -162,21 +158,18 @@ export class Helpers { return Object.assign(variant, { propertyName: camelCase(structName), typeName: structName + '?', - structName + structName, }); } - propertyFromFragmentSpread( - fragmentSpread: FragmentSpread, - isConditional: boolean - ): FragmentSpread & Property & Struct { + propertyFromFragmentSpread(fragmentSpread: FragmentSpread, isConditional: boolean): FragmentSpread & Property & Struct { const structName = this.structNameForFragmentName(fragmentSpread.fragmentName); return Object.assign({}, fragmentSpread, { propertyName: camelCase(fragmentSpread.fragmentName), typeName: isConditional ? structName + '?' : structName, structName, - isConditional + isConditional, }); } @@ -188,15 +181,12 @@ export class Helpers { typeName: this.typeNameFromGraphQLType(field.type), isOptional: !(field.type instanceof GraphQLNonNull), description: field.description || null, - name: field.name + name: field.name, } ); } - propertiesForSelectionSet( - selectionSet: SelectionSet, - namespace?: string - ): (Field & Property)[] | undefined { + propertiesForSelectionSet(selectionSet: SelectionSet, namespace?: string): (Field & Property)[] | undefined { const properties = collectAndMergeFields(selectionSet, true) .filter(field => field.name !== '__typename') .map(field => this.propertyFromField(field, namespace)); @@ -249,11 +239,7 @@ export class Helpers { ); } - mapExpressionForType( - type: GraphQLType, - expression: (identifier: string) => string, - identifier = '' - ): string { + mapExpressionForType(type: GraphQLType, expression: (identifier: string) => string, identifier = ''): string { let isOptional; if (isNonNullType(type)) { isOptional = false; @@ -264,11 +250,7 @@ export class Helpers { if (isListType(type)) { if (isOptional) { - return `${identifier}.flatMap { $0.map { ${this.mapExpressionForType( - type.ofType, - expression, - '$0' - )} } }`; + return `${identifier}.flatMap { $0.map { ${this.mapExpressionForType(type.ofType, expression, '$0')} } }`; } else { return `${identifier}.map { ${this.mapExpressionForType(type.ofType, expression, '$0')} }`; } diff --git a/packages/amplify-graphql-types-generator/src/swift/language.ts b/packages/amplify-graphql-types-generator/src/swift/language.ts index 926acb74ea..03df6dd044 100644 --- a/packages/amplify-graphql-types-generator/src/swift/language.ts +++ b/packages/amplify-graphql-types-generator/src/swift/language.ts @@ -70,8 +70,8 @@ export class SwiftGenerator extends CodeGenerator 0) ? deprecationReason : "" - this.printOnNewline(`@available(*, deprecated, message: "${escapedString(deprecationReason)}")`) + deprecationReason = deprecationReason !== undefined && deprecationReason.length > 0 ? deprecationReason : ''; + this.printOnNewline(`@available(*, deprecated, message: "${escapedString(deprecationReason)}")`); } } diff --git a/packages/amplify-graphql-types-generator/src/typescript/codeGeneration.ts b/packages/amplify-graphql-types-generator/src/typescript/codeGeneration.ts index 8274a5427f..660aed5060 100644 --- a/packages/amplify-graphql-types-generator/src/typescript/codeGeneration.ts +++ b/packages/amplify-graphql-types-generator/src/typescript/codeGeneration.ts @@ -12,25 +12,16 @@ import { GraphQLInterfaceType, GraphQLObjectType, isListType, - isNonNullType -} from 'graphql' + isNonNullType, +} from 'graphql'; -import { - wrap -} from '../utilities/printing'; +import { wrap } from '../utilities/printing'; import CodeGenerator from '../utilities/CodeGenerator'; -import { - interfaceDeclaration, - propertyDeclaration, - propertySetsDeclaration, - Property -} from './language'; +import { interfaceDeclaration, propertyDeclaration, propertySetsDeclaration, Property } from './language'; -import { - typeNameFromGraphQLType, -} from './types'; +import { typeNameFromGraphQLType } from './types'; import Maybe from 'graphql/tsutils/Maybe'; export function generateSource(context: LegacyCompilerContext) { @@ -39,16 +30,12 @@ export function generateSource(context: LegacyCompilerContext) { generator.printOnNewline('/* tslint:disable */'); generator.printOnNewline('// This file was automatically generated and should not be edited.'); - context.typesUsed.forEach(type => - typeDeclarationForGraphQLType(generator, type) - ); + context.typesUsed.forEach(type => typeDeclarationForGraphQLType(generator, type)); Object.values(context.operations).forEach(operation => { interfaceVariablesDeclarationForOperation(generator, operation); interfaceDeclarationForOperation(generator, operation); }); - Object.values(context.fragments).forEach(operation => - interfaceDeclarationForFragment(generator, operation) - ); + Object.values(context.fragments).forEach(operation => interfaceDeclarationForFragment(generator, operation)); generator.printNewline(); @@ -69,43 +56,42 @@ function enumerationDeclaration(generator: CodeGenerator, type: GraphQLEnumType) generator.printNewlineIfNeeded(); if (description) { - description.split('\n') - .forEach(line => { - generator.printOnNewline(`// ${line.trim()}`); - }) + description.split('\n').forEach(line => { + generator.printOnNewline(`// ${line.trim()}`); + }); } generator.printOnNewline(`export enum ${name} {`); - values.forEach((value) => { + values.forEach(value => { if (!value.description || value.description.indexOf('\n') === -1) { - generator.printOnNewline(` ${value.value} = "${value.value}",${wrap(' // ', value.description || '')}`) + generator.printOnNewline(` ${value.value} = "${value.value}",${wrap(' // ', value.description || '')}`); } else { if (value.description) { - value.description.split('\n') - .forEach(line => { - generator.printOnNewline(` // ${line.trim()}`); - }) + value.description.split('\n').forEach(line => { + generator.printOnNewline(` // ${line.trim()}`); + }); } - generator.printOnNewline(` ${value.value} = "${value.value}",`) + generator.printOnNewline(` ${value.value} = "${value.value}",`); } }); generator.printOnNewline(`}`); generator.printNewline(); } -function structDeclarationForInputObjectType( - generator: CodeGenerator, - type: GraphQLInputObjectType -) { +function structDeclarationForInputObjectType(generator: CodeGenerator, type: GraphQLInputObjectType) { const interfaceName = type.name; - interfaceDeclaration(generator, { - interfaceName, - }, () => { - const properties = propertiesFromFields(generator.context, Object.values(type.getFields())); - propertyDeclarations(generator, properties, true); - }); + interfaceDeclaration( + generator, + { + interfaceName, + }, + () => { + const properties = propertiesFromFields(generator.context, Object.values(type.getFields())); + propertyDeclarations(generator, properties, true); + } + ); } -export function interfaceNameFromOperation({ operationName, operationType }: { operationName: string, operationType: string }) { +export function interfaceNameFromOperation({ operationName, operationType }: { operationName: string; operationType: string }) { switch (operationType) { case 'query': return `${operationName}Query`; @@ -123,23 +109,23 @@ export function interfaceNameFromOperation({ operationName, operationType }: { o export function interfaceVariablesDeclarationForOperation( generator: CodeGenerator, - { - operationName, - operationType, - variables - }: LegacyOperation + { operationName, operationType, variables }: LegacyOperation ) { if (!variables || variables.length < 1) { return; } const interfaceName = `${interfaceNameFromOperation({ operationName, operationType })}Variables`; - interfaceDeclaration(generator, { - interfaceName, - }, () => { - const properties = propertiesFromFields(generator.context, variables); - propertyDeclarations(generator, properties, true); - }); + interfaceDeclaration( + generator, + { + interfaceName, + }, + () => { + const properties = propertiesFromFields(generator.context, variables); + propertyDeclarations(generator, properties, true); + } + ); } function getObjectTypeName(type: GraphQLType): string { @@ -153,77 +139,73 @@ function getObjectTypeName(type: GraphQLType): string { return `"${type.name}"`; } if (type instanceof GraphQLUnionType) { - return type.getTypes().map(type => getObjectTypeName(type)).join(" | "); + return type + .getTypes() + .map(type => getObjectTypeName(type)) + .join(' | '); } return `"${type.name}"`; } export function updateTypeNameField(rootField: LegacyField): LegacyField { - const fields = rootField.fields && rootField.fields.map(field => { - if (field.fieldName === '__typename') { - const objectTypeName = getObjectTypeName(rootField.type); - return { - ...field, - typeName: objectTypeName, - type: { name: objectTypeName }, - }; - } + const fields = + rootField.fields && + rootField.fields.map(field => { + if (field.fieldName === '__typename') { + const objectTypeName = getObjectTypeName(rootField.type); + return { + ...field, + typeName: objectTypeName, + type: { name: objectTypeName }, + }; + } - if (field.fields) { - return updateTypeNameField(field); - } + if (field.fields) { + return updateTypeNameField(field); + } - return field; - }); + return field; + }); return { ...rootField, fields, } as LegacyField; } -export function interfaceDeclarationForOperation( - generator: CodeGenerator, - { - operationName, - operationType, - fields - }: LegacyOperation -) { +export function interfaceDeclarationForOperation(generator: CodeGenerator, { operationName, operationType, fields }: LegacyOperation) { const interfaceName = interfaceNameFromOperation({ operationName, operationType }); fields = fields.map(field => updateTypeNameField(field)); const properties = propertiesFromFields(generator.context, fields); - interfaceDeclaration(generator, { - interfaceName, - }, () => { - propertyDeclarations(generator, properties); - }); + interfaceDeclaration( + generator, + { + interfaceName, + }, + () => { + propertyDeclarations(generator, properties); + } + ); } -export function interfaceDeclarationForFragment( - generator: CodeGenerator, - fragment: LegacyFragment -) { - const { - fragmentName, - typeCondition, - fields, - inlineFragments - } = fragment; +export function interfaceDeclarationForFragment(generator: CodeGenerator, fragment: LegacyFragment) { + const { fragmentName, typeCondition, fields, inlineFragments } = fragment; const interfaceName = `${fragmentName}Fragment`; - interfaceDeclaration(generator, { - interfaceName, - noBrackets: isAbstractType(typeCondition) - }, () => { - if (isAbstractType(typeCondition)) { - const propertySets = fragment.possibleTypes - .map(type => { + interfaceDeclaration( + generator, + { + interfaceName, + noBrackets: isAbstractType(typeCondition), + }, + () => { + if (isAbstractType(typeCondition)) { + const propertySets = fragment.possibleTypes.map(type => { // NOTE: inlineFragment currently consists of the merged fields // from both inline fragments and fragment spreads. // TODO: Rename inlineFragments in the IR. const inlineFragment = inlineFragments.find(inlineFragment => { - return inlineFragment.typeCondition.toString() == type.toString() + return inlineFragment.typeCondition.toString() == type.toString(); }); if (inlineFragment) { @@ -232,8 +214,8 @@ export function interfaceDeclarationForFragment( return { ...field, typeName: `"${inlineFragment.typeCondition}"`, - type: { name: `"${inlineFragment.typeCondition}"` } as GraphQLType - } + type: { name: `"${inlineFragment.typeCondition}"` } as GraphQLType, + }; } else { return field; } @@ -246,8 +228,8 @@ export function interfaceDeclarationForFragment( return { ...field, typeName: `"${type}"`, - type: { name: `"${type}"` } as GraphQLType - } + type: { name: `"${type}"` } as GraphQLType, + }; } else { return field; } @@ -257,48 +239,55 @@ export function interfaceDeclarationForFragment( } }); - propertySetsDeclaration(generator, fragment, propertySets, true); - } else { - const fragmentFields = fields.map(field => { - if (field.fieldName === '__typename') { - return { - ...field, - typeName: `"${fragment.typeCondition}"`, - type: { name: `"${fragment.typeCondition}"` } as GraphQLType + propertySetsDeclaration(generator, fragment, propertySets, true); + } else { + const fragmentFields = fields.map(field => { + if (field.fieldName === '__typename') { + return { + ...field, + typeName: `"${fragment.typeCondition}"`, + type: { name: `"${fragment.typeCondition}"` } as GraphQLType, + }; + } else { + return field; } - } else { - return field; - } - }); + }); - const properties = propertiesFromFields(generator.context, fragmentFields) - propertyDeclarations(generator, properties); + const properties = propertiesFromFields(generator.context, fragmentFields); + propertyDeclarations(generator, properties); + } } - }); + ); } -export function propertiesFromFields(context: LegacyCompilerContext, fields: { - name?: string, - type: GraphQLType, - responseName?: string, - description?: Maybe, - fragmentSpreads?: any, - inlineFragments?: LegacyInlineFragment[], - fieldName?: string -}[]) { +export function propertiesFromFields( + context: LegacyCompilerContext, + fields: { + name?: string; + type: GraphQLType; + responseName?: string; + description?: Maybe; + fragmentSpreads?: any; + inlineFragments?: LegacyInlineFragment[]; + fieldName?: string; + }[] +) { return fields.map(field => propertyFromField(context, field)); } -export function propertyFromField(context: LegacyCompilerContext, field: { - name?: string, - type: GraphQLType, - fields?: any[], - responseName?: string, - description?: Maybe, - fragmentSpreads?: any, - inlineFragments?: LegacyInlineFragment[], - fieldName?: string -}): Property { +export function propertyFromField( + context: LegacyCompilerContext, + field: { + name?: string; + type: GraphQLType; + fields?: any[]; + responseName?: string; + description?: Maybe; + fragmentSpreads?: any; + inlineFragments?: LegacyInlineFragment[]; + fieldName?: string; + } +): Property { let { name: fieldName, type: fieldType, description, fragmentSpreads, inlineFragments } = field; fieldName = fieldName || field.responseName; @@ -320,8 +309,8 @@ export function propertyFromField(context: LegacyCompilerContext, field: { if (isListType(fieldType)) { isArray = true; isArrayElementNullable = !(fieldType.ofType instanceof GraphQLNonNull); - } else if (isNonNullType(fieldType) && isListType(fieldType.ofType)) { - isArray = true + } else if (isNonNullType(fieldType) && isListType(fieldType.ofType)) { + isArray = true; isArrayElementNullable = !(fieldType.ofType.ofType instanceof GraphQLNonNull); } @@ -330,8 +319,12 @@ export function propertyFromField(context: LegacyCompilerContext, field: { typeName, fields: field.fields, isComposite: true, - fragmentSpreads, inlineFragments, fieldType, - isArray, isNullable, isArrayElementNullable, + fragmentSpreads, + inlineFragments, + fieldType, + isArray, + isNullable, + isArrayElementNullable, }; } else { if (field.fieldName === '__typename') { @@ -345,52 +338,53 @@ export function propertyFromField(context: LegacyCompilerContext, field: { } export function propertyDeclarations(generator: CodeGenerator, properties: Property[], isInput = false) { - if (!properties) return; properties.forEach(property => { if (isAbstractType(getNamedType(property.type || property.fieldType!))) { - const propertySets = getPossibleTypeNames(generator, property) - .map(type => { - const inlineFragment = property.inlineFragments && property.inlineFragments.find(inlineFragment => { - return inlineFragment.typeCondition.toString() == type + const propertySets = getPossibleTypeNames(generator, property).map(type => { + const inlineFragment = + property.inlineFragments && + property.inlineFragments.find(inlineFragment => { + return inlineFragment.typeCondition.toString() == type; }); - if (inlineFragment) { - const fields = inlineFragment.fields.map(field => { - if (field.fieldName === '__typename') { - return { - ...field, - typeName: `"${inlineFragment.typeCondition}"`, - type: { name: `"${inlineFragment.typeCondition}"` } as GraphQLType - } - } else { - return field; - } - }); + if (inlineFragment) { + const fields = inlineFragment.fields.map(field => { + if (field.fieldName === '__typename') { + return { + ...field, + typeName: `"${inlineFragment.typeCondition}"`, + type: { name: `"${inlineFragment.typeCondition}"` } as GraphQLType, + }; + } else { + return field; + } + }); - return propertiesFromFields(generator.context, fields); - } else { - const fields = property.fields!.map(field => { - if (field.fieldName === '__typename') { - return { - ...field, - typeName: `"${type}"`, - type: { name: `"${type}"` } as GraphQLType - } - } else { - return field; - } - }); + return propertiesFromFields(generator.context, fields); + } else { + const fields = property.fields!.map(field => { + if (field.fieldName === '__typename') { + return { + ...field, + typeName: `"${type}"`, + type: { name: `"${type}"` } as GraphQLType, + }; + } else { + return field; + } + }); - return propertiesFromFields(generator.context, fields); - } - }); + return propertiesFromFields(generator.context, fields); + } + }); propertySetsDeclaration(generator, property, propertySets); } else { - if (property.fields && property.fields.length > 0 - || property.inlineFragments && property.inlineFragments.length > 0 - || property.fragmentSpreads && property.fragmentSpreads.length > 0 + if ( + (property.fields && property.fields.length > 0) || + (property.inlineFragments && property.inlineFragments.length > 0) || + (property.fragmentSpreads && property.fragmentSpreads.length > 0) ) { propertyDeclaration(generator, property, () => { const properties = propertiesFromFields(generator.context, property.fields!); diff --git a/packages/amplify-graphql-types-generator/src/typescript/language.ts b/packages/amplify-graphql-types-generator/src/typescript/language.ts index 2d41f42969..ba4a8c0d55 100644 --- a/packages/amplify-graphql-types-generator/src/typescript/language.ts +++ b/packages/amplify-graphql-types-generator/src/typescript/language.ts @@ -3,33 +3,32 @@ import { LegacyInlineFragment } from '../compiler/legacyIR'; import { propertyDeclarations } from './codeGeneration'; import { typeNameFromGraphQLType } from './types'; -import CodeGenerator from "../utilities/CodeGenerator"; -import { GraphQLType } from "graphql"; +import CodeGenerator from '../utilities/CodeGenerator'; +import { GraphQLType } from 'graphql'; import Maybe from 'graphql/tsutils/Maybe'; export interface Property { - fieldName?: string, - fieldType?: GraphQLType, - propertyName?: string, - type?: GraphQLType, - description?: Maybe, - typeName?: string, - isComposite?: boolean, - isNullable?: boolean, - fields?: any[], - inlineFragments?: LegacyInlineFragment[], - fragmentSpreads?: any, - isInput?: boolean, - isArray?: boolean, - isArrayElementNullable?: boolean | null, + fieldName?: string; + fieldType?: GraphQLType; + propertyName?: string; + type?: GraphQLType; + description?: Maybe; + typeName?: string; + isComposite?: boolean; + isNullable?: boolean; + fields?: any[]; + inlineFragments?: LegacyInlineFragment[]; + fragmentSpreads?: any; + isInput?: boolean; + isArray?: boolean; + isArrayElementNullable?: boolean | null; } - -export function interfaceDeclaration(generator: CodeGenerator, { - interfaceName, - noBrackets -}: { interfaceName: string, noBrackets?: boolean }, - closure: () => void) { +export function interfaceDeclaration( + generator: CodeGenerator, + { interfaceName, noBrackets }: { interfaceName: string; noBrackets?: boolean }, + closure: () => void +) { generator.printNewlineIfNeeded(); generator.printNewline(); generator.print(`export type ${interfaceName} = `); @@ -43,24 +42,17 @@ export function interfaceDeclaration(generator: CodeGenerator, { generator.print(';'); } -export function propertyDeclaration(generator: CodeGenerator, { - fieldName, - type, - propertyName, - typeName, - description, - isInput, - isArray, - isNullable, - isArrayElementNullable -}: Property, closure?: () => void) { +export function propertyDeclaration( + generator: CodeGenerator, + { fieldName, type, propertyName, typeName, description, isInput, isArray, isNullable, isArrayElementNullable }: Property, + closure?: () => void +) { const name = fieldName || propertyName; if (description) { - description.split('\n') - .forEach(line => { - generator.printOnNewline(`// ${line.trim()}`); - }) + description.split('\n').forEach(line => { + generator.printOnNewline(`// ${line.trim()}`); + }); } if (closure) { @@ -90,29 +82,24 @@ export function propertyDeclaration(generator: CodeGenerator, { if (isNullable) { generator.print(' | null'); } - } else { generator.printOnNewline(name); if (isInput && isNullable) { - generator.print('?') + generator.print('?'); } - generator.print(`: ${typeName || type && typeNameFromGraphQLType(generator.context, type)}`); + generator.print(`: ${typeName || (type && typeNameFromGraphQLType(generator.context, type))}`); } generator.print(','); } export function propertySetsDeclaration(generator: CodeGenerator, property: Property, propertySets: Property[][], standalone = false) { - const { - description, fieldName, propertyName, - isNullable, isArray, isArrayElementNullable, - } = property; + const { description, fieldName, propertyName, isNullable, isArray, isArrayElementNullable } = property; const name = fieldName || propertyName; if (description) { - description.split('\n') - .forEach(line => { - generator.printOnNewline(`// ${line.trim()}`); - }) + description.split('\n').forEach(line => { + generator.printOnNewline(`// ${line.trim()}`); + }); } if (!standalone) { @@ -125,16 +112,20 @@ export function propertySetsDeclaration(generator: CodeGenerator, property: Prop generator.pushScope({ typeName: name }); - generator.withinBlock(() => { - propertySets.forEach((propertySet, index, propertySets) => { - generator.withinBlock(() => { - propertyDeclarations(generator, propertySet); + generator.withinBlock( + () => { + propertySets.forEach((propertySet, index, propertySets) => { + generator.withinBlock(() => { + propertyDeclarations(generator, propertySet); + }); + if (index !== propertySets.length - 1) { + generator.print(' |'); + } }); - if (index !== propertySets.length - 1) { - generator.print(' |'); - } - }) - }, '(', ')'); + }, + '(', + ')' + ); generator.popScope(); @@ -154,21 +145,25 @@ export function propertySetsDeclaration(generator: CodeGenerator, property: Prop } } -export function methodDeclaration(generator: CodeGenerator, { - methodName, - returnType, - async, - args, -}: { - methodName:string, - returnType: string, - async: boolean, - args: Array -}, closure: () => void) { +export function methodDeclaration( + generator: CodeGenerator, + { + methodName, + returnType, + async, + args, + }: { + methodName: string; + returnType: string; + async: boolean; + args: Array; + }, + closure: () => void +) { generator.printNewline(); if (async) generator.print('async '); generator.print(`${methodName}(${args.join(', ')}):${returnType}`); generator.pushScope({ methodName }); generator.withinBlock(closure, '{', '}'); generator.popScope(); -} \ No newline at end of file +} diff --git a/packages/amplify-graphql-types-generator/src/typescript/types.ts b/packages/amplify-graphql-types-generator/src/typescript/types.ts index 794a1afdd9..316f586ab7 100644 --- a/packages/amplify-graphql-types-generator/src/typescript/types.ts +++ b/packages/amplify-graphql-types-generator/src/typescript/types.ts @@ -9,7 +9,7 @@ import { GraphQLScalarType, GraphQLType, isNonNullType, - isListType + isListType, } from 'graphql'; const builtInScalarMap = { @@ -18,16 +18,20 @@ const builtInScalarMap = { [GraphQLFloat.name]: 'number', [GraphQLBoolean.name]: 'boolean', [GraphQLID.name]: 'string', -} +}; const appSyncScalars: any = { AWSTimestamp: 'number', -} - +}; -export function typeNameFromGraphQLType(context: LegacyCompilerContext, type: GraphQLType, bareTypeName?: string | null, nullable = true): string { +export function typeNameFromGraphQLType( + context: LegacyCompilerContext, + type: GraphQLType, + bareTypeName?: string | null, + nullable = true +): string { if (isNonNullType(type)) { - return typeNameFromGraphQLType(context, type.ofType, bareTypeName, false) + return typeNameFromGraphQLType(context, type.ofType, bareTypeName, false); } let typeName; @@ -37,9 +41,7 @@ export function typeNameFromGraphQLType(context: LegacyCompilerContext, type: Gr typeName = builtInScalarMap[type.name] || appSyncScalars[type.name] || - (context.options.passthroughCustomScalars - ? context.options.customScalarsPrefix + type.name - : builtInScalarMap[GraphQLString.name]); + (context.options.passthroughCustomScalars ? context.options.customScalarsPrefix + type.name : builtInScalarMap[GraphQLString.name]); } else { typeName = bareTypeName || type.name; } diff --git a/packages/amplify-graphql-types-generator/src/utilities/CodeGenerator.ts b/packages/amplify-graphql-types-generator/src/utilities/CodeGenerator.ts index ba8bebadcd..b0c7938c7e 100644 --- a/packages/amplify-graphql-types-generator/src/utilities/CodeGenerator.ts +++ b/packages/amplify-graphql-types-generator/src/utilities/CodeGenerator.ts @@ -1,8 +1,7 @@ export interface BasicGeneratedFile { - output: string + output: string; } - export class GeneratedFile implements BasicGeneratedFile { scopeStack: Scope[] = []; indentWidth = 2; diff --git a/packages/amplify-graphql-types-generator/src/utilities/complextypes.ts b/packages/amplify-graphql-types-generator/src/utilities/complextypes.ts index 913b2e07ea..51d9dbad0b 100644 --- a/packages/amplify-graphql-types-generator/src/utilities/complextypes.ts +++ b/packages/amplify-graphql-types-generator/src/utilities/complextypes.ts @@ -1,6 +1,6 @@ import { GraphQLType, isInputObjectType, getNamedType, isObjectType } from 'graphql'; -const S3_FIELD_NAMES = ['bucket', 'key', 'region' ]; +const S3_FIELD_NAMES = ['bucket', 'key', 'region']; export function hasS3Fields(input: GraphQLType): boolean { if (isObjectType(input) || isInputObjectType(input)) { @@ -8,8 +8,7 @@ export function hasS3Fields(input: GraphQLType): boolean { return true; } const fields = input.getFields(); - return Object.keys(fields) - .some(f => hasS3Fields((fields[f]) as GraphQLType)); + return Object.keys(fields).some(f => hasS3Fields((fields[f]) as GraphQLType)); } return false; } diff --git a/packages/amplify-graphql-types-generator/src/utilities/graphql.ts b/packages/amplify-graphql-types-generator/src/utilities/graphql.ts index 4ea5ddcec3..32d301033c 100644 --- a/packages/amplify-graphql-types-generator/src/utilities/graphql.ts +++ b/packages/amplify-graphql-types-generator/src/utilities/graphql.ts @@ -24,14 +24,11 @@ import { DocumentNode, DirectiveNode, isListType, - isNonNullType + isNonNullType, } from 'graphql'; declare module 'graphql/utilities/buildASTSchema' { - function buildASTSchema( - ast: DocumentNode, - options?: { assumeValid?: boolean; commentDescriptions?: boolean } - ): GraphQLSchema; + function buildASTSchema(ast: DocumentNode, options?: { assumeValid?: boolean; commentDescriptions?: boolean }): GraphQLSchema; } export function sortEnumValues(values: GraphQLEnumValue[]): GraphQLEnumValue[] { @@ -51,29 +48,28 @@ export function removeConnectionDirectives(ast: ASTNode) { Directive(node: DirectiveNode): DirectiveNode | null { if (node.name.value === 'connection') return null; return node; - } + }, }); } export function removeClientDirectives(ast: ASTNode) { return visit(ast, { Field(node: FieldNode): FieldNode | null { - if (node.directives && node.directives.find(directive => directive.name.value === 'client')) - return null; + if (node.directives && node.directives.find(directive => directive.name.value === 'client')) return null; return node; }, OperationDefinition: { leave(node: OperationDefinitionNode): OperationDefinitionNode | null { if (!node.selectionSet.selections.length) return null; return node; - } - } + }, + }, }); } const typenameField = { kind: Kind.FIELD, - name: { kind: Kind.NAME, value: '__typename' } + name: { kind: Kind.NAME, value: '__typename' }, }; export function withTypenameFieldAddedWhereNeeded(ast: ASTNode) { @@ -83,11 +79,10 @@ export function withTypenameFieldAddedWhereNeeded(ast: ASTNode) { return { ...node, selections: node.selections.filter( - selection => - !(selection.kind === 'Field' && (selection as FieldNode).name.value === '__typename') - ) + selection => !(selection.kind === 'Field' && (selection as FieldNode).name.value === '__typename') + ), }; - } + }, }, leave(node: ASTNode) { if (!(node.kind === 'Field' || node.kind === 'FragmentDefinition')) return undefined; @@ -98,13 +93,13 @@ export function withTypenameFieldAddedWhereNeeded(ast: ASTNode) { ...node, selectionSet: { ...node.selectionSet, - selections: [typenameField, ...node.selectionSet.selections] - } + selections: [typenameField, ...node.selectionSet.selections], + }, }; } else { return undefined; } - } + }, }); } @@ -120,9 +115,7 @@ export function filePathForNode(node: ASTNode): string { return name; } -export function valueFromValueNode( - valueNode: ValueNode -): any | { kind: 'Variable'; variableName: string } { +export function valueFromValueNode(valueNode: ValueNode): any | { kind: 'Variable'; variableName: string } { switch (valueNode.kind) { case 'IntValue': case 'FloatValue': @@ -146,15 +139,10 @@ export function valueFromValueNode( } } -export function isTypeProperSuperTypeOf( - schema: GraphQLSchema, - maybeSuperType: GraphQLCompositeType, - subType: GraphQLCompositeType -) { +export function isTypeProperSuperTypeOf(schema: GraphQLSchema, maybeSuperType: GraphQLCompositeType, subType: GraphQLCompositeType) { return ( isEqualType(maybeSuperType, subType) || - (subType instanceof GraphQLObjectType && - (isAbstractType(maybeSuperType) && schema.isPossibleType(maybeSuperType, subType))) + (subType instanceof GraphQLObjectType && (isAbstractType(maybeSuperType) && schema.isPossibleType(maybeSuperType, subType))) ); } @@ -203,9 +191,7 @@ export function getFieldDef( } if ( name === TypeNameMetaFieldDef.name && - (parentType instanceof GraphQLObjectType || - parentType instanceof GraphQLInterfaceType || - parentType instanceof GraphQLUnionType) + (parentType instanceof GraphQLObjectType || parentType instanceof GraphQLInterfaceType || parentType instanceof GraphQLUnionType) ) { return TypeNameMetaFieldDef; } diff --git a/packages/amplify-graphql-types-generator/src/utilities/printing.ts b/packages/amplify-graphql-types-generator/src/utilities/printing.ts index d021015327..b0571d197f 100644 --- a/packages/amplify-graphql-types-generator/src/utilities/printing.ts +++ b/packages/amplify-graphql-types-generator/src/utilities/printing.ts @@ -13,9 +13,7 @@ export function join(maybeArray?: any[], separator?: string) { * indented "{ }" block. */ export function block(array: any[]) { - return array && array.length !== 0 ? - indent('{\n' + join(array, '\n')) + '\n}' : - '{}'; + return array && array.length !== 0 ? indent('{\n' + join(array, '\n')) + '\n}' : '{}'; } /** @@ -23,9 +21,7 @@ export function block(array: any[]) { * print an empty string. */ export function wrap(start: string, maybeString?: string, end?: string) { - return maybeString ? - start + maybeString + (end || '') : - ''; + return maybeString ? start + maybeString + (end || '') : ''; } export function indent(maybeString?: string) { diff --git a/packages/amplify-graphql-types-generator/src/validation.ts b/packages/amplify-graphql-types-generator/src/validation.ts index 5cb7721932..f070717482 100644 --- a/packages/amplify-graphql-types-generator/src/validation.ts +++ b/packages/amplify-graphql-types-generator/src/validation.ts @@ -7,7 +7,7 @@ import { ValidationContext, GraphQLSchema, DocumentNode, - OperationDefinitionNode + OperationDefinitionNode, } from 'graphql'; import { ToolError, logError } from './errors'; @@ -15,11 +15,7 @@ import { ToolError, logError } from './errors'; export function validateQueryDocument(schema: GraphQLSchema, document: DocumentNode) { const specifiedRulesToBeRemoved = [NoUnusedFragmentsRule]; - const rules = [ - NoAnonymousQueries, - NoTypenameAlias, - ...specifiedRules.filter(rule => !specifiedRulesToBeRemoved.includes(rule)) - ]; + const rules = [NoAnonymousQueries, NoTypenameAlias, ...specifiedRules.filter(rule => !specifiedRulesToBeRemoved.includes(rule))]; const validationErrors = validate(schema, document, rules); if (validationErrors && validationErrors.length > 0) { @@ -37,7 +33,7 @@ export function NoAnonymousQueries(context: ValidationContext) { context.reportError(new GraphQLError('Apollo does not support anonymous operations', [node])); } return false; - } + }, }; } @@ -47,12 +43,9 @@ export function NoTypenameAlias(context: ValidationContext) { const aliasName = node.alias && node.alias.value; if (aliasName == '__typename') { context.reportError( - new GraphQLError( - 'Apollo needs to be able to insert __typename when needed, please do not use it as an alias', - [node] - ) + new GraphQLError('Apollo needs to be able to insert __typename when needed, please do not use it as an alias', [node]) ); } - } + }, }; } diff --git a/packages/amplify-graphql-types-generator/test/angular/index.js b/packages/amplify-graphql-types-generator/test/angular/index.js index 3841e2cb24..f1aca1e87e 100644 --- a/packages/amplify-graphql-types-generator/test/angular/index.js +++ b/packages/amplify-graphql-types-generator/test/angular/index.js @@ -1,18 +1,8 @@ import { stripIndent } from 'common-tags'; -import { - parse, - isType, - GraphQLID, - GraphQLString, - GraphQLInt, - GraphQLList, - GraphQLNonNull -} from 'graphql'; - -import { - generateSource -} from '../../src/angular'; +import { parse, isType, GraphQLID, GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull } from 'graphql'; + +import { generateSource } from '../../src/angular'; import { loadSchema } from '../../src/loading'; const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); @@ -32,22 +22,22 @@ describe('Angular code generation', function() { schema: schema, operations: {}, fragments: {}, - typesUsed: {} - } + typesUsed: {}, + }; generator = new CodeGenerator(context); - compileFromSource = (source) => { + compileFromSource = source => { const document = parse(source); const context = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: true, - addTypename: true + addTypename: true, }); generator.context = context; return context; }; - addFragment = (fragment) => { + addFragment = fragment => { generator.context.fragments[fragment.fragmentName] = fragment; }; diff --git a/packages/amplify-graphql-types-generator/test/compiler/legacyIR.js b/packages/amplify-graphql-types-generator/test/compiler/legacyIR.js index ee91ebd3df..7d977083d6 100644 --- a/packages/amplify-graphql-types-generator/test/compiler/legacyIR.js +++ b/packages/amplify-graphql-types-generator/test/compiler/legacyIR.js @@ -1,18 +1,11 @@ -import { stripIndent } from 'common-tags' +import { stripIndent } from 'common-tags'; -import { - parse, - isType, - GraphQLID, - GraphQLString, - GraphQLList, - GraphQLNonNull -} from 'graphql'; +import { parse, isType, GraphQLID, GraphQLString, GraphQLList, GraphQLNonNull } from 'graphql'; -import { loadSchema } from '../../src/loading' +import { loadSchema } from '../../src/loading'; -import { compileToLegacyIR } from '../../src/compiler/legacyIR' -import { serializeAST } from '../../src/serializeToJSON' +import { compileToLegacyIR } from '../../src/compiler/legacyIR'; +import { serializeAST } from '../../src/serializeToJSON'; function withStringifiedTypes(ir) { return JSON.parse(serializeAST(ir)); @@ -47,24 +40,14 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = withStringifiedTypes(compileToLegacyIR(schema, document)); - expect(operations['HeroName'].variables).toEqual( - [ - { name: 'episode', type: 'Episode' } - ] - ); - - expect(operations['Search'].variables).toEqual( - [ - { name: 'text', type: 'String!' } - ] - ); - - expect(operations['CreateReviewForEpisode'].variables).toEqual( - [ - { name: 'episode', type: 'Episode!' }, - { name: 'review', type: 'ReviewInput!' } - ] - ); + expect(operations['HeroName'].variables).toEqual([{ name: 'episode', type: 'Episode' }]); + + expect(operations['Search'].variables).toEqual([{ name: 'text', type: 'String!' }]); + + expect(operations['CreateReviewForEpisode'].variables).toEqual([ + { name: 'episode', type: 'Episode!' }, + { name: 'review', type: 'ReviewInput!' }, + ]); }); it(`should keep track of enums and input object types used in variables`, () => { @@ -144,7 +127,7 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HeroName'].fields[0].fieldName).toBe("hero"); + expect(operations['HeroName'].fields[0].fieldName).toBe('hero'); }); it(`should include field arguments`, () => { @@ -158,8 +141,7 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HeroName'].fields[0].args) - .toEqual([{ name: "episode", value: "EMPIRE", type: schema.getType("Episode") }]); + expect(operations['HeroName'].fields[0].args).toEqual([{ name: 'episode', value: 'EMPIRE', type: schema.getType('Episode') }]); }); it(`should include isConditional if a field has skip or include directives with variables`, () => { @@ -181,12 +163,12 @@ describe('Compiling query documents to the legacy IR', () => { expect(operations['HeroNameConditionalInclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: true + isConditional: true, }); expect(operations['HeroNameConditionalExclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: true + isConditional: true, }); }); @@ -209,12 +191,12 @@ describe('Compiling query documents to the legacy IR', () => { expect(operations['HeroNameConditionalInclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: false + isConditional: false, }); expect(operations['HeroNameConditionalExclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: false + isConditional: false, }); }); @@ -239,7 +221,7 @@ describe('Compiling query documents to the legacy IR', () => { expect(operations['HeroNameConditionalExclusion'].fields[0].fields).toHaveLength(0); }); - it(`should include isConditional if a field in inside an inline fragment with skip or include directives with variables`, () => { + it(`should include isConditional if a field in inside an inline fragment with skip or include directives with variables`, () => { const document = parse(` query HeroNameConditionalInclusion($includeName: Boolean!) { hero { @@ -262,12 +244,12 @@ describe('Compiling query documents to the legacy IR', () => { expect(operations['HeroNameConditionalInclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: true + isConditional: true, }); expect(operations['HeroNameConditionalExclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: true + isConditional: true, }); }); @@ -294,12 +276,12 @@ describe('Compiling query documents to the legacy IR', () => { expect(operations['HeroNameConditionalInclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: true + isConditional: true, }); expect(operations['HeroNameConditionalExclusion'].fields[0].fields[0]).toMatchObject({ fieldName: 'name', - isConditional: true + isConditional: true, }); expect(operations['HeroNameConditionalInclusion'].fragmentsReferenced).toEqual(['HeroName']); @@ -328,8 +310,7 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['id', 'name', 'appearsIn']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['id', 'name', 'appearsIn']); }); it(`should recursively include fragment spreads with type conditions that match the parent type`, () => { @@ -354,14 +335,11 @@ describe('Compiling query documents to the legacy IR', () => { const { operations, fragments } = compileToLegacyIR(schema, document); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['id', 'name', 'appearsIn']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['id', 'name', 'appearsIn']); - expect(fragments['HeroDetails'].fields.map(field => field.fieldName)) - .toEqual(['name', 'appearsIn', 'id']); + expect(fragments['HeroDetails'].fields.map(field => field.fieldName)).toEqual(['name', 'appearsIn', 'id']); - expect(fragments['MoreHeroDetails'].fields.map(field => field.fieldName)) - .toEqual(['appearsIn']); + expect(fragments['MoreHeroDetails'].fields.map(field => field.fieldName)).toEqual(['appearsIn']); expect(operations['Hero'].fragmentsReferenced).toEqual(['HeroDetails', 'MoreHeroDetails']); expect(operations['Hero'].fields[0].fragmentSpreads).toEqual(['HeroDetails']); @@ -390,13 +368,10 @@ describe('Compiling query documents to the legacy IR', () => { const { operations, fragments } = compileToLegacyIR(schema, document); - expect(operations['HeroAndFriends'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['name', 'id', 'appearsIn', 'friends']); - expect(operations['HeroAndFriends'].fields[0].fields[3].fields.map(field => field.fieldName)) - .toEqual(['id', 'name']); + expect(operations['HeroAndFriends'].fields[0].fields.map(field => field.fieldName)).toEqual(['name', 'id', 'appearsIn', 'friends']); + expect(operations['HeroAndFriends'].fields[0].fields[3].fields.map(field => field.fieldName)).toEqual(['id', 'name']); - expect(fragments['HeroDetails'].fields.map(field => field.fieldName)) - .toEqual(['name', 'id']); + expect(fragments['HeroDetails'].fields.map(field => field.fieldName)).toEqual(['name', 'id']); expect(operations['HeroAndFriends'].fragmentsReferenced).toEqual(['HeroDetails']); expect(operations['HeroAndFriends'].fields[0].fragmentSpreads).toEqual(['HeroDetails']); @@ -419,18 +394,15 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['name']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['name']); - return; + return; - expect(operations['Hero'].fields[0].inlineFragments["Droid"].typeCondition.toString()).toEqual('Droid'); - expect(operations['Hero'].fields[0].inlineFragments["Droid"].fields.map(field => field.fieldName)) - .toEqual(['name', 'primaryFunction']); + expect(operations['Hero'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); + expect(operations['Hero'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name', 'primaryFunction']); - expect(operations['Hero'].fields[0].inlineFragments["Human"].typeCondition.toString()).toEqual('Human'); - expect(operations['Hero'].fields[0].inlineFragments["Human"].fields.map(field => field.fieldName)) - .toEqual(['name', 'height']); + expect(operations['Hero'].fields[0].inlineFragments['Human'].typeCondition.toString()).toEqual('Human'); + expect(operations['Hero'].fields[0].inlineFragments['Human'].fields.map(field => field.fieldName)).toEqual(['name', 'height']); }); it(`should include fragment spreads with type conditions`, () => { @@ -454,16 +426,13 @@ describe('Compiling query documents to the legacy IR', () => { const { operations, fragments } = compileToLegacyIR(schema, document); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['name']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['name']); - expect(operations['Hero'].fields[0].inlineFragments["Droid"].typeCondition.toString()).toEqual('Droid'); - expect(operations['Hero'].fields[0].inlineFragments["Droid"].fields.map(field => field.fieldName)) - .toEqual(['name', 'primaryFunction']); + expect(operations['Hero'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); + expect(operations['Hero'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name', 'primaryFunction']); expect(operations['Hero'].fields[0].inlineFragments['Human'].typeCondition.toString()).toEqual('Human'); - expect(operations['Hero'].fields[0].inlineFragments['Human'].fields.map(field => field.fieldName)) - .toEqual(['name', 'height']); + expect(operations['Hero'].fields[0].inlineFragments['Human'].fields.map(field => field.fieldName)).toEqual(['name', 'height']); expect(operations['Hero'].fragmentsReferenced).toEqual(['DroidDetails', 'HumanDetails']); expect(operations['Hero'].fields[0].fragmentSpreads).toEqual(['DroidDetails', 'HumanDetails']); @@ -485,8 +454,7 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['name']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['name']); expect(operations['Hero'].fields[0].inlineFragments).toEqual([]); }); @@ -511,16 +479,13 @@ describe('Compiling query documents to the legacy IR', () => { const { operations, fragments } = compileToLegacyIR(schema, document); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['name']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['name']); expect(operations['Hero'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); - expect(operations['Hero'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)) - .toEqual(['name', 'primaryFunction']); + expect(operations['Hero'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name', 'primaryFunction']); expect(operations['Hero'].fields[0].inlineFragments['Human'].typeCondition.toString()).toEqual('Human'); - expect(operations['Hero'].fields[0].inlineFragments['Human'].fields.map(field => field.fieldName)) - .toEqual(['name', 'height']); + expect(operations['Hero'].fields[0].inlineFragments['Human'].fields.map(field => field.fieldName)).toEqual(['name', 'height']); expect(operations['Hero'].fragmentsReferenced).toEqual(['HeroDetails']); expect(operations['Hero'].fields[0].fragmentSpreads).toEqual(['HeroDetails']); @@ -541,11 +506,9 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)) - .toEqual([]); + expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)).toEqual([]); expect(operations['HeroName'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); - expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)) - .toEqual(['name']); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name']); }); it(`should not inherit type condition when nesting an inline fragment in an inline fragment with a less specific type condition`, () => { @@ -563,11 +526,9 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)) - .toEqual([]); + expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)).toEqual([]); expect(operations['HeroName'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); - expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)) - .toEqual(['name']); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name']); }); it(`should inherit type condition when nesting a fragment spread in an inline fragment with a more specific type condition`, () => { @@ -587,12 +548,10 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)) - .toEqual([]); - expect(operations['HeroName'].fields[0].inlineFragments["Droid"].typeCondition.toString()).toEqual('Droid'); - expect(operations['HeroName'].fields[0].inlineFragments["Droid"].fields.map(field => field.fieldName)) - .toEqual(['name']); - expect(operations['HeroName'].fields[0].inlineFragments["Droid"].fragmentSpreads).toEqual(['CharacterName']); + expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)).toEqual([]); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name']); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fragmentSpreads).toEqual(['CharacterName']); expect(operations['HeroName'].fragmentsReferenced).toEqual(['CharacterName']); expect(operations['HeroName'].fields[0].fragmentSpreads).toEqual([]); @@ -615,12 +574,10 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)) - .toEqual([]); - expect(operations['HeroName'].fields[0].inlineFragments["Droid"].typeCondition.toString()).toEqual('Droid'); - expect(operations['HeroName'].fields[0].inlineFragments["Droid"].fields.map(field => field.fieldName)) - .toEqual(['name']); - expect(operations['HeroName'].fields[0].inlineFragments["Droid"].fragmentSpreads).toEqual(['DroidName']); + expect(operations['HeroName'].fields[0].fields.map(field => field.fieldName)).toEqual([]); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name']); + expect(operations['HeroName'].fields[0].inlineFragments['Droid'].fragmentSpreads).toEqual(['DroidName']); expect(operations['HeroName'].fragmentsReferenced).toEqual(['DroidName']); expect(operations['HeroName'].fields[0].fragmentSpreads).toEqual(['DroidName']); }); @@ -648,13 +605,10 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HumanAndDroid'].fields.map(field => field.fieldName)) - .toEqual(['human', 'droid']); - expect(operations['HumanAndDroid'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['height']); + expect(operations['HumanAndDroid'].fields.map(field => field.fieldName)).toEqual(['human', 'droid']); + expect(operations['HumanAndDroid'].fields[0].fields.map(field => field.fieldName)).toEqual(['height']); expect(operations['HumanAndDroid'].fields[0].inlineFragments).toEqual([]); - expect(operations['HumanAndDroid'].fields[1].fields.map(field => field.fieldName)) - .toEqual(['primaryFunction']); + expect(operations['HumanAndDroid'].fields[1].fields.map(field => field.fieldName)).toEqual(['primaryFunction']); expect(operations['HumanAndDroid'].fields[1].inlineFragments).toEqual([]); }); @@ -685,13 +639,10 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['HumanAndDroid'].fields.map(field => field.fieldName)) - .toEqual(['human', 'droid']); - expect(operations['HumanAndDroid'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['height']); + expect(operations['HumanAndDroid'].fields.map(field => field.fieldName)).toEqual(['human', 'droid']); + expect(operations['HumanAndDroid'].fields[0].fields.map(field => field.fieldName)).toEqual(['height']); expect(operations['HumanAndDroid'].fields[0].inlineFragments).toEqual([]); - expect(operations['HumanAndDroid'].fields[1].fields.map(field => field.fieldName)) - .toEqual(['primaryFunction']); + expect(operations['HumanAndDroid'].fields[1].fields.map(field => field.fieldName)).toEqual(['primaryFunction']); expect(operations['HumanAndDroid'].fields[1].inlineFragments).toEqual([]); }); @@ -714,16 +665,16 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['Search'].fields[0].fields.map(field => field.fieldName)) - .toEqual([]); + expect(operations['Search'].fields[0].fields.map(field => field.fieldName)).toEqual([]); - expect(operations['Search'].fields[0].inlineFragments["Droid"].typeCondition.toString()).toEqual('Droid'); - expect(operations['Search'].fields[0].inlineFragments["Droid"].fields.map(field => field.fieldName)) - .toEqual(['name', 'primaryFunction']); + expect(operations['Search'].fields[0].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); + expect(operations['Search'].fields[0].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual([ + 'name', + 'primaryFunction', + ]); - expect(operations['Search'].fields[0].inlineFragments["Human"].typeCondition.toString()).toEqual('Human'); - expect(operations['Search'].fields[0].inlineFragments["Human"].fields.map(field => field.fieldName)) - .toEqual(['name', 'height']); + expect(operations['Search'].fields[0].inlineFragments['Human'].typeCondition.toString()).toEqual('Human'); + expect(operations['Search'].fields[0].inlineFragments['Human'].fields.map(field => field.fieldName)).toEqual(['name', 'height']); }); it(`should keep correct field ordering even if fragment is visited multiple times`, () => { @@ -743,8 +694,7 @@ describe('Compiling query documents to the legacy IR', () => { const { operations } = compileToLegacyIR(schema, document); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['name', 'appearsIn']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['name', 'appearsIn']); }); it(`should keep correct field ordering even if field has been visited before for other type condition`, () => { @@ -764,8 +714,7 @@ describe('Compiling query documents to the legacy IR', () => { const { fragments } = compileToLegacyIR(schema, document); expect(fragments['HeroDetails'].inlineFragments['Droid'].typeCondition.toString()).toEqual('Droid'); - expect(fragments['HeroDetails'].inlineFragments['Droid'].fields.map(field => field.fieldName)) - .toEqual(['name', 'appearsIn']); + expect(fragments['HeroDetails'].inlineFragments['Droid'].fields.map(field => field.fieldName)).toEqual(['name', 'appearsIn']); }); it(`should keep track of fragments referenced in a subselection`, () => { @@ -836,7 +785,7 @@ describe('Compiling query documents to the legacy IR', () => { expect(operations['HeroAndFriends'].fragmentsReferenced).toEqual(['HeroDetails']); }); - describe("with mergeInFieldsFromFragmentSpreads set to false", () => { + describe('with mergeInFieldsFromFragmentSpreads set to false', () => { it(`should not morge fields from recursively included fragment spreads with type conditions that match the parent type`, () => { const document = parse(` query Hero { @@ -859,14 +808,11 @@ describe('Compiling query documents to the legacy IR', () => { const { operations, fragments } = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: false }); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['id']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['id']); - expect(fragments['HeroDetails'].fields.map(field => field.fieldName)) - .toEqual(['name', 'id']); + expect(fragments['HeroDetails'].fields.map(field => field.fieldName)).toEqual(['name', 'id']); - expect(fragments['MoreHeroDetails'].fields.map(field => field.fieldName)) - .toEqual(['appearsIn']); + expect(fragments['MoreHeroDetails'].fields.map(field => field.fieldName)).toEqual(['appearsIn']); expect(operations['Hero'].fragmentsReferenced).toEqual(['HeroDetails', 'MoreHeroDetails']); expect(operations['Hero'].fields[0].fragmentSpreads).toEqual(['HeroDetails']); @@ -895,13 +841,10 @@ describe('Compiling query documents to the legacy IR', () => { const { operations, fragments } = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: false }); - expect(operations['HeroAndFriends'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['appearsIn', 'id','friends']); - expect(operations['HeroAndFriends'].fields[0].fields[2].fields.map(field => field.fieldName)) - .toEqual(['id']); + expect(operations['HeroAndFriends'].fields[0].fields.map(field => field.fieldName)).toEqual(['appearsIn', 'id', 'friends']); + expect(operations['HeroAndFriends'].fields[0].fields[2].fields.map(field => field.fieldName)).toEqual(['id']); - expect(fragments['HeroDetails'].fields.map(field => field.fieldName)) - .toEqual(['name', 'id']); + expect(fragments['HeroDetails'].fields.map(field => field.fieldName)).toEqual(['name', 'id']); expect(operations['HeroAndFriends'].fragmentsReferenced).toEqual(['HeroDetails']); expect(operations['HeroAndFriends'].fields[0].fragmentSpreads).toEqual(['HeroDetails']); @@ -928,8 +871,7 @@ describe('Compiling query documents to the legacy IR', () => { const { operations, fragments } = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: false }); - expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)) - .toEqual(['name']); + expect(operations['Hero'].fields[0].fields.map(field => field.fieldName)).toEqual(['name']); expect(operations['Hero'].fields[0].inlineFragment).toBeUndefined(); @@ -945,7 +887,7 @@ describe('Compiling query documents to the legacy IR', () => { name } } - ` + `; const document = parse(source); const { operations } = compileToLegacyIR(schema, document); @@ -958,7 +900,7 @@ describe('Compiling query documents to the legacy IR', () => { fragment HeroDetails on Character { name } - ` + `; const document = parse(source); const { fragments } = compileToLegacyIR(schema, document); @@ -973,7 +915,7 @@ describe('Compiling query documents to the legacy IR', () => { name } } - ` + `; const document = parse(source); const { operations } = compileToLegacyIR(schema, document, { addTypename: true }); @@ -993,7 +935,7 @@ describe('Compiling query documents to the legacy IR', () => { fragment HeroDetails on Character { name } - ` + `; const document = parse(source); const { fragments } = compileToLegacyIR(schema, document, { addTypename: true }); @@ -1013,7 +955,7 @@ describe('Compiling query documents to the legacy IR', () => { name } } - ` + `; const document = parse(source); const { operations } = compileToLegacyIR(schema, document); @@ -1029,7 +971,7 @@ describe('Compiling query documents to the legacy IR', () => { commentary } } - ` + `; const document = parse(source); const { operations } = compileToLegacyIR(schema, document); diff --git a/packages/amplify-graphql-types-generator/test/compiler/visitors/conditionalFields.ts b/packages/amplify-graphql-types-generator/test/compiler/visitors/conditionalFields.ts index 54cadd806d..653ac900b0 100644 --- a/packages/amplify-graphql-types-generator/test/compiler/visitors/conditionalFields.ts +++ b/packages/amplify-graphql-types-generator/test/compiler/visitors/conditionalFields.ts @@ -14,8 +14,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeFalsy(); @@ -30,8 +29,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeTruthy(); @@ -48,8 +46,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeFalsy(); @@ -64,8 +61,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], []); }); @@ -79,8 +75,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeTruthy(); @@ -97,8 +92,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeFalsy(); @@ -113,8 +107,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], []); }); @@ -128,15 +121,14 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeTruthy(); expect(selectionSet.selections[0]).toMatchObject({ variableName: 'includeName' }); expect((selectionSet.selections[0] as BooleanCondition).selectionSet.selections[0]).toMatchObject({ - variableName: 'skipName' + variableName: 'skipName', }); }); @@ -150,8 +142,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeTruthy(); @@ -169,8 +160,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], []); }); @@ -185,8 +175,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeFalsy(); @@ -202,8 +191,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; expect(selectionSet).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(collectAndMergeFields(selectionSet)[0].isConditional).toBeFalsy(); @@ -221,8 +209,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['name']); @@ -245,8 +232,7 @@ describe('@skip/@include directives', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['name']); diff --git a/packages/amplify-graphql-types-generator/test/compiler/visitors/typeCase.ts b/packages/amplify-graphql-types-generator/test/compiler/visitors/typeCase.ts index 2333767a35..b8bc73c99d 100644 --- a/packages/amplify-graphql-types-generator/test/compiler/visitors/typeCase.ts +++ b/packages/amplify-graphql-types-generator/test/compiler/visitors/typeCase.ts @@ -59,8 +59,7 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['id', 'name', 'appearsIn']); @@ -68,10 +67,7 @@ describe('TypeCase', () => { expect(typeCase.variants).toHaveLength(0); expect(typeCase.exhaustiveVariants).toHaveLength(1); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Human', 'Droid'], - ['id', 'name', 'appearsIn'] - ); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Human', 'Droid'], ['id', 'name', 'appearsIn']); }); it('should recursively include fragment spreads with type conditions that match the parent type', () => { @@ -94,8 +90,7 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['id', 'name', 'appearsIn']); @@ -104,10 +99,7 @@ describe('TypeCase', () => { expect(typeCase.variants).toHaveLength(0); expect(typeCase.exhaustiveVariants).toHaveLength(1); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Human', 'Droid'], - ['id', 'name', 'appearsIn'] - ); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Human', 'Droid'], ['id', 'name', 'appearsIn']); }); it('should include fragment spreads when nested within inline fragments', () => { @@ -125,8 +117,7 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['name']); @@ -155,9 +146,9 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; - const typeCase = typeCaseForSelectionSet(collectAndMergeFields(typeCaseForSelectionSet(selectionSet).variants[0])[0].selectionSet as SelectionSet); + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; + const typeCase = typeCaseForSelectionSet(collectAndMergeFields(typeCaseForSelectionSet(selectionSet).variants[0])[0] + .selectionSet as SelectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(typeCase.default.fragmentSpreads.map(fragmentSpread => fragmentSpread.fragmentName)).toEqual(['CharacterName']); @@ -195,8 +186,7 @@ describe('TypeCase', () => { schema ); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['name']); @@ -223,28 +213,18 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['name', 'appearsIn']); expect(typeCase.variants).toHaveLength(2); - expect(typeCase.variants).toContainSelectionSetMatching( - ['Droid'], - ['name', 'primaryFunction', 'appearsIn'] - ); + expect(typeCase.variants).toContainSelectionSetMatching(['Droid'], ['name', 'primaryFunction', 'appearsIn']); expect(typeCase.variants).toContainSelectionSetMatching(['Human'], ['name', 'appearsIn', 'height']); expect(typeCase.exhaustiveVariants).toHaveLength(2); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Droid'], - ['name', 'primaryFunction', 'appearsIn'] - ); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Human'], - ['name', 'appearsIn', 'height'] - ); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Droid'], ['name', 'primaryFunction', 'appearsIn']); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Human'], ['name', 'appearsIn', 'height']); }); it(`should merge fields from type conditions with the same type`, () => { @@ -262,23 +242,16 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], ['name']); expect(typeCase.variants).toHaveLength(1); - expect(typeCase.variants).toContainSelectionSetMatching( - ['Droid'], - ['name', 'primaryFunction', 'appearsIn'] - ); + expect(typeCase.variants).toContainSelectionSetMatching(['Droid'], ['name', 'primaryFunction', 'appearsIn']); expect(typeCase.exhaustiveVariants).toHaveLength(2); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Droid'], - ['name', 'primaryFunction', 'appearsIn'] - ); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Droid'], ['name', 'primaryFunction', 'appearsIn']); expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Human'], ['name']); }); @@ -295,8 +268,7 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], []); @@ -322,8 +294,7 @@ describe('TypeCase', () => { } `); - const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Hero'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Human', 'Droid'], []); @@ -353,8 +324,7 @@ describe('TypeCase', () => { animalSchema ); - const selectionSet = (context.operations['Animal'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Animal'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Cat', 'Bird', 'Fish', 'Crocodile'], []); @@ -364,10 +334,7 @@ describe('TypeCase', () => { expect(typeCase.variants).toContainSelectionSetMatching(['Fish'], ['name']); expect(typeCase.exhaustiveVariants).toHaveLength(3); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Cat', 'Bird'], - ['name', 'bodyTemperature'] - ); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Cat', 'Bird'], ['name', 'bodyTemperature']); expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Fish'], ['name']); expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Crocodile'], []); }); @@ -395,8 +362,7 @@ describe('TypeCase', () => { animalSchema ); - const selectionSet = (context.operations['Animal'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Animal'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Cat', 'Bird', 'Fish', 'Crocodile'], []); @@ -406,10 +372,7 @@ describe('TypeCase', () => { expect(typeCase.variants).toContainSelectionSetMatching(['Fish'], ['name']); expect(typeCase.exhaustiveVariants).toHaveLength(3); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Cat', 'Bird'], - ['name', 'bodyTemperature'] - ); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Cat', 'Bird'], ['name', 'bodyTemperature']); expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Fish'], ['name']); expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Crocodile'], []); }); @@ -431,17 +394,13 @@ describe('TypeCase', () => { animalSchema ); - const selectionSet = (context.operations['Animal'].selectionSet.selections[0] as Field) - .selectionSet as SelectionSet; + const selectionSet = (context.operations['Animal'].selectionSet.selections[0] as Field).selectionSet as SelectionSet; const typeCase = typeCaseForSelectionSet(selectionSet); expect(typeCase.default).toMatchSelectionSet(['Cat', 'Bird'], ['name', 'bodyTemperature']); expect(typeCase.variants).toHaveLength(0); expect(typeCase.exhaustiveVariants).toHaveLength(1); - expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching( - ['Cat', 'Bird'], - ['name', 'bodyTemperature'] - ); + expect(typeCase.exhaustiveVariants).toContainSelectionSetMatching(['Cat', 'Bird'], ['name', 'bodyTemperature']); }); }); diff --git a/packages/amplify-graphql-types-generator/test/fixtures/misc/invalid-gqlQueries.js b/packages/amplify-graphql-types-generator/test/fixtures/misc/invalid-gqlQueries.js index d808af96f6..406d044564 100644 --- a/packages/amplify-graphql-types-generator/test/fixtures/misc/invalid-gqlQueries.js +++ b/packages/amplify-graphql-types-generator/test/fixtures/misc/invalid-gqlQueries.js @@ -1,5 +1,5 @@ -const gql = String.raw +const gql = String.raw; export const hello = gql`world and other words`; -export const foo = `bar` +export const foo = `bar`; diff --git a/packages/amplify-graphql-types-generator/test/fixtures/starwars/gqlQueries.js b/packages/amplify-graphql-types-generator/test/fixtures/starwars/gqlQueries.js index af6eb8bdcc..413e5cf2cd 100644 --- a/packages/amplify-graphql-types-generator/test/fixtures/starwars/gqlQueries.js +++ b/packages/amplify-graphql-types-generator/test/fixtures/starwars/gqlQueries.js @@ -1,5 +1,5 @@ -import React from 'react' -import { gql, graphql } from 'react-apollo' +import React from 'react'; +import { gql, graphql } from 'react-apollo'; const Query = gql` query HeroAndFriends($episode: Episode) { @@ -19,7 +19,7 @@ const Query = gql` } ${'this should be ignored'} -` +`; const AnotherQuery = gql` query HeroName { @@ -27,10 +27,10 @@ const AnotherQuery = gql` name } } -` +`; function Component() { - return
+ return
; } -export default graphql(Query)(Component) +export default graphql(Query)(Component); diff --git a/packages/amplify-graphql-types-generator/test/flow/codeGeneration.js b/packages/amplify-graphql-types-generator/test/flow/codeGeneration.js index 245d1b9c97..b1c333ebf3 100644 --- a/packages/amplify-graphql-types-generator/test/flow/codeGeneration.js +++ b/packages/amplify-graphql-types-generator/test/flow/codeGeneration.js @@ -1,16 +1,6 @@ -import { - parse, - isType, - GraphQLID, - GraphQLString, - GraphQLInt, - GraphQLList, - GraphQLNonNull -} from 'graphql'; - -import { - generateSource -} from '../../src/flow/codeGeneration'; +import { parse, isType, GraphQLID, GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull } from 'graphql'; + +import { generateSource } from '../../src/flow/codeGeneration'; import { loadSchema } from '../../src/loading'; const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); @@ -25,19 +15,19 @@ function setup(schema) { schema: schema, operations: {}, fragments: {}, - typesUsed: {} - } + typesUsed: {}, + }; const generator = new CodeGenerator(context); - const compileFromSource = (source) => { + const compileFromSource = source => { const document = parse(source); - const context = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: true, addTypename: true } ); + const context = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: true, addTypename: true }); generator.context = context; return context; }; - const addFragment = (fragment) => { + const addFragment = fragment => { generator.context.fragments[fragment.fragmentName] = fragment; }; @@ -332,7 +322,7 @@ describe('Flow code generation', function() { const source = generateSource(context); expect(source).toMatchSnapshot(); }); - + test('should have __typename value matching fragment type on generic type', () => { const { compileFromSource } = setup(starWarsSchema); const context = compileFromSource(` diff --git a/packages/amplify-graphql-types-generator/test/jsonOutput.ts b/packages/amplify-graphql-types-generator/test/jsonOutput.ts index 9499232668..da272dda3e 100644 --- a/packages/amplify-graphql-types-generator/test/jsonOutput.ts +++ b/packages/amplify-graphql-types-generator/test/jsonOutput.ts @@ -9,7 +9,7 @@ function compileFromSource(source: string, schema: GraphQLSchema = starWarsSchem const document = parse(source); return compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: false, - addTypename: true + addTypename: true, }); } diff --git a/packages/amplify-graphql-types-generator/test/loading.ts b/packages/amplify-graphql-types-generator/test/loading.ts index 9987e8cc8c..af98e2e8fd 100644 --- a/packages/amplify-graphql-types-generator/test/loading.ts +++ b/packages/amplify-graphql-types-generator/test/loading.ts @@ -4,20 +4,16 @@ import { loadAndMergeQueryDocuments } from '../src/loading'; describe('Validation', () => { test(`should extract gql snippet from javascript file`, () => { - const inputPaths = [ - path.join(__dirname, './fixtures/starwars/gqlQueries.js'), - ]; + const inputPaths = [path.join(__dirname, './fixtures/starwars/gqlQueries.js')]; const document = loadAndMergeQueryDocuments(inputPaths); expect(document).toMatchSnapshot(); - }) + }); test(`should throw a helpful message when a file has invalid gql snippets`, () => { - const inputPaths = [ - path.join(__dirname, './fixtures/misc/invalid-gqlQueries.js'), - ]; + const inputPaths = [path.join(__dirname, './fixtures/misc/invalid-gqlQueries.js')]; expect(() => { loadAndMergeQueryDocuments(inputPaths); }).toThrowErrorMatchingSnapshot(); - }) + }); }); diff --git a/packages/amplify-graphql-types-generator/test/scala/codeGeneration.js b/packages/amplify-graphql-types-generator/test/scala/codeGeneration.js index 1a4d4216aa..fae95dc6cb 100644 --- a/packages/amplify-graphql-types-generator/test/scala/codeGeneration.js +++ b/packages/amplify-graphql-types-generator/test/scala/codeGeneration.js @@ -9,7 +9,7 @@ import { GraphQLList, GraphQLNonNull, GraphQLInputObjectType, - GraphQLEnumType + GraphQLEnumType, } from 'graphql'; import { @@ -19,9 +19,7 @@ import { typeDeclarationForGraphQLType, } from '../../src/scala/codeGeneration'; -import { - dictionaryLiteralForFieldArguments, -} from '../../src/scala/values'; +import { dictionaryLiteralForFieldArguments } from '../../src/scala/values'; import { loadSchema } from '../../src/loading'; const schema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); @@ -37,14 +35,13 @@ describe('Scala code generation', function() { let addFragment; beforeEach(function() { - resetGenerator = () => { const context = { schema: schema, operations: {}, fragments: {}, - typesUsed: {} - } + typesUsed: {}, + }; generator = new CodeGenerator(context); }; @@ -56,7 +53,7 @@ describe('Scala code generation', function() { return context; }; - addFragment = (fragment) => { + addFragment = fragment => { generator.context.fragments[fragment.fragmentName] = fragment; }; @@ -150,7 +147,8 @@ describe('Scala code generation', function() { let compileOptions = { generateOperationIds: true }; test(`should generate a class declaration with an operationId property`, function() { - const context = compileFromSource(` + const context = compileFromSource( + ` query Hero { hero { ...HeroDetails @@ -159,14 +157,17 @@ describe('Scala code generation', function() { fragment HeroDetails on Character { name } - `, compileOptions); + `, + compileOptions + ); classDeclarationForOperation(generator, context.operations['Hero'], Object.values(context.fragments)); expect(generator.output).toMatchSnapshot(); }); test(`should generate different operation ids for different operations`, function() { - const context1 = compileFromSource(` + const context1 = compileFromSource( + ` query Hero { hero { ...HeroDetails @@ -175,13 +176,16 @@ describe('Scala code generation', function() { fragment HeroDetails on Character { name } - `, compileOptions); + `, + compileOptions + ); classDeclarationForOperation(generator, context1.operations['Hero'], Object.values(context1.fragments)); const output1 = generator.output; resetGenerator(); - const context2 = compileFromSource(` + const context2 = compileFromSource( + ` query Hero { hero { ...HeroDetails @@ -190,7 +194,9 @@ describe('Scala code generation', function() { fragment HeroDetails on Character { appearsIn } - `, compileOptions); + `, + compileOptions + ); classDeclarationForOperation(generator, context2.operations['Hero'], Object.values(context2.fragments)); const output2 = generator.output; @@ -199,23 +205,29 @@ describe('Scala code generation', function() { }); test(`should generate the same operation id regardless of operation formatting/commenting`, function() { - const context1 = compileFromSource(` + const context1 = compileFromSource( + ` query HeroName($episode: Episode) { hero(episode: $episode) { name } } - `, compileOptions); + `, + compileOptions + ); classDeclarationForOperation(generator, context1.operations['HeroName'], Object.values(context1.fragments)); const output1 = generator.output; resetGenerator(); - const context2 = compileFromSource(` + const context2 = compileFromSource( + ` # Profound comment query HeroName($episode:Episode) { hero(episode: $episode) { name } } # Deeply meaningful comment - `, compileOptions); + `, + compileOptions + ); classDeclarationForOperation(generator, context2.operations['HeroName'], Object.values(context2.fragments)); const output2 = generator.output; @@ -224,7 +236,8 @@ describe('Scala code generation', function() { }); test(`should generate the same operation id regardless of fragment order`, function() { - const context1 = compileFromSource(` + const context1 = compileFromSource( + ` query Hero { hero { ...HeroName @@ -237,13 +250,16 @@ describe('Scala code generation', function() { fragment HeroAppearsIn on Character { appearsIn } - `, compileOptions); + `, + compileOptions + ); classDeclarationForOperation(generator, context1.operations['Hero'], Object.values(context1.fragments)); const output1 = generator.output; resetGenerator(); - const context2 = compileFromSource(` + const context2 = compileFromSource( + ` query Hero { hero { ...HeroName @@ -256,7 +272,9 @@ describe('Scala code generation', function() { fragment HeroName on Character { name } - `, compileOptions); + `, + compileOptions + ); classDeclarationForOperation(generator, context2.operations['Hero'], Object.values(context2.fragments)); const output2 = generator.output; @@ -282,7 +300,6 @@ describe('Scala code generation', function() { const context = compileFromSource(source, true); expect(context.operations['Hero'].sourceWithFragments).toMatchSnapshot(); }); - }); }); @@ -355,9 +372,9 @@ describe('Scala code generation', function() { { responseName: 'name', fieldName: 'name', - type: GraphQLString - } - ] + type: GraphQLString, + }, + ], }); expect(generator.output).toMatchSnapshot(); @@ -371,9 +388,9 @@ describe('Scala code generation', function() { { responseName: 'private', fieldName: 'name', - type: GraphQLString - } - ] + type: GraphQLString, + }, + ], }); expect(generator.output).toMatchSnapshot(); @@ -392,11 +409,11 @@ describe('Scala code generation', function() { { responseName: 'name', fieldName: 'name', - type: GraphQLString - } - ] - } - ] + type: GraphQLString, + }, + ], + }, + ], }); expect(generator.output).toMatchSnapshot(); @@ -405,7 +422,7 @@ describe('Scala code generation', function() { test(`should generate a caseClass declaration for a selection set with a fragment spread that matches the parent type`, function() { addFragment({ fragmentName: 'HeroDetails', - typeCondition: schema.getType('Character') + typeCondition: schema.getType('Character'), }); caseClassDeclarationForSelectionSet(generator, { @@ -416,9 +433,9 @@ describe('Scala code generation', function() { { responseName: 'name', fieldName: 'name', - type: GraphQLString - } - ] + type: GraphQLString, + }, + ], }); expect(generator.output).toMatchSnapshot(); @@ -427,7 +444,7 @@ describe('Scala code generation', function() { test(`should generate a caseClass declaration for a selection set with a fragment spread with a more specific type condition`, function() { addFragment({ fragmentName: 'DroidDetails', - typeCondition: schema.getType('Droid') + typeCondition: schema.getType('Droid'), }); caseClassDeclarationForSelectionSet(generator, { @@ -438,9 +455,9 @@ describe('Scala code generation', function() { { responseName: 'name', fieldName: 'name', - type: GraphQLString - } - ] + type: GraphQLString, + }, + ], }); expect(generator.output).toMatchSnapshot(); @@ -454,8 +471,8 @@ describe('Scala code generation', function() { { responseName: 'name', fieldName: 'name', - type: new GraphQLNonNull(GraphQLString) - } + type: new GraphQLNonNull(GraphQLString), + }, ], inlineFragments: [ { @@ -465,16 +482,16 @@ describe('Scala code generation', function() { { responseName: 'name', fieldName: 'name', - type: new GraphQLNonNull(GraphQLString) + type: new GraphQLNonNull(GraphQLString), }, { responseName: 'primaryFunction', fieldName: 'primaryFunction', - type: GraphQLString - } - ] - } - ] + type: GraphQLString, + }, + ], + }, + ], }); expect(generator.output).toMatchSnapshot(); @@ -483,7 +500,7 @@ describe('Scala code generation', function() { test(`should generate a caseClass declaration for a fragment spread nested in an inline fragment`, function() { addFragment({ fragmentName: 'HeroDetails', - typeCondition: schema.getType('Character') + typeCondition: schema.getType('Character'), }); caseClassDeclarationForSelectionSet(generator, { @@ -496,8 +513,8 @@ describe('Scala code generation', function() { possibleTypes: ['Droid'], fields: [], fragmentSpreads: ['HeroDetails'], - } - ] + }, + ], }); expect(generator.output).toMatchSnapshot(); @@ -517,7 +534,9 @@ describe('Scala code generation', function() { const fieldArguments = operations['FieldArgumentsWithInputObjects'].fields[0].args; const dictionaryLiteral = dictionaryLiteralForFieldArguments(fieldArguments); - expect(dictionaryLiteral).toBe('["episode": "JEDI", "review": ["stars": 2, "commentary": Variable("commentary"), "favorite_color": ["red": Variable("red"), "blue": 100, "green": 50]]]'); + expect(dictionaryLiteral).toBe( + '["episode": "JEDI", "review": ["stars": 2, "commentary": Variable("commentary"), "favorite_color": ["red": Variable("red"), "blue": 100, "green": 50]]]' + ); }); }); @@ -535,7 +554,7 @@ describe('Scala code generation', function() { const albumPrivaciesEnum = new GraphQLEnumType({ name: 'AlbumPrivacies', - values: { PUBLIC: { value: "PUBLIC" }, PRIVATE: { value: "PRIVATE" } } + values: { PUBLIC: { value: 'PUBLIC' }, PRIVATE: { value: 'PRIVATE' } }, }); typeDeclarationForGraphQLType(generator, albumPrivaciesEnum); diff --git a/packages/amplify-graphql-types-generator/test/scala/language.js b/packages/amplify-graphql-types-generator/test/scala/language.js index 21a518fb4e..ed9232b649 100644 --- a/packages/amplify-graphql-types-generator/test/scala/language.js +++ b/packages/amplify-graphql-types-generator/test/scala/language.js @@ -2,11 +2,7 @@ import { stripIndent } from 'common-tags'; import CodeGenerator from '../../src/utilities/CodeGenerator'; -import { - objectDeclaration, - caseClassDeclaration, - propertyDeclaration, -} from '../../src/scala/language'; +import { objectDeclaration, caseClassDeclaration, propertyDeclaration } from '../../src/scala/language'; describe('Scala code generation: Basic language constructs', function() { let generator; @@ -32,7 +28,11 @@ describe('Scala code generation: Basic language constructs', function() { }); test(`should generate a case class declaration`, function() { - caseClassDeclaration(generator, { caseClassName: 'Hero', params: [{name: 'name', type: 'String'}, {name: 'age', type: 'Int'}] }, () => {}); + caseClassDeclaration( + generator, + { caseClassName: 'Hero', params: [{ name: 'name', type: 'String' }, { name: 'age', type: 'Int' }] }, + () => {} + ); expect(generator.output).toBe(stripIndent` case class Hero(name: String, age: Int) { @@ -41,9 +41,13 @@ describe('Scala code generation: Basic language constructs', function() { }); test(`should generate nested case class declarations`, function() { - caseClassDeclaration(generator, { caseClassName: 'Hero', params: [{name: 'name', type: 'String'}, {name: 'age', type: 'Int'}] }, () => { - caseClassDeclaration(generator, { caseClassName: 'Friend', params: [{name: 'name', type: 'String'}] }, () => {}); - }); + caseClassDeclaration( + generator, + { caseClassName: 'Hero', params: [{ name: 'name', type: 'String' }, { name: 'age', type: 'Int' }] }, + () => { + caseClassDeclaration(generator, { caseClassName: 'Friend', params: [{ name: 'name', type: 'String' }] }, () => {}); + } + ); expect(generator.output).toBe(stripIndent` case class Hero(name: String, age: Int) { @@ -55,8 +59,16 @@ describe('Scala code generation: Basic language constructs', function() { test(`should handle multi-line descriptions`, () => { caseClassDeclaration(generator, { caseClassName: 'Hero', description: 'A hero' }, () => { - propertyDeclaration(generator, { propertyName: 'name', typeName: 'String', description: `A multiline comment \n on the hero's name.` }, () => {}); - propertyDeclaration(generator, { propertyName: 'age', typeName: 'String', description: `A multiline comment \n on the hero's age.` }, () => {}); + propertyDeclaration( + generator, + { propertyName: 'name', typeName: 'String', description: `A multiline comment \n on the hero's name.` }, + () => {} + ); + propertyDeclaration( + generator, + { propertyName: 'age', typeName: 'String', description: `A multiline comment \n on the hero's age.` }, + () => {} + ); }); expect(generator.output).toMatchSnapshot(); diff --git a/packages/amplify-graphql-types-generator/test/scala/types.js b/packages/amplify-graphql-types-generator/test/scala/types.js index 0139cfeaa3..8dd8497ca4 100644 --- a/packages/amplify-graphql-types-generator/test/scala/types.js +++ b/packages/amplify-graphql-types-generator/test/scala/types.js @@ -1,4 +1,4 @@ -import { stripIndent } from 'common-tags' +import { stripIndent } from 'common-tags'; import { GraphQLString, @@ -11,12 +11,12 @@ import { GraphQLScalarType, } from 'graphql'; -import { loadSchema } from '../../src/loading' +import { loadSchema } from '../../src/loading'; const schema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); import CodeGenerator from '../../src/utilities/CodeGenerator'; -import { typeNameFromGraphQLType } from '../../src/scala/types' +import { typeNameFromGraphQLType } from '../../src/scala/types'; describe('Scala code generation: Types', function() { describe('#typeNameFromGraphQLType()', function() { @@ -49,7 +49,9 @@ describe('Scala code generation: Types', function() { }); test('should return Option[Seq[Seq[Option[String]]]] for GraphQLList(GraphQLNonNull(GraphQLList(GraphQLString)))', function() { - expect(typeNameFromGraphQLType({}, new GraphQLList(new GraphQLNonNull(new GraphQLList(GraphQLString))))).toBe('Option[Seq[Seq[Option[String]]]]'); + expect(typeNameFromGraphQLType({}, new GraphQLList(new GraphQLNonNull(new GraphQLList(GraphQLString))))).toBe( + 'Option[Seq[Seq[Option[String]]]]' + ); }); test('should return Option[Int] for GraphQLInt', function() { @@ -73,11 +75,21 @@ describe('Scala code generation: Types', function() { }); test('should return a passed through custom scalar type with the passthroughCustomScalars option', function() { - expect(typeNameFromGraphQLType({ passthroughCustomScalars: true, customScalarsPrefix: '' }, new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe('Option[CustomScalarType]'); + expect( + typeNameFromGraphQLType( + { passthroughCustomScalars: true, customScalarsPrefix: '' }, + new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) + ) + ).toBe('Option[CustomScalarType]'); }); test('should return a passed through custom scalar type with a prefix with the customScalarsPrefix option', function() { - expect(typeNameFromGraphQLType({ passthroughCustomScalars: true, customScalarsPrefix: 'My' }, new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe('Option[MyCustomScalarType]'); + expect( + typeNameFromGraphQLType( + { passthroughCustomScalars: true, customScalarsPrefix: 'My' }, + new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) + ) + ).toBe('Option[MyCustomScalarType]'); }); }); }); diff --git a/packages/amplify-graphql-types-generator/test/swift/language.ts b/packages/amplify-graphql-types-generator/test/swift/language.ts index ad599f1441..352b9cf2e1 100644 --- a/packages/amplify-graphql-types-generator/test/swift/language.ts +++ b/packages/amplify-graphql-types-generator/test/swift/language.ts @@ -1,8 +1,6 @@ import { stripIndent } from 'common-tags'; -import { - SwiftGenerator -} from '../../src/swift/language'; +import { SwiftGenerator } from '../../src/swift/language'; describe('Swift code generation: Basic language constructs', () => { let generator: SwiftGenerator; @@ -91,7 +89,11 @@ describe('Swift code generation: Basic language constructs', () => { it(`should handle multi-line descriptions`, () => { generator.structDeclaration({ structName: 'Hero', description: 'A hero' }, () => { - generator.propertyDeclaration({ propertyName: 'name', typeName: 'String', description: `A multiline comment \n on the hero's name.` }); + generator.propertyDeclaration({ + propertyName: 'name', + typeName: 'String', + description: `A multiline comment \n on the hero's name.`, + }); generator.propertyDeclaration({ propertyName: 'age', typeName: 'String', description: `A multiline comment \n on the hero's age.` }); }); diff --git a/packages/amplify-graphql-types-generator/test/swift/typeNameFromGraphQLType.ts b/packages/amplify-graphql-types-generator/test/swift/typeNameFromGraphQLType.ts index cb0b82a9b9..4ce6edae89 100644 --- a/packages/amplify-graphql-types-generator/test/swift/typeNameFromGraphQLType.ts +++ b/packages/amplify-graphql-types-generator/test/swift/typeNameFromGraphQLType.ts @@ -6,7 +6,7 @@ import { GraphQLID, GraphQLList, GraphQLNonNull, - GraphQLScalarType + GraphQLScalarType, } from 'graphql'; import { Helpers } from '../../src/swift/helpers'; @@ -32,35 +32,23 @@ describe('Swift code generation: Types', () => { }); it('should return [String?] for GraphQLNonNull(GraphQLList(GraphQLString))', () => { - expect(helpers.typeNameFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLString)))).toBe( - '[String?]' - ); + expect(helpers.typeNameFromGraphQLType(new GraphQLNonNull(new GraphQLList(GraphQLString)))).toBe('[String?]'); }); it('should return [String]? for GraphQLList(GraphQLNonNull(GraphQLString))', () => { - expect(helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLString)))).toBe( - '[String]?' - ); + expect(helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLNonNull(GraphQLString)))).toBe('[String]?'); }); it('should return [String] for GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString)))', () => { - expect( - helpers.typeNameFromGraphQLType( - new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))) - ) - ).toBe('[String]'); + expect(helpers.typeNameFromGraphQLType(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))))).toBe('[String]'); }); it('should return [[String?]?]? for GraphQLList(GraphQLList(GraphQLString))', () => { - expect(helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLList(GraphQLString)))).toBe( - '[[String?]?]?' - ); + expect(helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLList(GraphQLString)))).toBe('[[String?]?]?'); }); it('should return [[String?]]? for GraphQLList(GraphQLNonNull(GraphQLList(GraphQLString)))', () => { - expect( - helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLNonNull(new GraphQLList(GraphQLString)))) - ).toBe('[[String?]]?'); + expect(helpers.typeNameFromGraphQLType(new GraphQLList(new GraphQLNonNull(new GraphQLList(GraphQLString))))).toBe('[[String?]]?'); }); it('should return Int? for GraphQLInt', () => { @@ -80,33 +68,25 @@ describe('Swift code generation: Types', () => { }); it('should return String? for a custom scalar type', () => { - expect( - helpers.typeNameFromGraphQLType( - new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) - ) - ).toBe('String?'); + expect(helpers.typeNameFromGraphQLType(new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe('String?'); }); it('should return a passed through custom scalar type with the passthroughCustomScalars option', () => { helpers.options.passthroughCustomScalars = true; helpers.options.customScalarsPrefix = ''; - expect( - helpers.typeNameFromGraphQLType( - new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) - ) - ).toBe('CustomScalarType?'); + expect(helpers.typeNameFromGraphQLType(new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe( + 'CustomScalarType?' + ); }); it('should return a passed through custom scalar type with a prefix with the customScalarsPrefix option', () => { helpers.options.passthroughCustomScalars = true; helpers.options.customScalarsPrefix = 'My'; - expect( - helpers.typeNameFromGraphQLType( - new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }) - ) - ).toBe('MyCustomScalarType?'); + expect(helpers.typeNameFromGraphQLType(new GraphQLScalarType({ name: 'CustomScalarType', serialize: String }))).toBe( + 'MyCustomScalarType?' + ); }); }); }); diff --git a/packages/amplify-graphql-types-generator/test/test-utils/helpers.ts b/packages/amplify-graphql-types-generator/test/test-utils/helpers.ts index a2f3341fe8..eb1a93ba16 100644 --- a/packages/amplify-graphql-types-generator/test/test-utils/helpers.ts +++ b/packages/amplify-graphql-types-generator/test/test-utils/helpers.ts @@ -1,5 +1,5 @@ import { parse, GraphQLSchema } from 'graphql'; -import { compileToIR, CompilerOptions, } from '../../src/compiler'; +import { compileToIR, CompilerOptions } from '../../src/compiler'; import { loadSchema } from '../../src/loading'; export const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); diff --git a/packages/amplify-graphql-types-generator/test/test-utils/matchers.ts b/packages/amplify-graphql-types-generator/test/test-utils/matchers.ts index be2b06afea..1ce11dbcad 100644 --- a/packages/amplify-graphql-types-generator/test/test-utils/matchers.ts +++ b/packages/amplify-graphql-types-generator/test/test-utils/matchers.ts @@ -32,7 +32,7 @@ function toMatchSelectionSet( ` ${this.utils.printExpected(expectedResponseKeys)}` + 'Received:\n' + ` ${this.utils.printReceived(actualResponseKeys)}`, - pass: true + pass: true, }; } else { return { @@ -42,7 +42,7 @@ function toMatchSelectionSet( ` ${this.utils.printExpected(expectedResponseKeys)}\n` + 'Received:\n' + ` ${this.utils.printReceived(actualResponseKeys)}`, - pass: false + pass: false, }; } } @@ -71,7 +71,7 @@ function toContainSelectionSetMatching( )}` ) .join('\n'), - pass: false + pass: false, }; } @@ -80,5 +80,5 @@ function toContainSelectionSetMatching( expect.extend({ toMatchSelectionSet, - toContainSelectionSetMatching + toContainSelectionSetMatching, } as any); diff --git a/packages/amplify-graphql-types-generator/test/typescript/codeGeneration.js b/packages/amplify-graphql-types-generator/test/typescript/codeGeneration.js index 473596501e..0d30495563 100644 --- a/packages/amplify-graphql-types-generator/test/typescript/codeGeneration.js +++ b/packages/amplify-graphql-types-generator/test/typescript/codeGeneration.js @@ -1,18 +1,8 @@ import { stripIndent } from 'common-tags'; -import { - parse, - isType, - GraphQLID, - GraphQLString, - GraphQLInt, - GraphQLList, - GraphQLNonNull -} from 'graphql'; - -import { - generateSource -} from '../../src/typescript/codeGeneration'; +import { parse, isType, GraphQLID, GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull } from 'graphql'; + +import { generateSource } from '../../src/typescript/codeGeneration'; import { loadSchema } from '../../src/loading'; const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); @@ -32,22 +22,22 @@ describe('TypeScript code generation', function() { schema: schema, operations: {}, fragments: {}, - typesUsed: {} - } + typesUsed: {}, + }; generator = new CodeGenerator(context); - compileFromSource = (source) => { + compileFromSource = source => { const document = parse(source); const context = compileToLegacyIR(schema, document, { mergeInFieldsFromFragmentSpreads: true, - addTypename: true + addTypename: true, }); generator.context = context; return context; }; - addFragment = (fragment) => { + addFragment = fragment => { generator.context.fragments[fragment.fragmentName] = fragment; }; diff --git a/packages/amplify-graphql-types-generator/test/validation.ts b/packages/amplify-graphql-types-generator/test/validation.ts index 4207a11694..d81aba02e7 100644 --- a/packages/amplify-graphql-types-generator/test/validation.ts +++ b/packages/amplify-graphql-types-generator/test/validation.ts @@ -8,28 +8,18 @@ const schema = loadSchema(require.resolve('./fixtures/starwars/schema.json')); describe('Validation', () => { function loadQueryDocument(filename: string) { - return loadAndMergeQueryDocuments([ - path.join(__dirname, './fixtures/starwars', filename), - ]); + return loadAndMergeQueryDocuments([path.join(__dirname, './fixtures/starwars', filename)]); } test(`should throw an error for AnonymousQuery.graphql`, () => { const document = loadQueryDocument('AnonymousQuery.graphql'); - expect( - () => validateQueryDocument(schema, document) - ).toThrow( - 'Validation of GraphQL query document failed' - ); + expect(() => validateQueryDocument(schema, document)).toThrow('Validation of GraphQL query document failed'); }); test(`should throw an error for TypenameAlias.graphql`, () => { const document = loadQueryDocument('TypenameAlias.graphql'); - expect( - () => validateQueryDocument(schema, document) - ).toThrow( - 'Validation of GraphQL query document failed' - ); + expect(() => validateQueryDocument(schema, document)).toThrow('Validation of GraphQL query document failed'); }); }); diff --git a/packages/amplify-graphql-types-generator/test/valueFromValueNode.ts b/packages/amplify-graphql-types-generator/test/valueFromValueNode.ts index 097361c76d..c5ce60545f 100644 --- a/packages/amplify-graphql-types-generator/test/valueFromValueNode.ts +++ b/packages/amplify-graphql-types-generator/test/valueFromValueNode.ts @@ -67,7 +67,7 @@ describe('#valueFromValueNode', () => { foo: 'foo', bar: 1, bla: 'JEDI', - baz: { kind: 'Variable', variableName: 'something' } + baz: { kind: 'Variable', variableName: 'something' }, }); }); }); diff --git a/packages/amplify-storage-simulator/src/__tests__/S3server.test.ts b/packages/amplify-storage-simulator/src/__tests__/S3server.test.ts index 0f5fef696d..dc93fc6e56 100644 --- a/packages/amplify-storage-simulator/src/__tests__/S3server.test.ts +++ b/packages/amplify-storage-simulator/src/__tests__/S3server.test.ts @@ -198,21 +198,21 @@ describe('Test put api', () => { expect(data3.Key).toBe('upload/long_image3.jpg'); }); - test(" async uploads", async () => { + test(' async uploads', async () => { const params1 = { Bucket: bucket, // pass your bucket name - Key: "upload/long_image1.jpg", - Body: buf2 + Key: 'upload/long_image1.jpg', + Body: buf2, }; const params2 = { Bucket: bucket, // pass your bucket name - Key: "upload/long_image2.jpg", - Body: buf2 + Key: 'upload/long_image2.jpg', + Body: buf2, }; const params3 = { Bucket: bucket, // pass your bucket name - Key: "upload/long_image3.jpg", - Body: buf2 + Key: 'upload/long_image3.jpg', + Body: buf2, }; const uploadPromises = []; @@ -222,7 +222,7 @@ describe('Test put api', () => { const uploadResults = await Promise.all(uploadPromises); expect(uploadResults[0].Key).toBe('upload/long_image1.jpg'); - expect(uploadResults[1].Key).toBe("upload/long_image2.jpg"); - expect(uploadResults[2].Key).toBe("upload/long_image3.jpg"); + expect(uploadResults[1].Key).toBe('upload/long_image2.jpg'); + expect(uploadResults[2].Key).toBe('upload/long_image3.jpg'); }); }); diff --git a/packages/amplify-storage-simulator/src/index.ts b/packages/amplify-storage-simulator/src/index.ts index 5974d603e8..f79c961970 100755 --- a/packages/amplify-storage-simulator/src/index.ts +++ b/packages/amplify-storage-simulator/src/index.ts @@ -1,4 +1,4 @@ -import { StorageServer } from "./server/S3server"; +import { StorageServer } from './server/S3server'; export interface StorageSimulatorDataSourceBaseConfig { name: string; diff --git a/packages/amplify-storage-simulator/src/server/S3server.ts b/packages/amplify-storage-simulator/src/server/S3server.ts index 8997886fb1..d352424561 100755 --- a/packages/amplify-storage-simulator/src/server/S3server.ts +++ b/packages/amplify-storage-simulator/src/server/S3server.ts @@ -208,17 +208,14 @@ export class StorageServer extends EventEmitter { } private async handleRequestPut(request, response) { - const directoryPath = normalize( - join(String(this.localDirectoryPath), String(request.params.path)) - ); + const directoryPath = normalize(join(String(this.localDirectoryPath), String(request.params.path))); ensureFileSync(directoryPath); // strip signature in android , returns same buffer for other clients var new_data = util.stripChunkSignature(request.body); // loading data in map for each part if (request.query.partNumber !== undefined) { this.upload_bufferMap[request.query.uploadId][request.query.partNumber] = request.body; - } - else { + } else { writeFileSync(directoryPath, new_data); // event trigger to differentitiate between multipart and normal put let eventObj = this.createEvent(request); @@ -228,9 +225,7 @@ export class StorageServer extends EventEmitter { } private async handleRequestPost(request, response) { - const directoryPath = normalize( - join(String(this.localDirectoryPath), String(request.params.path)) - ); + const directoryPath = normalize(join(String(this.localDirectoryPath), String(request.params.path))); if (request.query.uploads !== undefined) { let id = uuid(); this.uploadIds.push(id); @@ -241,7 +236,7 @@ export class StorageServer extends EventEmitter { InitiateMultipartUploadResult: { Bucket: this.route, Key: request.params.path, - UploadId: id + UploadId: id, }, }) ); @@ -266,14 +261,12 @@ export class StorageServer extends EventEmitter { ); let buf = Buffer.concat(arr); writeFileSync(directoryPath, buf); - + // event trigger for multipart post let eventObj = this.createEvent(request); this.emit('event', eventObj); } else { - const directoryPath = normalize( - join(String(this.localDirectoryPath), String(request.params.path)) - ); + const directoryPath = normalize(join(String(this.localDirectoryPath), String(request.params.path))); ensureFileSync(directoryPath); var new_data = util.stripChunkSignature(request.body); writeFileSync(directoryPath, new_data); diff --git a/packages/amplify-storage-simulator/src/server/utils.ts b/packages/amplify-storage-simulator/src/server/utils.ts index 4c9dccf5d3..14f7cd887b 100644 --- a/packages/amplify-storage-simulator/src/server/utils.ts +++ b/packages/amplify-storage-simulator/src/server/utils.ts @@ -6,11 +6,10 @@ export function parseUrl(request, route: String) { request.url = normalize(decodeURIComponent(request.url)); const temp = request.url.split(route); request.params.path = ''; - + if (request.query.prefix !== undefined) request.params.path = request.query.prefix + '/'; - if (temp[1] !== undefined) - request.params.path = normalize(join(request.params.path, temp[1].split('?')[0])); + if (temp[1] !== undefined) request.params.path = normalize(join(request.params.path, temp[1].split('?')[0])); // change for IOS as no bucket name is present in the original url else request.params.path = normalize(join(request.params.path, temp[0].split('?')[0])); diff --git a/packages/amplify-ui-tests/__tests__/js/auth.test.ts b/packages/amplify-ui-tests/__tests__/js/auth.test.ts index a244bd7c0d..9c41c1b675 100644 --- a/packages/amplify-ui-tests/__tests__/js/auth.test.ts +++ b/packages/amplify-ui-tests/__tests__/js/auth.test.ts @@ -1,16 +1,19 @@ -import { - initProjectWithProfile, - deleteProject, - amplifyPush -} from '../../src/init'; +import { initProjectWithProfile, deleteProject, amplifyPush } from '../../src/init'; import { createNewProjectDir, deleteProjectDir, createTestMetaFile, getUITestConfig } from '../../src/utils'; import { addAuthWithDefault } from '../../src/categories/auth'; -import { existsAWSExportsPath, copyAWSExportsToProj} from '../../src/utils/projectMeta'; -import { runCypressTest, startServer, closeServer, gitCloneSampleApp, buildApp, signUpNewUser, setupCypress } from '../../src/utils/command' +import { existsAWSExportsPath, copyAWSExportsToProj } from '../../src/utils/projectMeta'; +import { + runCypressTest, + startServer, + closeServer, + gitCloneSampleApp, + buildApp, + signUpNewUser, + setupCypress, +} from '../../src/utils/command'; import { join } from 'path'; - describe('Auth tests in Javascript SDK:', () => { let projRoot: string; let destRoot: string; @@ -19,14 +22,13 @@ describe('Auth tests in Javascript SDK:', () => { const JS_SAMPLE_APP_REPO: string = gitRepo; describe('Simple Auth UI test:', async () => { - const { apps } = Auth.simpleAuth; let settings = {}; beforeAll(async () => { projRoot = createNewProjectDir(); // create a new project for each test jest.setTimeout(1000 * 60 * 60); // 1 hour timeout as pushing might be slow - await gitCloneSampleApp(projRoot, {repo: JS_SAMPLE_APP_REPO}); + await gitCloneSampleApp(projRoot, { repo: JS_SAMPLE_APP_REPO }); destRoot = projRoot + '/amplify-js-samples-staging'; await setupCypress(destRoot); }); @@ -45,25 +47,24 @@ describe('Auth tests in Javascript SDK:', () => { it('should have user pool in backend and sign up a user for test', async () => { settings = await signUpNewUser(projRoot); - }) - + }); describe('Run UI tests on JS app', async () => { let appPort = AUTH_PORT_NUMBER; afterEach(async () => { - closeServer({port: appPort}); - }) + closeServer({ port: appPort }); + }); for (let i = 0; i < apps.length; i++) { it('should pass all UI tests on app <' + apps[i].name + '>', async () => { const appRoot = join(destRoot, apps[i].path); appPort = apps[i].port ? apps[i].port : AUTH_PORT_NUMBER; copyAWSExportsToProj(projRoot, appRoot); - await createTestMetaFile(destRoot, {...settings, port: appPort, name: apps[i].name, testFiles: apps[i].testFiles}); + await createTestMetaFile(destRoot, { ...settings, port: appPort, name: apps[i].name, testFiles: apps[i].testFiles }); await buildApp(appRoot); - await startServer(appRoot, {port: appPort}); - await runCypressTest(destRoot).then(isPassed => expect(isPassed).toBeTruthy()) + await startServer(appRoot, { port: appPort }); + await runCypressTest(destRoot).then(isPassed => expect(isPassed).toBeTruthy()); }); } - }) - }) + }); + }); }); diff --git a/packages/amplify-ui-tests/__tests__/js/predictions.test.ts b/packages/amplify-ui-tests/__tests__/js/predictions.test.ts index 9f15a02f0f..06c41c075f 100644 --- a/packages/amplify-ui-tests/__tests__/js/predictions.test.ts +++ b/packages/amplify-ui-tests/__tests__/js/predictions.test.ts @@ -1,66 +1,73 @@ -import { getUITestConfig, createNewProjectDir, deleteProjectDir, createTestMetaFile } from "../../src/utils"; -import { gitCloneSampleApp, setupCypress, signUpNewUser, closeServer, buildApp, startServer, runCypressTest } from "../../src/utils/command"; -import { deleteProject, initProjectWithProfile, amplifyPush } from "../../src/init"; -import { addAuthWithDefault } from "../../src/categories/auth"; -import { addIdentityText, addConvertWithDefault } from "../../src/categories/predictions"; -import { existsAWSExportsPath, copyAWSExportsToProj } from "../../src/utils/projectMeta"; -import { join } from "path"; +import { getUITestConfig, createNewProjectDir, deleteProjectDir, createTestMetaFile } from '../../src/utils'; +import { + gitCloneSampleApp, + setupCypress, + signUpNewUser, + closeServer, + buildApp, + startServer, + runCypressTest, +} from '../../src/utils/command'; +import { deleteProject, initProjectWithProfile, amplifyPush } from '../../src/init'; +import { addAuthWithDefault } from '../../src/categories/auth'; +import { addIdentityText, addConvertWithDefault } from '../../src/categories/predictions'; +import { existsAWSExportsPath, copyAWSExportsToProj } from '../../src/utils/projectMeta'; +import { join } from 'path'; describe('Prediction tests in JavaScript SDK:', () => { - let projRoot: string; - let destRoot: string; - const { Predictions, gitRepo } = getUITestConfig(); - const PREDICT_PORT_NUMBER: string = Predictions.port; - const JS_SAMPLE_APP_REPO: string = - process.env.AMPLIFY_JS_SAMPLES_STAGING_URL ? process.env.AMPLIFY_JS_SAMPLES_STAGING_URL : gitRepo; + let projRoot: string; + let destRoot: string; + const { Predictions, gitRepo } = getUITestConfig(); + const PREDICT_PORT_NUMBER: string = Predictions.port; + const JS_SAMPLE_APP_REPO: string = process.env.AMPLIFY_JS_SAMPLES_STAGING_URL ? process.env.AMPLIFY_JS_SAMPLES_STAGING_URL : gitRepo; - describe('Simple predictions UI test:', async () => { - const { apps } = Predictions.simplePredictions; - let settings = {}; + describe('Simple predictions UI test:', async () => { + const { apps } = Predictions.simplePredictions; + let settings = {}; - beforeAll(async () => { - projRoot = createNewProjectDir(); - jest.setTimeout(1000 * 60 * 60); // 1 hour - await gitCloneSampleApp(projRoot, {repo: JS_SAMPLE_APP_REPO}); - destRoot = projRoot + '/amplify-js-samples-staging'; - await setupCypress(destRoot); - }); + beforeAll(async () => { + projRoot = createNewProjectDir(); + jest.setTimeout(1000 * 60 * 60); // 1 hour + await gitCloneSampleApp(projRoot, { repo: JS_SAMPLE_APP_REPO }); + destRoot = projRoot + '/amplify-js-samples-staging'; + await setupCypress(destRoot); + }); - afterAll(async () => { - await deleteProject(projRoot, true, true); - deleteProjectDir(projRoot); - }); + afterAll(async () => { + await deleteProject(projRoot, true, true); + deleteProjectDir(projRoot); + }); - it('should set up amplify backend and generate aws-export.js file', async () => { - await initProjectWithProfile(projRoot, {}, true); - await addAuthWithDefault(projRoot, {}, true); // should add auth before add predictions - await addIdentityText(projRoot, {}, true); - await addConvertWithDefault(projRoot, {}, true); - await amplifyPush(projRoot, true); - expect(existsAWSExportsPath(projRoot, 'js')).toBeTruthy() - }); + it('should set up amplify backend and generate aws-export.js file', async () => { + await initProjectWithProfile(projRoot, {}, true); + await addAuthWithDefault(projRoot, {}, true); // should add auth before add predictions + await addIdentityText(projRoot, {}, true); + await addConvertWithDefault(projRoot, {}, true); + await amplifyPush(projRoot, true); + expect(existsAWSExportsPath(projRoot, 'js')).toBeTruthy(); + }); - it('should have user pool in backend and sign up a user for test', async () => { - settings = await signUpNewUser(projRoot); - }); + it('should have user pool in backend and sign up a user for test', async () => { + settings = await signUpNewUser(projRoot); + }); - describe('Run UI tests on JS app', async () => { - let appPort = PREDICT_PORT_NUMBER; - afterEach(async () => { - closeServer({port: appPort}); - }); + describe('Run UI tests on JS app', async () => { + let appPort = PREDICT_PORT_NUMBER; + afterEach(async () => { + closeServer({ port: appPort }); + }); - for (let i = 0; i < apps.length; i++) { - it(`should pass all UI tests on app <${apps[i].name}>`, async () => { - const appRoot = join(destRoot, apps[i].path); - appPort = apps[i].port ? apps[i].port : PREDICT_PORT_NUMBER; - copyAWSExportsToProj(projRoot, appRoot); - await createTestMetaFile(destRoot, {...settings, port: appPort, name: apps[i].name, testFiles: apps[i].testFiles}); - await buildApp(appRoot); - await startServer(appRoot, {port: appPort}); - await runCypressTest(destRoot).then(isPassed => expect(isPassed).toBeTruthy()); - }); - } - }); + for (let i = 0; i < apps.length; i++) { + it(`should pass all UI tests on app <${apps[i].name}>`, async () => { + const appRoot = join(destRoot, apps[i].path); + appPort = apps[i].port ? apps[i].port : PREDICT_PORT_NUMBER; + copyAWSExportsToProj(projRoot, appRoot); + await createTestMetaFile(destRoot, { ...settings, port: appPort, name: apps[i].name, testFiles: apps[i].testFiles }); + await buildApp(appRoot); + await startServer(appRoot, { port: appPort }); + await runCypressTest(destRoot).then(isPassed => expect(isPassed).toBeTruthy()); + }); + } }); + }); }); diff --git a/packages/amplify-ui-tests/__tests__/js/storage.test.ts b/packages/amplify-ui-tests/__tests__/js/storage.test.ts index 4b8ea9013d..1a7f55cf4c 100644 --- a/packages/amplify-ui-tests/__tests__/js/storage.test.ts +++ b/packages/amplify-ui-tests/__tests__/js/storage.test.ts @@ -1,71 +1,74 @@ -import { - initProjectWithProfile, - deleteProject, - amplifyPushApi - } from '../../src/init'; +import { initProjectWithProfile, deleteProject, amplifyPushApi } from '../../src/init'; import { addStorageWithDefault } from '../../src/categories/storage'; import { createNewProjectDir, deleteProjectDir, createTestMetaFile, getUITestConfig } from '../../src/utils'; import { addAuthWithDefault } from '../../src/categories/auth'; import { copyAWSExportsToProj, existsAWSExportsPath } from '../../src/utils/projectMeta'; -import { runCypressTest, gitCloneSampleApp, buildApp, startServer, closeServer, signUpNewUser, setupCypress } from '../../src/utils/command'; +import { + runCypressTest, + gitCloneSampleApp, + buildApp, + startServer, + closeServer, + signUpNewUser, + setupCypress, +} from '../../src/utils/command'; import { join } from 'path'; import { addApiWithSimpleModel } from '../../src/categories/api'; describe('Storage tests in Javascript SDK:', () => { - let projRoot: string; - let destRoot: string; - const { Storage, gitRepo } = getUITestConfig(); - const STORAGE_PORT_NUMBER: string = Storage.port; - const JS_SAMPLE_APP_REPO: string = gitRepo; - - describe('Simple Storage UI test:', async () => { + let projRoot: string; + let destRoot: string; + const { Storage, gitRepo } = getUITestConfig(); + const STORAGE_PORT_NUMBER: string = Storage.port; + const JS_SAMPLE_APP_REPO: string = gitRepo; - const { apps } = Storage.simpleStorageWithGraphQL; - let settings = {}; + describe('Simple Storage UI test:', async () => { + const { apps } = Storage.simpleStorageWithGraphQL; + let settings = {}; - beforeAll(async () => { - projRoot = createNewProjectDir(); - jest.setTimeout(1000 * 60 * 60); // 1 hour - await gitCloneSampleApp(projRoot, {repo: JS_SAMPLE_APP_REPO}); - destRoot = projRoot + '/amplify-js-samples-staging'; - await setupCypress(destRoot); - }); + beforeAll(async () => { + projRoot = createNewProjectDir(); + jest.setTimeout(1000 * 60 * 60); // 1 hour + await gitCloneSampleApp(projRoot, { repo: JS_SAMPLE_APP_REPO }); + destRoot = projRoot + '/amplify-js-samples-staging'; + await setupCypress(destRoot); + }); - afterAll(async () => { - await deleteProject(projRoot, true, true); - deleteProjectDir(projRoot); - }); + afterAll(async () => { + await deleteProject(projRoot, true, true); + deleteProjectDir(projRoot); + }); - it('should set up amplify backend and generate aws-export.js file', async () => { - await initProjectWithProfile(projRoot, {}, true); - await addAuthWithDefault(projRoot, {}, true); // should add auth before add storage - await addStorageWithDefault(projRoot, {}, true); - await addApiWithSimpleModel(projRoot, {}, true); - await amplifyPushApi(projRoot, true); - expect(existsAWSExportsPath(projRoot, 'js')).toBeTruthy() - }); + it('should set up amplify backend and generate aws-export.js file', async () => { + await initProjectWithProfile(projRoot, {}, true); + await addAuthWithDefault(projRoot, {}, true); // should add auth before add storage + await addStorageWithDefault(projRoot, {}, true); + await addApiWithSimpleModel(projRoot, {}, true); + await amplifyPushApi(projRoot, true); + expect(existsAWSExportsPath(projRoot, 'js')).toBeTruthy(); + }); - it('should have user pool in backend and sign up a user for test', async () => { - settings = await signUpNewUser(projRoot); - }) + it('should have user pool in backend and sign up a user for test', async () => { + settings = await signUpNewUser(projRoot); + }); - describe('Run UI tests on JS app', async () => { - let appPort = STORAGE_PORT_NUMBER; - afterEach(async () => { - closeServer({port: appPort}); - }); + describe('Run UI tests on JS app', async () => { + let appPort = STORAGE_PORT_NUMBER; + afterEach(async () => { + closeServer({ port: appPort }); + }); - for (let i = 0; i < apps.length; i++) { - it(`should pass all UI tests on app <${apps[i].name}>`, async () => { - const appRoot = join(destRoot, apps[i].path); - appPort = apps[i].port ? apps[i].port : STORAGE_PORT_NUMBER; - copyAWSExportsToProj(projRoot, appRoot); - await createTestMetaFile(destRoot, {...settings, port: appPort, name: apps[i].name, testFiles: apps[i].testFiles}); - await buildApp(appRoot); - await startServer(appRoot, {port: appPort}); - await runCypressTest(destRoot).then(isPassed => expect(isPassed).toBeTruthy()); - }); - } + for (let i = 0; i < apps.length; i++) { + it(`should pass all UI tests on app <${apps[i].name}>`, async () => { + const appRoot = join(destRoot, apps[i].path); + appPort = apps[i].port ? apps[i].port : STORAGE_PORT_NUMBER; + copyAWSExportsToProj(projRoot, appRoot); + await createTestMetaFile(destRoot, { ...settings, port: appPort, name: apps[i].name, testFiles: apps[i].testFiles }); + await buildApp(appRoot); + await startServer(appRoot, { port: appPort }); + await runCypressTest(destRoot).then(isPassed => expect(isPassed).toBeTruthy()); }); + } }); -}) + }); +}); diff --git a/packages/amplify-ui-tests/src/categories/api.ts b/packages/amplify-ui-tests/src/categories/api.ts index c203b55af1..a93013210f 100644 --- a/packages/amplify-ui-tests/src/categories/api.ts +++ b/packages/amplify-ui-tests/src/categories/api.ts @@ -7,7 +7,7 @@ const defaultSettings = { }; export function readSchemaDocument(schemaName: string): string { - const docPath = `${__dirname}/../../schemas/${schemaName}.graphql` + const docPath = `${__dirname}/../../schemas/${schemaName}.graphql`; if (fs.existsSync(docPath)) { return fs.readFileSync(docPath).toString(); } else { @@ -16,15 +16,11 @@ export function readSchemaDocument(schemaName: string): string { } function getSchemaPath(schemaName: string): string { - return `${__dirname}/../../schemas/${schemaName}.graphql`; + return `${__dirname}/../../schemas/${schemaName}.graphql`; } -export function addApiWithSimpleModel( - cwd: string, - settings: any = {}, - verbose: boolean = !isCI() -) { - settings = {...defaultSettings, ...settings}; +export function addApiWithSimpleModel(cwd: string, settings: any = {}, verbose: boolean = !isCI()) { + settings = { ...defaultSettings, ...settings }; const schemaName: string = settings.schemaName ? settings.schemaName : 'simple_model'; readSchemaDocument(schemaName); return new Promise((resolve, reject) => { @@ -48,6 +44,6 @@ export function addApiWithSimpleModel( } else { reject(err); } - }) - }) + }); + }); } diff --git a/packages/amplify-ui-tests/src/categories/auth.ts b/packages/amplify-ui-tests/src/categories/auth.ts index b3403e55c9..d9e72623be 100644 --- a/packages/amplify-ui-tests/src/categories/auth.ts +++ b/packages/amplify-ui-tests/src/categories/auth.ts @@ -1,11 +1,7 @@ import * as nexpect from 'nexpect'; import { getCLIPath, isCI, getEnvVars } from '../utils'; -export function addAuthWithDefault( - cwd: string, - settings: any = {}, - verbose: boolean = !isCI() -) { +export function addAuthWithDefault(cwd: string, settings: any = {}, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true, verbose }) @@ -22,32 +18,33 @@ export function addAuthWithDefault( } else { reject(err); } - }) - }) + }); + }); } -export function addAuthWithDefaultSocial( - cwd: string, - settings: any = {}, - verbose: boolean = !isCI() -) { +export function addAuthWithDefaultSocial(cwd: string, settings: any = {}, verbose: boolean = !isCI()) { return new Promise((resolve, reject) => { - const { - FACEBOOK_APP_ID, - FACEBOOK_APP_SECRET, - GOOGLE_APP_ID, - GOOGLE_APP_SECRET, - AMAZON_APP_ID, - AMAZON_APP_SECRET, - }: any = getEnvVars(); + const { FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, GOOGLE_APP_ID, GOOGLE_APP_SECRET, AMAZON_APP_ID, AMAZON_APP_SECRET }: any = getEnvVars(); const missingVars = []; - if (!FACEBOOK_APP_ID) { missingVars.push('FACEBOOK_APP_ID') }; - if (!FACEBOOK_APP_SECRET) { missingVars.push('FACEBOOK_APP_SECRET') }; - if (!GOOGLE_APP_ID) { missingVars.push('GOOGLE_APP_ID') }; - if (!GOOGLE_APP_SECRET) { missingVars.push('GOOGLE_APP_SECRET') }; - if (!AMAZON_APP_ID) { missingVars.push('AMAZON_APP_ID') }; - if (!AMAZON_APP_SECRET) { missingVars.push('AMAZON_APP_SECRET') }; + if (!FACEBOOK_APP_ID) { + missingVars.push('FACEBOOK_APP_ID'); + } + if (!FACEBOOK_APP_SECRET) { + missingVars.push('FACEBOOK_APP_SECRET'); + } + if (!GOOGLE_APP_ID) { + missingVars.push('GOOGLE_APP_ID'); + } + if (!GOOGLE_APP_SECRET) { + missingVars.push('GOOGLE_APP_SECRET'); + } + if (!AMAZON_APP_ID) { + missingVars.push('AMAZON_APP_ID'); + } + if (!AMAZON_APP_SECRET) { + missingVars.push('AMAZON_APP_SECRET'); + } if (missingVars.length > 0) { throw new Error(`.env file is missing the following key/values: ${missingVars.join(', ')} `); @@ -103,6 +100,6 @@ export function addAuthWithDefaultSocial( } else { reject(err); } - }) - }) + }); + }); } diff --git a/packages/amplify-ui-tests/src/categories/predictions.ts b/packages/amplify-ui-tests/src/categories/predictions.ts index e00d8f02a4..561791194c 100644 --- a/packages/amplify-ui-tests/src/categories/predictions.ts +++ b/packages/amplify-ui-tests/src/categories/predictions.ts @@ -1,60 +1,52 @@ -import { isCI, getCLIPath } from "../utils"; +import { isCI, getCLIPath } from '../utils'; import * as nexpect from 'nexpect'; -export function addIdentityText ( - cwd: string, - settings: any = {}, - verbose: boolean = !isCI() -) { - return new Promise((resolve, reject) => { - nexpect - .spawn(getCLIPath(), ['add', 'predictions'], {cwd, stripColors: true, verbose}) - .wait('Please select from one of the categories below') - .sendline('\r') - .wait('What would you like to identify?') - .sendline('\r') - .wait('Provide a friendly name for your resource') - .sendline('\r') - .wait('Would you also like to identify documents?') - .sendline('y\r') - .wait('Who should have access?') - .sendline('j\r') - .run((err: Error) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); +export function addIdentityText(cwd: string, settings: any = {}, verbose: boolean = !isCI()) { + return new Promise((resolve, reject) => { + nexpect + .spawn(getCLIPath(), ['add', 'predictions'], { cwd, stripColors: true, verbose }) + .wait('Please select from one of the categories below') + .sendline('\r') + .wait('What would you like to identify?') + .sendline('\r') + .wait('Provide a friendly name for your resource') + .sendline('\r') + .wait('Would you also like to identify documents?') + .sendline('y\r') + .wait('Who should have access?') + .sendline('j\r') + .run((err: Error) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); } -export function addConvertWithDefault( - cwd: string, - settings: any = {}, - verbose: boolean = !isCI() -) { - return new Promise((resolve, reject) => { - nexpect - .spawn(getCLIPath(), ['add', 'predictions'], {cwd, stripColors: true, verbose}) - .wait('Please select from one of the categories below') - .sendline('j\r') - .wait('What would you like to convert?') - .sendline('\r') - .wait('Provide a friendly name for your resource') - .sendline('\r') - .wait('What is the source language?') - .sendline('\r') - .wait('What is the target language?') - .sendline('\r') - .wait('Who should have access?') - .sendline('j\r') - .run((err: Error) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} \ No newline at end of file +export function addConvertWithDefault(cwd: string, settings: any = {}, verbose: boolean = !isCI()) { + return new Promise((resolve, reject) => { + nexpect + .spawn(getCLIPath(), ['add', 'predictions'], { cwd, stripColors: true, verbose }) + .wait('Please select from one of the categories below') + .sendline('j\r') + .wait('What would you like to convert?') + .sendline('\r') + .wait('Provide a friendly name for your resource') + .sendline('\r') + .wait('What is the source language?') + .sendline('\r') + .wait('What is the target language?') + .sendline('\r') + .wait('Who should have access?') + .sendline('j\r') + .run((err: Error) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} diff --git a/packages/amplify-ui-tests/src/categories/storage.ts b/packages/amplify-ui-tests/src/categories/storage.ts index 7251ceaf90..e148dbc83d 100644 --- a/packages/amplify-ui-tests/src/categories/storage.ts +++ b/packages/amplify-ui-tests/src/categories/storage.ts @@ -3,33 +3,29 @@ import * as nexpect from 'nexpect'; import { getCLIPath, isCI } from '../utils'; //content, auth user only, all access -export function addStorageWithDefault( - cwd: string, - settings: any = {}, - verbose: boolean = !isCI() - ) { - return new Promise((resolve, reject) => { - nexpect - .spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true, verbose}) - .wait('Please select from one of the below mentioned services') - .sendline('\r') - .wait('Please provide a friendly name for your resource that will be used to label') - .sendline('\r') - .wait('Please provide bucket name:') - .sendline('\r') - .wait('Who should have access:') - .sendline('\r') - .wait('What kind of access do you want for Authenticated users?') - .sendline('a') - .sendline('\r') - .wait('Do you want to add a Lambda Trigger for your S3 Bucket?') - .sendline('n\r') - .run(function(err: Error) { - if (!err) { - resolve(); - } else { - reject(err); - } - }) - }) - } \ No newline at end of file +export function addStorageWithDefault(cwd: string, settings: any = {}, verbose: boolean = !isCI()) { + return new Promise((resolve, reject) => { + nexpect + .spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true, verbose }) + .wait('Please select from one of the below mentioned services') + .sendline('\r') + .wait('Please provide a friendly name for your resource that will be used to label') + .sendline('\r') + .wait('Please provide bucket name:') + .sendline('\r') + .wait('Who should have access:') + .sendline('\r') + .wait('What kind of access do you want for Authenticated users?') + .sendline('a') + .sendline('\r') + .wait('Do you want to add a Lambda Trigger for your S3 Bucket?') + .sendline('n\r') + .run(function(err: Error) { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} diff --git a/packages/amplify-ui-tests/src/configure/index.ts b/packages/amplify-ui-tests/src/configure/index.ts index 4de5ab0ddc..39cb88d824 100644 --- a/packages/amplify-ui-tests/src/configure/index.ts +++ b/packages/amplify-ui-tests/src/configure/index.ts @@ -2,9 +2,9 @@ import * as nexpect from 'nexpect'; import { getCLIPath, isCI } from '../utils'; type AmplifyConfiguration = { - accessKeyId: string, - secretAccessKey: string, - profileName?: string, + accessKeyId: string; + secretAccessKey: string; + profileName?: string; }; const defaultSettings = { profileName: 'amplify-integ-test-user', @@ -13,10 +13,7 @@ const defaultSettings = { }; const MANDATORY_PARAMS = ['accessKeyId', 'secretAccessKey']; -export default function amplifyConfigure( - settings: AmplifyConfiguration, - verbose: Boolean = isCI() ? false : true -) { +export default function amplifyConfigure(settings: AmplifyConfiguration, verbose: Boolean = isCI() ? false : true) { const s = { ...defaultSettings, ...settings }; const missingParam = MANDATORY_PARAMS.filter(p => !Object.keys(s).includes(p)); if (missingParam.length) { @@ -33,7 +30,7 @@ export default function amplifyConfigure( .sendline('\r') .wait('user name:') .sendline('\r') - .wait("Press Enter to continue") + .wait('Press Enter to continue') .sendline('\r') .wait('accessKeyId') .sendline(s.accessKeyId) @@ -41,9 +38,7 @@ export default function amplifyConfigure( .sendline(s.secretAccessKey) .wait('Profile Name:') .sendline(s.profileName) - .wait( - 'Successfully set up the new user.' - ) + .wait('Successfully set up the new user.') .run(function(err: Error) { if (!err) { resolve(); diff --git a/packages/amplify-ui-tests/src/configure_tests.ts b/packages/amplify-ui-tests/src/configure_tests.ts index e70e09f1b0..2169281707 100644 --- a/packages/amplify-ui-tests/src/configure_tests.ts +++ b/packages/amplify-ui-tests/src/configure_tests.ts @@ -8,14 +8,12 @@ async function setUpAmplify() { AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { - throw new Error( - 'Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env' - ); + throw new Error('Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env'); } await configure({ accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY, - profileName: 'amplify-integ-test-user' + profileName: 'amplify-integ-test-user', }); } else { console.log('AWS Profile is already configured'); diff --git a/packages/amplify-ui-tests/src/init/amplifyPush.ts b/packages/amplify-ui-tests/src/init/amplifyPush.ts index 649ad13f26..aeb54726f1 100644 --- a/packages/amplify-ui-tests/src/init/amplifyPush.ts +++ b/packages/amplify-ui-tests/src/init/amplifyPush.ts @@ -1,10 +1,7 @@ import * as nexpect from 'nexpect'; import { getCLIPath, isCI } from '../utils'; -function amplifyPushApi( - cwd: string, - verbose: Boolean = isCI() ? false : true -) { +function amplifyPushApi(cwd: string, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['push'], { cwd, stripColors: true, verbose }) @@ -23,10 +20,7 @@ function amplifyPushApi( }); } -function amplifyPush( - cwd: string, - verbose: Boolean = isCI() ? false : true -) { +function amplifyPush(cwd: string, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect .spawn(getCLIPath(), ['push'], { cwd, stripColors: true, verbose }) @@ -43,4 +37,4 @@ function amplifyPush( }); } -export { amplifyPushApi, amplifyPush }; \ No newline at end of file +export { amplifyPushApi, amplifyPush }; diff --git a/packages/amplify-ui-tests/src/init/deleteProject.ts b/packages/amplify-ui-tests/src/init/deleteProject.ts index c914dbf732..3a75f011d2 100644 --- a/packages/amplify-ui-tests/src/init/deleteProject.ts +++ b/packages/amplify-ui-tests/src/init/deleteProject.ts @@ -3,11 +3,7 @@ import * as nexpect from 'nexpect'; import { getCLIPath, isCI } from '../utils'; import { getProjectMeta } from '../utils'; -export default function deleteProject( - cwd: string, - deleteDeploymentBucket: Boolean = true, - verbose: Boolean = isCI() ? false : true -) { +export default function deleteProject(cwd: string, deleteDeploymentBucket: Boolean = true, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { const meta = getProjectMeta(cwd).providers.awscloudformation; nexpect @@ -20,9 +16,7 @@ export default function deleteProject( const { DeploymentBucketName } = meta; if (deleteDeploymentBucket) { const s3 = new AWS.S3(); - const { Contents: items } = await s3 - .listObjects({ Bucket: DeploymentBucketName }) - .promise(); + const { Contents: items } = await s3.listObjects({ Bucket: DeploymentBucketName }).promise(); const promises = []; items.forEach(item => { promises.push(s3.deleteObject({ Bucket: DeploymentBucketName, Key: item.Key }).promise()); diff --git a/packages/amplify-ui-tests/src/init/index.ts b/packages/amplify-ui-tests/src/init/index.ts index 0a507f1dfa..8652c69d64 100644 --- a/packages/amplify-ui-tests/src/init/index.ts +++ b/packages/amplify-ui-tests/src/init/index.ts @@ -1,9 +1,3 @@ -export { - default as initProjectWithProfile, - initAndroidProject, - initIosProject -} from './initProjectHelper'; -export { - amplifyPushApi, amplifyPush -} from './amplifyPush'; +export { default as initProjectWithProfile, initAndroidProject, initIosProject } from './initProjectHelper'; +export { amplifyPushApi, amplifyPush } from './amplifyPush'; export { default as deleteProject } from './deleteProject'; diff --git a/packages/amplify-ui-tests/src/init/initProjectHelper.ts b/packages/amplify-ui-tests/src/init/initProjectHelper.ts index b5fcadf640..71e73d0194 100644 --- a/packages/amplify-ui-tests/src/init/initProjectHelper.ts +++ b/packages/amplify-ui-tests/src/init/initProjectHelper.ts @@ -12,14 +12,10 @@ const defaultSettings = { buildCmd: '\r', startCmd: '\r', useProfile: '\r', - profileName: '\r' + profileName: '\r', }; -export default function initProjectWithProfile( - cwd: string, - settings: any = {}, - verbose: Boolean = isCI() ? false : true -) { +export default function initProjectWithProfile(cwd: string, settings: any = {}, verbose: Boolean = isCI() ? false : true) { const s = { ...defaultSettings, ...settings }; return new Promise((resolve, reject) => { nexpect @@ -47,9 +43,7 @@ export default function initProjectWithProfile( .sendline('y') .wait('Please choose the profile you want to use') .sendline(s.profileName) - .wait( - 'Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything' - ) + .wait('Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything') .run(function(err: Error) { if (!err) { resolve(); @@ -60,14 +54,10 @@ export default function initProjectWithProfile( }); } -export function initAndroidProject( - cwd: string, - settings: any = {}, - verbose: Boolean = isCI() ? false : true -) { +export function initAndroidProject(cwd: string, settings: any = {}, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect - .spawn(getCLIPath(), ['init'], {cwd, stripColors: true, verbose}) + .spawn(getCLIPath(), ['init'], { cwd, stripColors: true, verbose }) .wait('Enter a name for the project') .sendline('\r') .wait('Enter a name for the environment') @@ -82,9 +72,7 @@ export function initAndroidProject( .sendline('y') .wait('Please choose the profile you want to use') .sendline('\r') - .wait( - 'Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything' - ) + .wait('Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything') .run(function(err: Error) { if (!err) { resolve(); @@ -92,17 +80,13 @@ export function initAndroidProject( reject(err); } }); - }) + }); } -export function initIosProject( - cwd: string, - settings: any = {}, - verbose: Boolean = isCI() ? false : true -) { +export function initIosProject(cwd: string, settings: any = {}, verbose: Boolean = isCI() ? false : true) { return new Promise((resolve, reject) => { nexpect - .spawn(getCLIPath(), ['init'], {cwd, stripColors: true, verbose}) + .spawn(getCLIPath(), ['init'], { cwd, stripColors: true, verbose }) .wait('Enter a name for the project') .sendline('\r') .wait('Enter a name for the environment') @@ -116,9 +100,7 @@ export function initIosProject( .sendline('y') .wait('Please choose the profile you want to use') .sendline('\r') - .wait( - 'Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything' - ) + .wait('Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything') .run(function(err: Error) { if (!err) { resolve(); @@ -126,5 +108,5 @@ export function initIosProject( reject(err); } }); - }) + }); } diff --git a/packages/amplify-ui-tests/src/main/delete.ts b/packages/amplify-ui-tests/src/main/delete.ts index b0fdfed431..a4ed27839d 100644 --- a/packages/amplify-ui-tests/src/main/delete.ts +++ b/packages/amplify-ui-tests/src/main/delete.ts @@ -1,18 +1,18 @@ -import { deleteProject } from "../init"; -import { existsSync } from "fs"; +import { deleteProject } from '../init'; +import { existsSync } from 'fs'; //node delete.js [projectRoot] async function deleteProj(projRoot: string) { - await deleteProject(projRoot); + await deleteProject(projRoot); } if (process.argv.length !== 3) { - console.log('Usage: node ./lib/main/delete [project_root]'); - process.exit(1); + console.log('Usage: node ./lib/main/delete [project_root]'); + process.exit(1); } const projRoot = process.argv[2]; if (!existsSync(projRoot)) { - console.log('Project path does not exist.'); - process.exit(1); + console.log('Project path does not exist.'); + process.exit(1); } -deleteProj(process.argv[2]); \ No newline at end of file +deleteProj(process.argv[2]); diff --git a/packages/amplify-ui-tests/src/main/setup_aws_resources.ts b/packages/amplify-ui-tests/src/main/setup_aws_resources.ts index e2fb42d8a6..560aa31a2e 100644 --- a/packages/amplify-ui-tests/src/main/setup_aws_resources.ts +++ b/packages/amplify-ui-tests/src/main/setup_aws_resources.ts @@ -1,70 +1,66 @@ -import initProjectWithProfile, { initIosProject, initAndroidProject } from "../init/initProjectHelper"; -import { addAuthWithDefault } from "../categories/auth"; -import { signUpNewUser } from "../utils/command" -import { amplifyPushApi, amplifyPush } from "../init"; -import { addStorageWithDefault } from "../categories/storage"; -import { addApiWithSimpleModel } from "../categories/api"; -import { existsSync } from "fs"; - - +import initProjectWithProfile, { initIosProject, initAndroidProject } from '../init/initProjectHelper'; +import { addAuthWithDefault } from '../categories/auth'; +import { signUpNewUser } from '../utils/command'; +import { amplifyPushApi, amplifyPush } from '../init'; +import { addStorageWithDefault } from '../categories/storage'; +import { addApiWithSimpleModel } from '../categories/api'; +import { existsSync } from 'fs'; async function test(projRoot: string, sdk: string, categories: string[]) { - let addApi: Boolean = false; - //check sdk - if (sdk === 'ios') { - await initIosProject(projRoot, {}, true); - } else if (sdk === 'android') { - await initAndroidProject(projRoot, {}, true); - } else if (sdk === 'js') { - await initProjectWithProfile(projRoot, {}, true); - } else { - console.log('sdk should be "ios", "android" or "js"'); - return; - } - //add categories - if (categories.includes('auth')) { - await addAuthWithDefault(projRoot, {}, true); - if (categories.includes('storage')) { - await addStorageWithDefault(projRoot, {}, true); - } - if (categories.includes('api')) { - addApi = true; - await addApiWithSimpleModel(projRoot, {}, true); - } - } else { - console.log('The categories input are invalid'); - return; + let addApi: Boolean = false; + //check sdk + if (sdk === 'ios') { + await initIosProject(projRoot, {}, true); + } else if (sdk === 'android') { + await initAndroidProject(projRoot, {}, true); + } else if (sdk === 'js') { + await initProjectWithProfile(projRoot, {}, true); + } else { + console.log('sdk should be "ios", "android" or "js"'); + return; + } + //add categories + if (categories.includes('auth')) { + await addAuthWithDefault(projRoot, {}, true); + if (categories.includes('storage')) { + await addStorageWithDefault(projRoot, {}, true); } - //push resources - if (addApi) { - await amplifyPushApi(projRoot, true); - } else { - await amplifyPush(projRoot, true); + if (categories.includes('api')) { + addApi = true; + await addApiWithSimpleModel(projRoot, {}, true); } + } else { + console.log('The categories input are invalid'); + return; + } + //push resources + if (addApi) { + await amplifyPushApi(projRoot, true); + } else { + await amplifyPush(projRoot, true); + } - //sign up a new user - await signUpNewUser(projRoot, {username: 'test01', password: 'The#test1'}, true); + //sign up a new user + await signUpNewUser(projRoot, { username: 'test01', password: 'The#test1' }, true); } if (process.argv.length <= 3) { - console.log( - 'Usage: npm run config [list: categories], you can add auth,storage and api for your project' - ); - process.exit(1); + console.log('Usage: npm run config [list: categories], you can add auth,storage and api for your project'); + process.exit(1); } const projRoot = process.argv[2]; if (!existsSync(projRoot)) { - console.log('Project path does not exist.'); - process.exit(1); + console.log('Project path does not exist.'); + process.exit(1); } const sdk = process.argv[3]; const categories = process.argv.slice(4); try { - test(projRoot, sdk, categories); - console.log('The backend resources are set up successfully.'); + test(projRoot, sdk, categories); + console.log('The backend resources are set up successfully.'); } catch (e) { - console.log(e.stack); - process.exit(1); + console.log(e.stack); + process.exit(1); } diff --git a/packages/amplify-ui-tests/src/utils/command.ts b/packages/amplify-ui-tests/src/utils/command.ts index f8afc72424..c16973cb66 100644 --- a/packages/amplify-ui-tests/src/utils/command.ts +++ b/packages/amplify-ui-tests/src/utils/command.ts @@ -6,126 +6,109 @@ import { exec } from 'child_process'; const SEVER_LAUNCH_TIME: number = 30000; //default settings for new user sign up const defaultSettings = { - username: process.env.CYPRESS_COGNITO_SIGN_IN_USERNAME ? process.env.CYPRESS_COGNITO_SIGN_IN_USERNAME : 'test01', - password: process.env.CYPRESS_COGNITO_SIGN_IN_PASSWORD ? process.env.CYPRESS_COGNITO_SIGN_IN_PASSWORD : 'The#test1', - email: process.env.CYPRESS_COGNITO_SIGN_IN_EMAIL ? process.env.CYPRESS_COGNITO_SIGN_IN_EMAIL : 'lizeyutest01@amazon.com', - phone: process.env.CYPRESS_COGNITO_SIGN_IN_PHONE_NUMBER ? process.env.CYPRESS_COGNITO_SIGN_IN_PHONE_NUMBER : '6666666666' + username: process.env.CYPRESS_COGNITO_SIGN_IN_USERNAME ? process.env.CYPRESS_COGNITO_SIGN_IN_USERNAME : 'test01', + password: process.env.CYPRESS_COGNITO_SIGN_IN_PASSWORD ? process.env.CYPRESS_COGNITO_SIGN_IN_PASSWORD : 'The#test1', + email: process.env.CYPRESS_COGNITO_SIGN_IN_EMAIL ? process.env.CYPRESS_COGNITO_SIGN_IN_EMAIL : 'lizeyutest01@amazon.com', + phone: process.env.CYPRESS_COGNITO_SIGN_IN_PHONE_NUMBER ? process.env.CYPRESS_COGNITO_SIGN_IN_PHONE_NUMBER : '6666666666', }; -export function gitCloneSampleApp( - cwd: string, - settings: { repo: string}, - verbose: boolean = !isCI() -) { - return new Promise((resolve, reject) => { - nexpect.spawn('git', ['clone', settings.repo], {cwd, stripColors: true, verbose}) - .run(err => { - if (err) { - reject(err); - } else { - resolve(); - } - }) +export function gitCloneSampleApp(cwd: string, settings: { repo: string }, verbose: boolean = !isCI()) { + return new Promise((resolve, reject) => { + nexpect.spawn('git', ['clone', settings.repo], { cwd, stripColors: true, verbose }).run(err => { + if (err) { + reject(err); + } else { + resolve(); + } }); + }); } export function setupCypress(cwd: string) { - return new Promise((resolve, reject) => { - exec('CYPRESS_INSTALL_BINARY=0 npm install', {cwd: cwd}, function(err: Error) { - if (err) { - reject(err); - } else { - resolve(); - } - }); + return new Promise((resolve, reject) => { + exec('CYPRESS_INSTALL_BINARY=0 npm install', { cwd: cwd }, function(err: Error) { + if (err) { + reject(err); + } else { + resolve(); + } }); + }); } -export function buildApp( - cwd: string, - settings?: any, - verbose: boolean = !isCI() -) { - return new Promise((resolve, reject) => { - nexpect.spawn('yarn', {cwd, stripColors: true, verbose}) - .run(err => { - if (err) { - reject(err); - } else { - resolve(); - } - }) +export function buildApp(cwd: string, settings?: any, verbose: boolean = !isCI()) { + return new Promise((resolve, reject) => { + nexpect.spawn('yarn', { cwd, stripColors: true, verbose }).run(err => { + if (err) { + reject(err); + } else { + resolve(); + } }); + }); } -export function runCypressTest( - cwd: string, - settings?: any, - verbose: boolean = true -) { - let isPassed: boolean = true; - let options = ['cypress', 'run']; - if (!isCI()) { - options.push('--headed'); - } - return new Promise((resolve) => { - nexpect - .spawn('yarn', options, {cwd, stripColors: true, verbose}) - .run(function(err: Error, outputs: string[], exitCode: string|number) { - if (err || exitCode) { - isPassed = false; - } - resolve(isPassed); - }); - }) +export function runCypressTest(cwd: string, settings?: any, verbose: boolean = true) { + let isPassed: boolean = true; + let options = ['cypress', 'run']; + if (!isCI()) { + options.push('--headed'); + } + return new Promise(resolve => { + nexpect + .spawn('yarn', options, { cwd, stripColors: true, verbose }) + .run(function(err: Error, outputs: string[], exitCode: string | number) { + if (err || exitCode) { + isPassed = false; + } + resolve(isPassed); + }); + }); } function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => setTimeout(resolve, ms)); } -export async function startServer( - cwd: string, - settings: any, -) { - exec('PORT=' + settings.port + ' yarn start', {cwd: cwd}); - //waiting for the server to launch - await sleep(SEVER_LAUNCH_TIME); +export async function startServer(cwd: string, settings: any) { + exec('PORT=' + settings.port + ' yarn start', { cwd: cwd }); + //waiting for the server to launch + await sleep(SEVER_LAUNCH_TIME); } -export function closeServer( - settings: {port: string}, -) { - exec(`kill -9 $(lsof -t -i:${settings.port})`); +export function closeServer(settings: { port: string }) { + exec(`kill -9 $(lsof -t -i:${settings.port})`); } -export async function signUpNewUser( - cwd: string, - settings: any = {}, - verbose: boolean = !isCI() - ) { - const meta = getProjectMeta(cwd); - const {UserPoolId, AppClientIDWeb} = Object.keys(meta.auth).map(key => meta.auth[key])[0].output; - const s = {...defaultSettings, ...settings, - clientId: AppClientIDWeb, - userPoolId: UserPoolId - }; - await signUpUser(cwd, s, verbose); - comfirmSignUp(cwd, s, verbose); - return s; - } +export async function signUpNewUser(cwd: string, settings: any = {}, verbose: boolean = !isCI()) { + const meta = getProjectMeta(cwd); + const { UserPoolId, AppClientIDWeb } = Object.keys(meta.auth).map(key => meta.auth[key])[0].output; + const s = { ...defaultSettings, ...settings, clientId: AppClientIDWeb, userPoolId: UserPoolId }; + await signUpUser(cwd, s, verbose); + comfirmSignUp(cwd, s, verbose); + return s; +} -function signUpUser( - cwd: string, - settings: any, - verbose: boolean = !isCI() - ) { - return new Promise((resolve, reject) => { - nexpect - .spawn(getAwsCLIPath(), ['cognito-idp', 'sign-up', '--client-id', settings.clientId, - '--username', settings.username, '--password', - settings.password, '--user-attributes', `Name=email,Value=${settings.email}`, - `Name=phone_number,Value=+1${settings.phone}`], { cwd, stripColors: true, verbose }) +function signUpUser(cwd: string, settings: any, verbose: boolean = !isCI()) { + return new Promise((resolve, reject) => { + nexpect + .spawn( + getAwsCLIPath(), + [ + 'cognito-idp', + 'sign-up', + '--client-id', + settings.clientId, + '--username', + settings.username, + '--password', + settings.password, + '--user-attributes', + `Name=email,Value=${settings.email}`, + `Name=phone_number,Value=+1${settings.phone}`, + ], + { cwd, stripColors: true, verbose } + ) .run(function(err: Error) { if (!err) { resolve(); @@ -133,16 +116,14 @@ function signUpUser( reject(err); } }); - }) - } + }); +} -function comfirmSignUp( - cwd: string, - settings: any, - verbose: boolean = !isCI() - ) { - // Comfirm the sign-up status and get the email verified - exec(`aws cognito-idp admin-confirm-sign-up --user-pool-id ${settings.userPoolId} --username ${settings.username}`); - exec(`aws cognito-idp admin-update-user-attributes --user-pool-id ${settings.userPoolId} --username ${settings.username}` + - ` --user-attributes Name=email_verified,Value=true`); - } +function comfirmSignUp(cwd: string, settings: any, verbose: boolean = !isCI()) { + // Comfirm the sign-up status and get the email verified + exec(`aws cognito-idp admin-confirm-sign-up --user-pool-id ${settings.userPoolId} --username ${settings.username}`); + exec( + `aws cognito-idp admin-update-user-attributes --user-pool-id ${settings.userPoolId} --username ${settings.username}` + + ` --user-attributes Name=email_verified,Value=true` + ); +} diff --git a/packages/amplify-ui-tests/src/utils/index.ts b/packages/amplify-ui-tests/src/utils/index.ts index a15e7c801f..68b6e2357b 100644 --- a/packages/amplify-ui-tests/src/utils/index.ts +++ b/packages/amplify-ui-tests/src/utils/index.ts @@ -3,7 +3,7 @@ import { mkdirSync, readFileSync } from 'fs'; import * as rimraf from 'rimraf'; import { config } from 'dotenv'; import { writeFile } from 'fs'; -export { default as getProjectMeta } from './projectMeta' +export { default as getProjectMeta } from './projectMeta'; // run dotenv config to update env variable config(); @@ -18,11 +18,7 @@ export function getAwsCLIPath() { export function createNewProjectDir(root?: string): string { if (!root) { - root = join( - __dirname, - '../../../../..', - `amplify-integ-${Math.round(Math.random() * 100)}-test-${Math.round(Math.random() * 1000)}` - ); + root = join(__dirname, '../../../../..', `amplify-integ-${Math.round(Math.random() * 100)}-test-${Math.round(Math.random() * 1000)}`); } mkdirSync(root); return root; @@ -36,13 +32,13 @@ export function isCI(): Boolean { return process.env.CI ? true : false; } -export function getEnvVars(): { } { +export function getEnvVars(): {} { return { ...process.env }; } -export function getSampleRootPath() : string { +export function getSampleRootPath(): string { //return join(__dirname, '../../../../..', 'amplify-js-samples-staging') - return join(__dirname, '../../../../..', 'photo-albums') + return join(__dirname, '../../../../..', 'photo-albums'); } export function getUITestConfig() { @@ -57,15 +53,16 @@ export function createTestMetaFile(destRoot: string, settings: any) { COGNITO_SIGN_IN_USERNAME: settings.username, COGNITO_SIGN_IN_PASSWORD: settings.password, COGNITO_SIGN_IN_EMAIL: settings.email, - COGNITO_SIGN_IN_PHONE_NUMBER: settings.phone + COGNITO_SIGN_IN_PHONE_NUMBER: settings.phone, }, - testFiles: settings.testFiles + testFiles: settings.testFiles, }; if (isCI()) { - testEnv = {...testEnv, - videosFolder: "/root/videos/" + settings.name, - screenshotsFolder: "/root/screenshots/" + settings.name, - video: true + testEnv = { + ...testEnv, + videosFolder: '/root/videos/' + settings.name, + screenshotsFolder: '/root/screenshots/' + settings.name, + video: true, }; } const outputPath = join(destRoot, 'cypress.json'); @@ -75,4 +72,3 @@ export function createTestMetaFile(destRoot: string, settings: any) { } }); } - diff --git a/packages/amplify-ui-tests/src/utils/projectMeta.ts b/packages/amplify-ui-tests/src/utils/projectMeta.ts index f68c7679a6..cc5c3b450d 100644 --- a/packages/amplify-ui-tests/src/utils/projectMeta.ts +++ b/packages/amplify-ui-tests/src/utils/projectMeta.ts @@ -2,22 +2,22 @@ import { join } from 'path'; import { readFileSync, existsSync, copyFileSync } from 'fs'; const metaFilePathDic = { - js: "src/aws-exports.js", - android: "app/src/main/res/raw/awsconfiguration.json", - ios: "awsconfiguration.json" -} + js: 'src/aws-exports.js', + android: 'app/src/main/res/raw/awsconfiguration.json', + ios: 'awsconfiguration.json', +}; export default function getProjectMeta(projectRoot: string) { const metaFilePath = join(projectRoot, 'amplify', '#current-cloud-backend', 'amplify-meta.json'); return JSON.parse(readFileSync(metaFilePath, 'utf8')); } -export function existsAWSExportsPath(projectRoot: string, platform: string) : boolean { - const metaFilePath = join(projectRoot, metaFilePathDic[platform]) - return existsSync(metaFilePath) +export function existsAWSExportsPath(projectRoot: string, platform: string): boolean { + const metaFilePath = join(projectRoot, metaFilePathDic[platform]); + return existsSync(metaFilePath); } export function copyAWSExportsToProj(projectRoot: string, destRoot: string) { - const awsExporFiletPath = join(projectRoot, 'src', 'aws-exports.js') - const destFilePath = join(destRoot, 'src', 'aws-exports.js') - copyFileSync(awsExporFiletPath, destFilePath) -} \ No newline at end of file + const awsExporFiletPath = join(projectRoot, 'src', 'aws-exports.js'); + const destFilePath = join(destRoot, 'src', 'aws-exports.js'); + copyFileSync(awsExporFiletPath, destFilePath); +} diff --git a/packages/amplify-util-mock/commands/mock/api.js b/packages/amplify-util-mock/commands/mock/api.js index 1a37b75fee..77e6cf0371 100644 --- a/packages/amplify-util-mock/commands/mock/api.js +++ b/packages/amplify-util-mock/commands/mock/api.js @@ -4,14 +4,14 @@ module.exports = { run: async function(context) { if (context.parameters.options.help) { const header = `amplify mock ${this.name} \nDescription: - Mock GraphQL API locally` + Mock GraphQL API locally`; context.amplify.showHelp(header, []); return; } try { await testUtil.api.start(context); - } catch(e) { + } catch (e) { context.print.error(e.message); } - } -} \ No newline at end of file + }, +}; diff --git a/packages/amplify-util-mock/commands/mock/function.js b/packages/amplify-util-mock/commands/mock/function.js index 972c3bf6e4..7250d0115e 100644 --- a/packages/amplify-util-mock/commands/mock/function.js +++ b/packages/amplify-util-mock/commands/mock/function.js @@ -1,17 +1,17 @@ -const {invokeWalkthroughRun} = require('amplify-category-function'); +const { invokeWalkthroughRun } = require('amplify-category-function'); module.exports = { name: 'function', run: async function(context) { if (context.parameters.options.help) { const header = `amplify mock ${this.name} \nDescriptions: - Mock Functions locally` + Mock Functions locally`; context.amplify.showHelp(header, []); return; } try { await invokeWalkthroughRun(context); - } catch(e) { + } catch (e) { context.print.error(e.message); } - } -} + }, +}; diff --git a/packages/amplify-util-mock/commands/mock/mock.js b/packages/amplify-util-mock/commands/mock/mock.js index e777ddac82..e5ce96078e 100755 --- a/packages/amplify-util-mock/commands/mock/mock.js +++ b/packages/amplify-util-mock/commands/mock/mock.js @@ -8,5 +8,5 @@ module.exports = { return help.run(context); } testUtil.mockAllCategories(context); - } -} \ No newline at end of file + }, +}; diff --git a/packages/amplify-util-mock/commands/mock/storage.js b/packages/amplify-util-mock/commands/mock/storage.js index 8de47600df..2d4aba68ea 100644 --- a/packages/amplify-util-mock/commands/mock/storage.js +++ b/packages/amplify-util-mock/commands/mock/storage.js @@ -4,14 +4,14 @@ module.exports = { run: async function(context) { if (context.parameters.options.help) { const header = `amplify mock ${this.name} \nDescriptions: - Mock Storage locally` + Mock Storage locally`; context.amplify.showHelp(header, []); return; } try { await testUtil.storage.start(context); - } catch(e) { + } catch (e) { context.print.error(e.message); } - } -} \ No newline at end of file + }, +}; diff --git a/packages/amplify-util-mock/src/CFNParser/appsync-resource-processor.ts b/packages/amplify-util-mock/src/CFNParser/appsync-resource-processor.ts index 60addbc737..bc4f592d31 100644 --- a/packages/amplify-util-mock/src/CFNParser/appsync-resource-processor.ts +++ b/packages/amplify-util-mock/src/CFNParser/appsync-resource-processor.ts @@ -1,7 +1,11 @@ import { CloudFormationParseContext } from './types'; import { isPlainObject } from 'lodash'; import { parseValue } from './field-parser'; -import { AmplifyAppSyncSimulatorConfig, AmplifyAppSyncAPIConfig, AmplifyAppSyncSimulatorAuthenticationType } from 'amplify-appsync-simulator'; +import { + AmplifyAppSyncSimulatorConfig, + AmplifyAppSyncAPIConfig, + AmplifyAppSyncSimulatorAuthenticationType, +} from 'amplify-appsync-simulator'; const CFN_DEFAULT_PARAMS = { 'AWS::Region': 'us-east-1-fake', 'AWS::AccountId': '12345678910', @@ -21,12 +25,7 @@ const resourceProcessorMapping = { 'AWS::AppSync::DataSource': graphQLDataSource, 'AWS::AppSync::FunctionConfiguration': graphqlFunctionHandler, }; -export function dynamoDBResourceHandler( - resourceName, - resource, - cfnContext: CloudFormationParseContext, - transformResult: any -) { +export function dynamoDBResourceHandler(resourceName, resource, cfnContext: CloudFormationParseContext, transformResult: any) { const tableName = resourceName; const gsis = (resource.Properties.GlobalSecondaryIndexes || []).map(gsi => { const p = { ...gsi }; @@ -48,12 +47,7 @@ export function dynamoDBResourceHandler( return processedResource; } -export function graphQLDataSource( - resourceName, - resource, - cfnContext: CloudFormationParseContext, - transformResult: any -) { +export function graphQLDataSource(resourceName, resource, cfnContext: CloudFormationParseContext, transformResult: any) { const tableName = resource.Properties.Name; const typeName = resource.Properties.Type; if (typeName === 'AMAZON_DYNAMODB') { @@ -82,9 +76,7 @@ export function graphQLDataSource( } // XXX: Handle un-supported data sources - console.log( - `Data source of type ${typeName} is not supported by local mocking. A NONE data source will be used.` - ); + console.log(`Data source of type ${typeName} is not supported by local mocking. A NONE data source will be used.`); return { name: resourceName, type: 'NONE', @@ -103,36 +95,29 @@ export function graphQLAPIResourceHandler( name: cfnContext.params.AppSyncApiName || 'AppSyncTransformer', defaultAuthenticationType: { authenticationType: resource.Properties.AuthenticationType, - ...(resource.Properties.OpenIDConnectConfig ? { openIDConnectConfig : resource.Properties.OpenIDConnectConfig } : {}), - ...(resource.Properties.UserPoolConfig ? { cognitoUserPoolConfig: resource.Properties.UserPoolConfig}: {}) + ...(resource.Properties.OpenIDConnectConfig ? { openIDConnectConfig: resource.Properties.OpenIDConnectConfig } : {}), + ...(resource.Properties.UserPoolConfig ? { cognitoUserPoolConfig: resource.Properties.UserPoolConfig } : {}), }, // authenticationType: parseValue(resource.Properties.AuthenticationType, cfnContext, transformResult: any), ref: `arn:aws:appsync:us-east-1:123456789012:apis/${apiId}`, ...(resource.Properties.AdditionalAuthenticationProviders ? { - additionalAuthenticationProviders: resource.Properties.AdditionalAuthenticationProviders.map( - p => { - return { - authenticationType: p.AuthenticationType, - ...(p.OpenIDConnectConfig ? { openIDConnectConfig: p.OpenIDConnectConfig } : {}), - ...(p.CognitoUserPoolConfig ? { cognitoUserPoolConfig: p.CognitoUserPoolConfig}: {}) - }; - } - ), + additionalAuthenticationProviders: resource.Properties.AdditionalAuthenticationProviders.map(p => { + return { + authenticationType: p.AuthenticationType, + ...(p.OpenIDConnectConfig ? { openIDConnectConfig: p.OpenIDConnectConfig } : {}), + ...(p.CognitoUserPoolConfig ? { cognitoUserPoolConfig: p.CognitoUserPoolConfig } : {}), + }; + }), } : { - additionalAuthenticationProviders: [] - }), + additionalAuthenticationProviders: [], + }), }; return processedResource; } -export function graphQLAPIKeyResourceHandler( - resourceName, - resource, - cfnContext: CloudFormationParseContext, - transformResult: any -) { +export function graphQLAPIKeyResourceHandler(resourceName, resource, cfnContext: CloudFormationParseContext, transformResult: any) { const value = 'da2-fakeApiId123456'; // TODO: Generate const processedResource = { type: resource.Type, @@ -143,33 +128,23 @@ export function graphQLAPIKeyResourceHandler( return processedResource; } -export function graphQLSchemaHandler( - resourceName, - resource, - cfnContext: CloudFormationParseContext, - transformResult: any -) { +export function graphQLSchemaHandler(resourceName, resource, cfnContext: CloudFormationParseContext, transformResult: any) { return { content: transformResult.schema, path: 'schema.json', // XXX: handle schema folder }; } -export function graphQLResolverHandler( - resourceName, - resource, - cfnContext: CloudFormationParseContext, - transformResult: any -) { +export function graphQLResolverHandler(resourceName, resource, cfnContext: CloudFormationParseContext, transformResult: any) { const { Properties: properties } = resource; - const requestMappingTemplate = parseValue( - properties.RequestMappingTemplateS3Location, - cfnContext - ).replace('s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', ''); - const responseMappingTemplate = parseValue( - properties.ResponseMappingTemplateS3Location, - cfnContext - ).replace('s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', ''); + const requestMappingTemplate = parseValue(properties.RequestMappingTemplateS3Location, cfnContext).replace( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', + '' + ); + const responseMappingTemplate = parseValue(properties.ResponseMappingTemplateS3Location, cfnContext).replace( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', + '' + ); let dataSourceName; let functions; if (properties.Kind === 'PIPELINE') { @@ -214,21 +189,16 @@ function getAppSyncFunctionName(functionConfig) { return functionConfig; } -export function graphqlFunctionHandler( - resourceName, - resource, - cfnContext: CloudFormationParseContext, - transformResult: any -) { +export function graphqlFunctionHandler(resourceName, resource, cfnContext: CloudFormationParseContext, transformResult: any) { const { Properties: properties } = resource; - const requestMappingTemplate = parseValue( - properties.RequestMappingTemplateS3Location, - cfnContext - ).replace('s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', ''); - const responseMappingTemplate = parseValue( - properties.ResponseMappingTemplateS3Location, - cfnContext - ).replace('s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', ''); + const requestMappingTemplate = parseValue(properties.RequestMappingTemplateS3Location, cfnContext).replace( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', + '' + ); + const responseMappingTemplate = parseValue(properties.ResponseMappingTemplateS3Location, cfnContext).replace( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/', + '' + ); const dataSourceName = getDataSourceName(properties.DataSourceName, cfnContext.resources); @@ -240,11 +210,7 @@ export function graphqlFunctionHandler( }; } -export function processResources( - resources, - transformResult: any, - params = {} -): AmplifyAppSyncSimulatorConfig { +export function processResources(resources, transformResult: any, params = {}): AmplifyAppSyncSimulatorConfig { const cfnContext: CloudFormationParseContext = { conditions: { ...CFN_DEFAULT_CONDITIONS, @@ -261,7 +227,7 @@ export function processResources( }; const processedResources: AmplifyAppSyncSimulatorConfig = { schema: { - content: '' + content: '', }, resolvers: [], functions: [], @@ -271,22 +237,17 @@ export function processResources( appSync: { name: '', defaultAuthenticationType: { - authenticationType: AmplifyAppSyncSimulatorAuthenticationType.API_KEY + authenticationType: AmplifyAppSyncSimulatorAuthenticationType.API_KEY, }, apiKey: null, - additionalAuthenticationProviders: [] + additionalAuthenticationProviders: [], }, }; Object.entries(resources).forEach(entry => { const [resourceName, resource] = entry; const { Type: resourceType } = resource as any; if (Object.keys(resourceProcessorMapping).includes(resourceType)) { - const result = resourceProcessorMapping[resourceType]( - resourceName, - resource, - cfnContext, - transformResult - ); + const result = resourceProcessorMapping[resourceType](resourceName, resource, cfnContext, transformResult); cfnContext.resources[resourceName] = result; switch (resourceType) { diff --git a/packages/amplify-util-mock/src/CFNParser/field-parser.ts b/packages/amplify-util-mock/src/CFNParser/field-parser.ts index f79eca2fd7..79206755eb 100644 --- a/packages/amplify-util-mock/src/CFNParser/field-parser.ts +++ b/packages/amplify-util-mock/src/CFNParser/field-parser.ts @@ -4,21 +4,14 @@ import { CloudFormationParseContext } from './types'; const intrinsicFunctionMap = {}; -export function addIntrinsicFunction( - keyword: string, - func: (node, cfnContext: CloudFormationParseContext, parse: Function) => void -) { +export function addIntrinsicFunction(keyword: string, func: (node, cfnContext: CloudFormationParseContext, parse: Function) => void) { intrinsicFunctionMap[keyword] = func; } export function parseValue(node, context: CloudFormationParseContext) { if (typeof node === 'string') return node; - if ( - isPlainObject(node) && - Object.keys(node).length === 1 && - Object.keys(intrinsicFunctionMap).includes(Object.keys(node)[0]) - ) { + if (isPlainObject(node) && Object.keys(node).length === 1 && Object.keys(intrinsicFunctionMap).includes(Object.keys(node)[0])) { const op = Object.keys(node)[0]; const valNode = node[op]; return intrinsicFunctionMap[op](valNode, context, parseValue); diff --git a/packages/amplify-util-mock/src/CFNParser/intrinsic-functions.ts b/packages/amplify-util-mock/src/CFNParser/intrinsic-functions.ts index ae0e523474..e065e779e9 100644 --- a/packages/amplify-util-mock/src/CFNParser/intrinsic-functions.ts +++ b/packages/amplify-util-mock/src/CFNParser/intrinsic-functions.ts @@ -2,45 +2,27 @@ import { CloudFormationParseContext } from './types'; import { isPlainObject, isInteger } from 'lodash'; import { isString, isArray, isObject } from 'util'; -export function cfnJoin( - valNode: [string, string[]], - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnJoin(valNode: [string, string[]], { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!(Array.isArray(valNode) && valNode.length === 2 && Array.isArray(valNode[1]))) { - throw new Error( - `FN::Join expects an array with 2 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::Join expects an array with 2 elements instead got ${JSON.stringify(valNode)}`); } const delimiter = valNode[0]; - const items = valNode[1].map(item => - processValue(item, { params, conditions, resources, exports }) - ); + const items = valNode[1].map(item => processValue(item, { params, conditions, resources, exports })); return items.join(delimiter); } -export function cfnSub( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnSub(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && valNode.length !== 2) { - throw new Error( - `FN::Sub expects an array with 2 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::Sub expects an array with 2 elements instead got ${JSON.stringify(valNode)}`); } const strTemplate = valNode[0]; const subs = valNode[1]; if (!isString(strTemplate)) { - throw new Error( - `FN::Sub expects template to be an a string instead got ${JSON.stringify(strTemplate)}` - ); + throw new Error(`FN::Sub expects template to be an a string instead got ${JSON.stringify(strTemplate)}`); } if (!isPlainObject(subs)) { - throw new Error( - `FN::Sub expects substitution to be an object instead got ${JSON.stringify(subs)}` - ); + throw new Error(`FN::Sub expects substitution to be an object instead got ${JSON.stringify(subs)}`); } const subValues = {}; Object.entries(subs).forEach(([key, value]) => { @@ -59,15 +41,9 @@ export function cfnSub( return result; } -export function cfnGetAtt( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnGetAtt(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && valNode.length !== 2) { - throw new Error( - `FN::GetAtt expects an array with 2 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::GetAtt expects an array with 2 elements instead got ${JSON.stringify(valNode)}`); } const resourceName = valNode[0]; if (!Object.keys(resources).includes(resourceName)) { @@ -81,15 +57,9 @@ export function cfnGetAtt( return selectedResource[attributeName]; } -export function cfnSplit( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnSplit(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && valNode.length !== 2) { - throw new Error( - `FN::Split expects an array with 2 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::Split expects an array with 2 elements instead got ${JSON.stringify(valNode)}`); } const delim: string = valNode[0]; const str: string = processValue(valNode[1], { @@ -101,20 +71,14 @@ export function cfnSplit( return str.split(delim); } -export function cfnRef( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnRef(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { let key; if (isString(valNode)) { key = valNode; } else if (isArray(valNode) && valNode.length === 1) { key = processValue(valNode[1]); } else { - throw new Error( - `Ref expects a string or an array with 1 item. Instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`Ref expects a string or an array with 1 item. Instead got ${JSON.stringify(valNode)}`); } if (Object.keys(params).includes(key)) { @@ -130,45 +94,25 @@ export function cfnRef( throw new Error(`Could not find ref for ${JSON.stringify(valNode)}`); } -export function cfnSelect( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnSelect(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && valNode.length !== 2) { - throw new Error( - `FN::Select expects an array with 2 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::Select expects an array with 2 elements instead got ${JSON.stringify(valNode)}`); } const index = parseInt(valNode[0], 10); if (!Array.isArray(valNode[1])) { - throw new Error( - `FN::Select expects list item to be an array instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::Select expects list item to be an array instead got ${JSON.stringify(valNode)}`); } if (index >= valNode[1].length) { - throw new Error( - `FN::Select expects index tp be less than or equal to size of listOfObject ${JSON.stringify( - valNode - )}` - ); + throw new Error(`FN::Select expects index tp be less than or equal to size of listOfObject ${JSON.stringify(valNode)}`); } - const map = valNode[1].map(item => - processValue(item, { params, conditions, resources, exports }) - ); + const map = valNode[1].map(item => processValue(item, { params, conditions, resources, exports })); return map[index]; } -export function cfnIf( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnIf(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && valNode.length !== 3) { - throw new Error( - `FN::If expects an array with 3 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::If expects an array with 3 elements instead got ${JSON.stringify(valNode)}`); } const condition = conditions[valNode[0]]; if (condition) { @@ -177,15 +121,9 @@ export function cfnIf( return processValue(valNode[2], { params, condition, resources, exports }); } -export function cfnEquals( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnEquals(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && valNode.length !== 2) { - throw new Error( - `FN::Equal expects an array with 2 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::Equal expects an array with 2 elements instead got ${JSON.stringify(valNode)}`); } const lhs = processValue(valNode[0], { params, @@ -202,58 +140,30 @@ export function cfnEquals( return lhs == rhs; } -export function cfnNot( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnNot(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && valNode.length !== 1) { - throw new Error( - `FN::Not expects an array with 1 element instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::Not expects an array with 1 element instead got ${JSON.stringify(valNode)}`); } return !processValue(valNode[0], { params, conditions, resources, exports }); } -export function cfnAnd( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnAnd(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && !(valNode.length >= 2 && valNode.length <= 10)) { - throw new Error( - `FN::And expects an array with 2-10 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::And expects an array with 2-10 elements instead got ${JSON.stringify(valNode)}`); } - return valNode - .map(val => processValue(val, { params, conditions, resources, exports })) - .every(val => !!val); + return valNode.map(val => processValue(val, { params, conditions, resources, exports })).every(val => !!val); } -export function cfnOr( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnOr(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!Array.isArray(valNode) && !(valNode.length >= 2 && valNode.length <= 10)) { - throw new Error( - `FN::And expects an array with 2-10 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::And expects an array with 2-10 elements instead got ${JSON.stringify(valNode)}`); } - return valNode - .map(val => processValue(val, { params, conditions, resources, exports })) - .some(val => !!val); + return valNode.map(val => processValue(val, { params, conditions, resources, exports })).some(val => !!val); } -export function cfnImportValue( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnImportValue(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!(isPlainObject(valNode) || isString(valNode))) { - throw new Error( - `FN::ImportValue expects an array with 1 elements instead got ${JSON.stringify(valNode)}` - ); + throw new Error(`FN::ImportValue expects an array with 1 elements instead got ${JSON.stringify(valNode)}`); } const key = processValue(valNode, { params, conditions, resources, exports }); if (!Object.keys(exports).includes(key)) { @@ -262,11 +172,7 @@ export function cfnImportValue( return exports[key]; } -export function cfnCondition( - valNode, - { params, conditions, resources, exports }: CloudFormationParseContext, - processValue -) { +export function cfnCondition(valNode, { params, conditions, resources, exports }: CloudFormationParseContext, processValue) { if (!isString(valNode)) { throw new Error(`Condition should be a string value, instead got ${JSON.stringify(valNode)}`); } diff --git a/packages/amplify-util-mock/src/CFNParser/lambda-resource-processor.ts b/packages/amplify-util-mock/src/CFNParser/lambda-resource-processor.ts index eddf0f0017..811a152b73 100644 --- a/packages/amplify-util-mock/src/CFNParser/lambda-resource-processor.ts +++ b/packages/amplify-util-mock/src/CFNParser/lambda-resource-processor.ts @@ -19,11 +19,7 @@ const CFN_DEFAULT_CONDITIONS = { ShouldNotCreateEnvResources: true, }; -export function lambdaFunctionHandler( - resourceName, - resource, - cfnContext: CloudFormationParseContext -): LambdaFunctionConfig { +export function lambdaFunctionHandler(resourceName, resource, cfnContext: CloudFormationParseContext): LambdaFunctionConfig { const name: string = parseValue(resource.Properties.FunctionName, cfnContext); const handler = parseValue(resource.Properties.Handler, cfnContext); const environment = @@ -43,14 +39,8 @@ export function lambdaFunctionHandler( }; } -export function processResources( - resources: { [key: string]: any }, - transformResult: any, - params = {} -): LambdaFunctionConfig | undefined { - const definition = Object.entries(resources).find( - (entry: [string, any]) => entry[1].Type === 'AWS::Lambda::Function' - ); +export function processResources(resources: { [key: string]: any }, transformResult: any, params = {}): LambdaFunctionConfig | undefined { + const definition = Object.entries(resources).find((entry: [string, any]) => entry[1].Type === 'AWS::Lambda::Function'); if (definition) { return lambdaFunctionHandler(definition[0], definition[1], { conditions: CFN_DEFAULT_CONDITIONS, diff --git a/packages/amplify-util-mock/src/__e2e__/connections-with-auth-tests.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/connections-with-auth-tests.e2e.test.ts index 090b2726bd..c55a92825f 100644 --- a/packages/amplify-util-mock/src/__e2e__/connections-with-auth-tests.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/connections-with-auth-tests.e2e.test.ts @@ -129,9 +129,7 @@ type ConnectionProtected @model( Authorization: idToken, }); - const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [ - DEVS_GROUP_NAME, - ]); + const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME]); GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2, }); diff --git a/packages/amplify-util-mock/src/__e2e__/dynamo-db-model-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/dynamo-db-model-transformer.e2e.test.ts index 1ed7e99fd5..661351e302 100644 --- a/packages/amplify-util-mock/src/__e2e__/dynamo-db-model-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/dynamo-db-model-transformer.e2e.test.ts @@ -48,13 +48,17 @@ beforeAll(async () => { `; try { const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer(), new ModelAuthTransformer({ - authConfig: { + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { defaultAuthentication: { - authenticationType: "API_KEY" + authenticationType: 'API_KEY', }, - additionalAuthenticationProviders: [] - }})], + additionalAuthenticationProviders: [], + }, + }), + ], }); const out = await transformer.transform(validSchema); let ddbClient; @@ -521,11 +525,7 @@ test('Test enum filters List', async () => { const appearsInNonJediItems = appearsInWithFilterResponseNonJedi.data.listPosts.items; expect(appearsInNonJediItems.length).toEqual(3); appearsInNonJediItems.forEach(item => { - expect( - ['Appears in Empire & JEDI', 'Appears in New Hope', 'Appears in Empire'].includes( - item.title - ) - ).toBeTruthy(); + expect(['Appears in Empire & JEDI', 'Appears in New Hope', 'Appears in Empire'].includes(item.title)).toBeTruthy(); }); const appearsInContainingJedi = await GRAPHQL_CLIENT.query( @@ -600,11 +600,7 @@ test('Test enum filters List', async () => { const nonJediEpisodeItems = nonJediEpisode.data.listPosts.items; expect(nonJediEpisodeItems.length).toEqual(3); nonJediEpisodeItems.forEach(item => { - expect( - ['Appears in New Hope', 'Appears in Empire', 'Appears in Empire & JEDI'].includes( - item.title - ) - ).toBeTruthy(); + expect(['Appears in New Hope', 'Appears in Empire', 'Appears in Empire & JEDI'].includes(item.title)).toBeTruthy(); }); } catch (e) { logDebug(e); diff --git a/packages/amplify-util-mock/src/__e2e__/key-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/key-transformer.e2e.test.ts index c69b20ac1c..d46b450dd1 100644 --- a/packages/amplify-util-mock/src/__e2e__/key-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/key-transformer.e2e.test.ts @@ -149,10 +149,7 @@ test('Test listX with three part primary key.', async () => { }); expect(items.data.listItems.items).toHaveLength(1); items = await listItem(hashKey, { - between: [ - { status: 'PENDING', createdAt: '2018-08-01' }, - { status: 'PENDING', createdAt: '2018-10-01' }, - ], + between: [{ status: 'PENDING', createdAt: '2018-08-01' }, { status: 'PENDING', createdAt: '2018-10-01' }], }); expect(items.data.listItems.items).toHaveLength(1); items = await listItem(hashKey, { @@ -291,22 +288,14 @@ test('Test update mutation validation with three part secondary key.', async () }); test('Test Customer Create with list member and secondary key', async () => { - const createCustomer1 = await createCustomer( - 'customer1@email.com', - ['thing1', 'thing2'], - 'customerusr1' - ); + const createCustomer1 = await createCustomer('customer1@email.com', ['thing1', 'thing2'], 'customerusr1'); const getCustomer1 = await getCustomer('customer1@email.com'); expect(getCustomer1.data.getCustomer.addresslist).toEqual(['thing1', 'thing2']); // const items = await onCreateCustomer }); test('Test Customer Mutation with list member', async () => { - const updateCustomer1 = await updateCustomer( - 'customer1@email.com', - ['thing3', 'thing4'], - 'new_customerusr1' - ); + const updateCustomer1 = await updateCustomer('customer1@email.com', ['thing3', 'thing4'], 'new_customerusr1'); const getCustomer1 = await getCustomer('customer1@email.com'); expect(getCustomer1.data.getCustomer.addresslist).toEqual(['thing3', 'thing4']); }); @@ -428,12 +417,7 @@ async function getOrder(customerEmail: string, createdAt: string) { return result; } -async function createItem( - orderId: string, - status: string, - name: string, - createdAt: string = new Date().toISOString() -) { +async function createItem(orderId: string, status: string, name: string, createdAt: string = new Date().toISOString()) { const input = { status, orderId, name, createdAt }; const result = await GRAPHQL_CLIENT.query( `mutation CreateItem($input: CreateItemInput!) { @@ -532,12 +516,7 @@ interface ItemCompositeKeyInput { status?: string; createdAt?: string; } -async function listItem( - orderId?: string, - statusCreatedAt?: ItemCompositeKeyConditionInput, - limit?: number, - nextToken?: string -) { +async function listItem(orderId?: string, statusCreatedAt?: ItemCompositeKeyConditionInput, limit?: number, nextToken?: string) { const result = await GRAPHQL_CLIENT.query( `query ListItems($orderId: ID, $statusCreatedAt: ModelItemPrimaryCompositeKeyConditionInput, $limit: Int, $nextToken: String) { listItems(orderId: $orderId, statusCreatedAt: $statusCreatedAt, limit: $limit, nextToken: $nextToken) { @@ -556,12 +535,7 @@ async function listItem( return result; } -async function itemsByStatus( - status: string, - createdAt?: StringKeyConditionInput, - limit?: number, - nextToken?: string -) { +async function itemsByStatus(status: string, createdAt?: StringKeyConditionInput, limit?: number, nextToken?: string) { const result = await GRAPHQL_CLIENT.query( `query ListByStatus($status: Status!, $createdAt: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) { itemsByStatus(status: $status, createdAt: $createdAt, limit: $limit, nextToken: $nextToken) { @@ -580,12 +554,7 @@ async function itemsByStatus( return result; } -async function itemsByCreatedAt( - createdAt: string, - status?: StringKeyConditionInput, - limit?: number, - nextToken?: string -) { +async function itemsByCreatedAt(createdAt: string, status?: StringKeyConditionInput, limit?: number, nextToken?: string) { const result = await GRAPHQL_CLIENT.query( `query ListByCreatedAt($createdAt: AWSDateTime!, $status: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) { itemsByCreatedAt(createdAt: $createdAt, status: $status, limit: $limit, nextToken: $nextToken) { @@ -604,12 +573,7 @@ async function itemsByCreatedAt( return result; } -async function createShippingUpdate( - orderId: string, - itemId: string, - status: string, - name?: string -) { +async function createShippingUpdate(orderId: string, itemId: string, status: string, name?: string) { const input = { status, orderId, itemId, name }; const result = await GRAPHQL_CLIENT.query( `mutation CreateShippingUpdate($input: CreateShippingUpdateInput!) { diff --git a/packages/amplify-util-mock/src/__e2e__/key-with-auth.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/key-with-auth.e2e.test.ts index d1c79ca1b7..b0a0fae709 100644 --- a/packages/amplify-util-mock/src/__e2e__/key-with-auth.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/key-with-auth.e2e.test.ts @@ -94,9 +94,7 @@ beforeAll(async () => { Authorization: idToken, }); - const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [ - DEVS_GROUP_NAME, - ]); + const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME]); GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2, }); @@ -259,11 +257,7 @@ async function getOrder(client: GraphQLClient, customerEmail: string, orderId: s return result; } -async function listOrders( - client: GraphQLClient, - customerEmail: string, - orderId: { beginsWith: string } -) { +async function listOrders(client: GraphQLClient, customerEmail: string, orderId: { beginsWith: string }) { const result = await client.query( `query ListOrder($customerEmail: String, $orderId: ModelStringKeyConditionInput) { listOrders(customerEmail: $customerEmail, orderId: $orderId) { diff --git a/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts index 11a1522ed6..3f470824a7 100644 --- a/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/model-auth-transformer.e2e.test.ts @@ -177,9 +177,7 @@ beforeAll(async () => { Authorization: accessToken, }); - const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [ - DEVS_GROUP_NAME, - ]); + const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME]); GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2, }); @@ -378,9 +376,7 @@ test('Test updatePost mutation when authorized', async () => { ); expect(updateResponse.data.updatePost.id).toEqual(response.data.createPost.id); expect(updateResponse.data.updatePost.title).toEqual('Bye, World!'); - expect(updateResponse.data.updatePost.updatedAt > response.data.createPost.updatedAt).toEqual( - true - ); + expect(updateResponse.data.updatePost.updatedAt > response.data.createPost.updatedAt).toEqual(true); const updateResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query( `mutation { @@ -396,9 +392,7 @@ test('Test updatePost mutation when authorized', async () => { ); expect(updateResponseAccess.data.updatePost.id).toEqual(response.data.createPost.id); expect(updateResponseAccess.data.updatePost.title).toEqual('Bye, World!'); - expect( - updateResponseAccess.data.updatePost.updatedAt > response.data.createPost.updatedAt - ).toEqual(true); + expect(updateResponseAccess.data.updatePost.updatedAt > response.data.createPost.updatedAt).toEqual(true); }); test('Test updatePost mutation when not authorized', async () => { @@ -434,9 +428,7 @@ test('Test updatePost mutation when not authorized', async () => { expect(updateResponse.data.updatePost).toEqual(null); expect(updateResponse.errors.length).toEqual(1); logDebug(updateResponse); - expect((updateResponse.errors[0] as any).errorType).toEqual( - 'DynamoDB:ConditionalCheckFailedException' - ); + expect((updateResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); }); test('Test deletePost mutation when authorized', async () => { @@ -523,9 +515,7 @@ test('Test deletePost mutation when not authorized', async () => { ); expect(deleteResponse.data.deletePost).toEqual(null); expect(deleteResponse.errors.length).toEqual(1); - expect((deleteResponse.errors[0] as any).errorType).toEqual( - 'DynamoDB:ConditionalCheckFailedException' - ); + expect((deleteResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); }); test('Test listPosts query when authorized', async () => { @@ -1439,9 +1429,7 @@ test(`Test listAllThrees as admin.`, async () => { `); logDebug(JSON.stringify(fetchOwnedBy2AsAdmin, null, 4)); expect(fetchOwnedBy2AsAdmin.data.listAllThrees.items).toHaveLength(1); - expect(fetchOwnedBy2AsAdmin.data.listAllThrees.items[0].id).toEqual( - ownedBy2.data.createAllThree.id - ); + expect(fetchOwnedBy2AsAdmin.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id); const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { @@ -1486,9 +1474,7 @@ test(`Test listAllThrees as owner.`, async () => { `); logDebug(JSON.stringify(fetchOwnedBy2AsOwner, null, 4)); expect(fetchOwnedBy2AsOwner.data.listAllThrees.items).toHaveLength(1); - expect(fetchOwnedBy2AsOwner.data.listAllThrees.items[0].id).toEqual( - ownedBy2.data.createAllThree.id - ); + expect(fetchOwnedBy2AsOwner.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id); const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { @@ -1533,9 +1519,7 @@ test(`Test listAllThrees as one of a set of editors.`, async () => { `); logDebug(JSON.stringify(fetchOwnedBy2AsEditor, null, 4)); expect(fetchOwnedBy2AsEditor.data.listAllThrees.items).toHaveLength(1); - expect(fetchOwnedBy2AsEditor.data.listAllThrees.items[0].id).toEqual( - ownedBy2.data.createAllThree.id - ); + expect(fetchOwnedBy2AsEditor.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id); const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { @@ -1580,9 +1564,7 @@ test(`Test listAllThrees as a member of a dynamic group.`, async () => { `); logDebug(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)); expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items).toHaveLength(1); - expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual( - ownedByAdmins.data.createAllThree.id - ); + expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual(ownedByAdmins.data.createAllThree.id); const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` query { @@ -1643,9 +1625,7 @@ test(`Test getAllThree as a member of the alternative group.`, async () => { `); logDebug(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)); expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items).toHaveLength(1); - expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual( - ownedByAdmins.data.createAllThree.id - ); + expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual(ownedByAdmins.data.createAllThree.id); const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` query { @@ -1874,9 +1854,7 @@ test(`Test createAllThree as one of a set of editors.`, async () => { } `); logDebug(JSON.stringify(deleteReq2, null, 4)); - expect(deleteReq2.data.deleteAllThree.id).toEqual( - ownedBy2WithDefaultOwner.data.createAllThree.id - ); + expect(deleteReq2.data.deleteAllThree.id).toEqual(ownedBy2WithDefaultOwner.data.createAllThree.id); }); test(`Test createAllThree as a member of a dynamic group.`, async () => { @@ -2295,9 +2273,7 @@ test(`Test updateAllThree and deleteAllThree as a member of the alternative grou `); logDebug(JSON.stringify(ownedByAdminsUnauthed, null, 4)); expect(ownedByAdminsUnauthed.errors.length).toEqual(1); - expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual( - 'DynamoDB:ConditionalCheckFailedException' - ); + expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); const ownedByDevs2 = await GRAPHQL_CLIENT_1.query(` mutation { @@ -2403,9 +2379,7 @@ test(`Test createTestIdentity as admin.`, async () => { }`, {} ); - const relevantPost = listResponse.data.listTestIdentitys.items.find( - p => p.id === getReq.data.getTestIdentity.id - ); + const relevantPost = listResponse.data.listTestIdentitys.items.find(p => p.id === getReq.data.getTestIdentity.id); logDebug(JSON.stringify(listResponse, null, 4)); expect(relevantPost).toBeTruthy(); expect(relevantPost.title).toEqual('Test title update'); diff --git a/packages/amplify-util-mock/src/__e2e__/model-connection-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/model-connection-transformer.e2e.test.ts index c2a8a42193..36fadf316e 100644 --- a/packages/amplify-util-mock/src/__e2e__/model-connection-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/model-connection-transformer.e2e.test.ts @@ -114,12 +114,8 @@ test('Test queryPost query', async () => { ); expect(createCommentResponse.data.createComment.id).toBeDefined(); expect(createCommentResponse.data.createComment.content).toEqual('A comment!'); - expect(createCommentResponse.data.createComment.post.id).toEqual( - createResponse.data.createPost.id - ); - expect(createCommentResponse.data.createComment.post.title).toEqual( - createResponse.data.createPost.title - ); + expect(createCommentResponse.data.createComment.post.id).toEqual(createResponse.data.createPost.id); + expect(createCommentResponse.data.createComment.post.title).toEqual(createResponse.data.createPost.title); const queryResponse = await GRAPHQL_CLIENT.query( `query { getPost(id: "${createResponse.data.createPost.id}") { @@ -181,12 +177,8 @@ test('Test queryPost query with sortField', async () => { ); expect(createCommentResponse1.data.createSortedComment.id).toBeDefined(); expect(createCommentResponse1.data.createSortedComment.content).toEqual(comment1); - expect(createCommentResponse1.data.createSortedComment.post.id).toEqual( - createResponse.data.createPost.id - ); - expect(createCommentResponse1.data.createSortedComment.post.title).toEqual( - createResponse.data.createPost.title - ); + expect(createCommentResponse1.data.createSortedComment.post.id).toEqual(createResponse.data.createPost.id); + expect(createCommentResponse1.data.createSortedComment.post.title).toEqual(createResponse.data.createPost.title); // create 2nd comment, 1 second later const createCommentResponse2 = await GRAPHQL_CLIENT.query( @@ -208,12 +200,8 @@ test('Test queryPost query with sortField', async () => { ); expect(createCommentResponse2.data.createSortedComment.id).toBeDefined(); expect(createCommentResponse2.data.createSortedComment.content).toEqual(comment2); - expect(createCommentResponse2.data.createSortedComment.post.id).toEqual( - createResponse.data.createPost.id - ); - expect(createCommentResponse2.data.createSortedComment.post.title).toEqual( - createResponse.data.createPost.title - ); + expect(createCommentResponse2.data.createSortedComment.post.id).toEqual(createResponse.data.createPost.id); + expect(createCommentResponse2.data.createSortedComment.post.title).toEqual(createResponse.data.createPost.title); const queryResponse = await GRAPHQL_CLIENT.query( `query { @@ -278,9 +266,7 @@ test('Test queryPost query with sortField', async () => { expect(queryResponseWithKeyCondition.data.getPost).toBeDefined(); const itemsDescWithKeyCondition = queryResponseWithKeyCondition.data.getPost.sortedComments.items; expect(itemsDescWithKeyCondition.length).toEqual(1); - expect(itemsDescWithKeyCondition[0].id).toEqual( - createCommentResponse1.data.createSortedComment.id - ); + expect(itemsDescWithKeyCondition[0].id).toEqual(createCommentResponse1.data.createSortedComment.id); const queryResponseWithKeyConditionEq = await GRAPHQL_CLIENT.query( `query { @@ -299,12 +285,9 @@ test('Test queryPost query with sortField', async () => { {} ); expect(queryResponseWithKeyConditionEq.data.getPost).toBeDefined(); - const itemsDescWithKeyConditionEq = - queryResponseWithKeyConditionEq.data.getPost.sortedComments.items; + const itemsDescWithKeyConditionEq = queryResponseWithKeyConditionEq.data.getPost.sortedComments.items; expect(itemsDescWithKeyConditionEq.length).toEqual(1); - expect(itemsDescWithKeyConditionEq[0].id).toEqual( - createCommentResponse1.data.createSortedComment.id - ); + expect(itemsDescWithKeyConditionEq[0].id).toEqual(createCommentResponse1.data.createSortedComment.id); const queryResponseWithKeyConditionGt = await GRAPHQL_CLIENT.query( `query { @@ -323,12 +306,9 @@ test('Test queryPost query with sortField', async () => { {} ); expect(queryResponseWithKeyConditionGt.data.getPost).toBeDefined(); - const itemsDescWithKeyConditionGt = - queryResponseWithKeyConditionGt.data.getPost.sortedComments.items; + const itemsDescWithKeyConditionGt = queryResponseWithKeyConditionGt.data.getPost.sortedComments.items; expect(itemsDescWithKeyConditionGt.length).toEqual(1); - expect(itemsDescWithKeyConditionGt[0].id).toEqual( - createCommentResponse2.data.createSortedComment.id - ); + expect(itemsDescWithKeyConditionGt[0].id).toEqual(createCommentResponse2.data.createSortedComment.id); const queryResponseWithKeyConditionGe = await GRAPHQL_CLIENT.query( `query { @@ -347,15 +327,10 @@ test('Test queryPost query with sortField', async () => { {} ); expect(queryResponseWithKeyConditionGe.data.getPost).toBeDefined(); - const itemsDescWithKeyConditionGe = - queryResponseWithKeyConditionGe.data.getPost.sortedComments.items; + const itemsDescWithKeyConditionGe = queryResponseWithKeyConditionGe.data.getPost.sortedComments.items; expect(itemsDescWithKeyConditionGe.length).toEqual(2); - expect(itemsDescWithKeyConditionGe[0].id).toEqual( - createCommentResponse1.data.createSortedComment.id - ); - expect(itemsDescWithKeyConditionGe[1].id).toEqual( - createCommentResponse2.data.createSortedComment.id - ); + expect(itemsDescWithKeyConditionGe[0].id).toEqual(createCommentResponse1.data.createSortedComment.id); + expect(itemsDescWithKeyConditionGe[1].id).toEqual(createCommentResponse2.data.createSortedComment.id); const queryResponseWithKeyConditionLe = await GRAPHQL_CLIENT.query( `query { @@ -374,15 +349,10 @@ test('Test queryPost query with sortField', async () => { {} ); expect(queryResponseWithKeyConditionLe.data.getPost).toBeDefined(); - const itemsDescWithKeyConditionLe = - queryResponseWithKeyConditionLe.data.getPost.sortedComments.items; + const itemsDescWithKeyConditionLe = queryResponseWithKeyConditionLe.data.getPost.sortedComments.items; expect(itemsDescWithKeyConditionLe.length).toEqual(2); - expect(itemsDescWithKeyConditionLe[0].id).toEqual( - createCommentResponse1.data.createSortedComment.id - ); - expect(itemsDescWithKeyConditionLe[1].id).toEqual( - createCommentResponse2.data.createSortedComment.id - ); + expect(itemsDescWithKeyConditionLe[0].id).toEqual(createCommentResponse1.data.createSortedComment.id); + expect(itemsDescWithKeyConditionLe[1].id).toEqual(createCommentResponse2.data.createSortedComment.id); const queryResponseWithKeyConditionLt = await GRAPHQL_CLIENT.query( `query { @@ -401,12 +371,9 @@ test('Test queryPost query with sortField', async () => { {} ); expect(queryResponseWithKeyConditionLt.data.getPost).toBeDefined(); - const itemsDescWithKeyConditionLt = - queryResponseWithKeyConditionLt.data.getPost.sortedComments.items; + const itemsDescWithKeyConditionLt = queryResponseWithKeyConditionLt.data.getPost.sortedComments.items; expect(itemsDescWithKeyConditionLt.length).toEqual(1); - expect(itemsDescWithKeyConditionLt[0].id).toEqual( - createCommentResponse1.data.createSortedComment.id - ); + expect(itemsDescWithKeyConditionLt[0].id).toEqual(createCommentResponse1.data.createSortedComment.id); const queryResponseWithKeyConditionBetween = await GRAPHQL_CLIENT.query( `query { @@ -425,12 +392,9 @@ test('Test queryPost query with sortField', async () => { {} ); expect(queryResponseWithKeyConditionBetween.data.getPost).toBeDefined(); - const itemsDescWithKeyConditionBetween = - queryResponseWithKeyConditionBetween.data.getPost.sortedComments.items; + const itemsDescWithKeyConditionBetween = queryResponseWithKeyConditionBetween.data.getPost.sortedComments.items; expect(itemsDescWithKeyConditionBetween.length).toEqual(1); - expect(itemsDescWithKeyConditionBetween[0].id).toEqual( - createCommentResponse2.data.createSortedComment.id - ); + expect(itemsDescWithKeyConditionBetween[0].id).toEqual(createCommentResponse2.data.createSortedComment.id); }); test('Test create comment without a post and then querying the comment.', async () => { @@ -476,8 +440,8 @@ test('Test create comment without a post and then querying the comment.', async test('Test default limit is 50', async () => { // create Auth logic around this - const postID = 'e2eConnectionPost' - const postTitle = 'samplePost' + const postID = 'e2eConnectionPost'; + const postTitle = 'samplePost'; const createPost = await GRAPHQL_CLIENT.query( `mutation CreatePost { createPost(input: {title: "${postTitle}", id: "${postID}"}) { @@ -485,13 +449,16 @@ test('Test default limit is 50', async () => { title } } - `, {}); - expect(createPost.data.createPost).toBeDefined() - expect(createPost.data.createPost.id).toEqual(postID) - expect(createPost.data.createPost.title).toEqual(postTitle) + `, + {} + ); + expect(createPost.data.createPost).toBeDefined(); + expect(createPost.data.createPost.id).toEqual(postID); + expect(createPost.data.createPost.title).toEqual(postTitle); - for (let i = 0; i < 51; i++) { - await GRAPHQL_CLIENT.query(` + for (let i = 0; i < 51; i++) { + await GRAPHQL_CLIENT.query( + ` mutation CreateComment { createComment(input: {postId: "${postID}", content: "content_${i}"}) { content @@ -501,10 +468,13 @@ test('Test default limit is 50', async () => { } } } - `, {}) - } + `, + {} + ); + } - const getPost = await GRAPHQL_CLIENT.query(` + const getPost = await GRAPHQL_CLIENT.query( + ` query GetPost($id: ID!) { getPost(id: $id) { id @@ -519,7 +489,9 @@ test('Test default limit is 50', async () => { nextToken } } - }`, {id: postID}) + }`, + { id: postID } + ); - expect(getPost.data.getPost.comments.items.length).toEqual(50) -}); \ No newline at end of file + expect(getPost.data.getPost.comments.items.length).toEqual(50); +}); diff --git a/packages/amplify-util-mock/src/__e2e__/model-connection-with-key-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/model-connection-with-key-transformer.e2e.test.ts index 2273232af6..85ac481b4d 100644 --- a/packages/amplify-util-mock/src/__e2e__/model-connection-with-key-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/model-connection-with-key-transformer.e2e.test.ts @@ -14,7 +14,7 @@ let dbPath = null; let server; jest.setTimeout(20000); beforeAll(async () => { - const validSchema = ` + const validSchema = ` type AProject @model(subscriptions: null) @key(fields: ["projectId"]) @@ -97,22 +97,23 @@ beforeAll(async () => { connection: Model1 @connection(sortField: "modelOneSort") modelOneSort: Int! } - ` + `; try { const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new ModelKeyTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}), - ] - }) + transformers: [ + new DynamoDBModelTransformer(), + new ModelConnectionTransformer(), + new ModelKeyTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); const out = transformer.transform(validSchema); let ddbClient; @@ -151,25 +152,32 @@ afterAll(async () => { */ test('Unnamed connection 1 way navigation, with primary @key directive 1:1', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateATeam { createATeam(input: {teamId: "T1", name: "Team 1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateAProject { createAProject(input: {projectId: "P1", name: "P1", aProjectTeamId: "T1"}) { projectId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListAProjects { listAProjects { items { @@ -182,44 +190,56 @@ test('Unnamed connection 1 way navigation, with primary @key directive 1:1', asy } } } - `, {}) - expect(queryResponse.data.listAProjects).toBeDefined() - const items = queryResponse.data.listAProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].team).toBeDefined() - expect(items[0].team.teamId).toEqual('T1') -}) + `, + {} + ); + expect(queryResponse.data.listAProjects).toBeDefined(); + const items = queryResponse.data.listAProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].team).toBeDefined(); + expect(items[0].team.teamId).toEqual('T1'); +}); test('Unnamed connection 1 way navigation, with primary @key directive 1:M', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateBProject { createBProject(input: {projectId: "P1", name: "P1"}) { projectId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateBTeam { createBTeam(input: {teamId: "T1", name: "Team 1", bProjectTeamsId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateBTeam { createBTeam(input: {teamId: "T2", name: "Team 2", bProjectTeamsId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListBProjects { listBProjects { items { @@ -234,38 +254,47 @@ test('Unnamed connection 1 way navigation, with primary @key directive 1:M', asy } } } - `, {}) - expect(queryResponse.data.listBProjects).toBeDefined() - const items = queryResponse.data.listBProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].teams).toBeDefined() - expect(items[0].teams.items).toBeDefined() - expect(items[0].teams.items.length).toEqual(2) - expect(items[0].teams.items[0].teamId).toEqual('T1') - expect(items[0].teams.items[1].teamId).toEqual('T2') -}) + `, + {} + ); + expect(queryResponse.data.listBProjects).toBeDefined(); + const items = queryResponse.data.listBProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].teams).toBeDefined(); + expect(items[0].teams.items).toBeDefined(); + expect(items[0].teams.items.length).toEqual(2); + expect(items[0].teams.items[0].teamId).toEqual('T1'); + expect(items[0].teams.items[1].teamId).toEqual('T2'); +}); test('Named connection 2 way navigation, with with custom @key fields 1:1', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateCTeam { createCTeam(input: {teamId: "T1", name: "Team 1", cTeamProjectId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateCProject { createCProject(input: {projectId: "P1", name: "P1", cProjectTeamId: "T1"}) { projectId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListCProjects { listCProjects { items { @@ -282,46 +311,58 @@ test('Named connection 2 way navigation, with with custom @key fields 1:1', asyn } } } - `, {}) - expect(queryResponse.data.listCProjects).toBeDefined() - const items = queryResponse.data.listCProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].team).toBeDefined() - expect(items[0].team.teamId).toEqual('T1') - expect(items[0].team.project).toBeDefined() - expect(items[0].team.project.projectId).toEqual('P1') -}) + `, + {} + ); + expect(queryResponse.data.listCProjects).toBeDefined(); + const items = queryResponse.data.listCProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].team).toBeDefined(); + expect(items[0].team.teamId).toEqual('T1'); + expect(items[0].team.project).toBeDefined(); + expect(items[0].team.project.projectId).toEqual('P1'); +}); test('Named connection 2 way navigation, with with custom @key fields 1:M', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateDProject { createDProject(input: {projectId: "P1", name: "P1"}) { projectId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateDTeam { createDTeam(input: {teamId: "T1", name: "Team 1", dTeamProjectId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateDTeam { createDTeam(input: {teamId: "T2", name: "Team 2", dTeamProjectId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListDProjects { listDProjects { items { @@ -340,24 +381,27 @@ test('Named connection 2 way navigation, with with custom @key fields 1:M', asyn } } } - `, {}) - expect(queryResponse.data.listDProjects).toBeDefined() - const items = queryResponse.data.listDProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].teams).toBeDefined() - expect(items[0].teams.items).toBeDefined() - expect(items[0].teams.items.length).toEqual(2) - expect(items[0].teams.items[0].teamId).toEqual('T1') - expect(items[0].teams.items[0].project).toBeDefined() - expect(items[0].teams.items[0].project.projectId).toEqual('P1') - expect(items[0].teams.items[1].teamId).toEqual('T2') - expect(items[0].teams.items[1].project).toBeDefined() - expect(items[0].teams.items[1].project.projectId).toEqual('P1') -}) + `, + {} + ); + expect(queryResponse.data.listDProjects).toBeDefined(); + const items = queryResponse.data.listDProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].teams).toBeDefined(); + expect(items[0].teams.items).toBeDefined(); + expect(items[0].teams.items.length).toEqual(2); + expect(items[0].teams.items[0].teamId).toEqual('T1'); + expect(items[0].teams.items[0].project).toBeDefined(); + expect(items[0].teams.items[0].project.projectId).toEqual('P1'); + expect(items[0].teams.items[1].teamId).toEqual('T2'); + expect(items[0].teams.items[1].project).toBeDefined(); + expect(items[0].teams.items[1].project.projectId).toEqual('P1'); +}); test('Unnamed connection with sortField parameter only #2100', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation M11 { createModel1(input: {id: "M11", sort: 10, name: "M1-1"}) { id @@ -365,9 +409,12 @@ test('Unnamed connection with sortField parameter only #2100', async () => { sort } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation M12 { createModel1(input: {id: "M12", sort: 10, name: "M1-2"}) { id @@ -375,18 +422,24 @@ test('Unnamed connection with sortField parameter only #2100', async () => { sort } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation M21 { createModel2(input: {id: "M21", modelOneSort: 10, model2ConnectionId: "M11"}) { id modelOneSort } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query Query { getModel2(id: "M21") { id @@ -397,11 +450,13 @@ test('Unnamed connection with sortField parameter only #2100', async () => { } } } - `, {}) - expect(queryResponse.data.getModel2).toBeDefined() - const item = queryResponse.data.getModel2 - expect(item.id).toEqual('M21') - expect(item.connection).toBeDefined() - expect(item.connection.id).toEqual('M11') - expect(item.connection.sort).toEqual(10) -}) + `, + {} + ); + expect(queryResponse.data.getModel2).toBeDefined(); + const item = queryResponse.data.getModel2; + expect(item.id).toEqual('M21'); + expect(item.connection).toBeDefined(); + expect(item.connection.id).toEqual('M11'); + expect(item.connection.sort).toEqual(10); +}); diff --git a/packages/amplify-util-mock/src/__e2e__/multi-auth-model-auth-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/multi-auth-model-auth-transformer.e2e.test.ts index 56b8511639..550af90cb9 100644 --- a/packages/amplify-util-mock/src/__e2e__/multi-auth-model-auth-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/multi-auth-model-auth-transformer.e2e.test.ts @@ -240,9 +240,7 @@ test(`Test 'public' authStrategy`, async () => { expect(true).toBe(false); } catch (e) { - expect(e.message).toMatch( - 'GraphQL error: Not Authorized to access getPostPublic on type Query' - ); + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublic on type Query'); } } catch (e) { expect(true).toBe(false); @@ -286,9 +284,7 @@ test(`Test 'private' authStrategy`, async () => { expect(true).toBe(false); } catch (e) { - expect(e.message).toMatch( - 'GraphQL error: Not Authorized to access getPostPrivate on type Query' - ); + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivate on type Query'); } } catch (e) { console.error(e); @@ -389,9 +385,7 @@ describe(`Connection tests with @auth on type`, () => { mutation: createPostMutation, fetchPolicy: 'no-cache', }) - ).rejects.toThrow( - 'GraphQL error: Not Authorized to access createPostConnection on type Mutation' - ); + ).rejects.toThrow('GraphQL error: Not Authorized to access createPostConnection on type Mutation'); }); it('Add a comment with ApiKey - Fail', async () => { diff --git a/packages/amplify-util-mock/src/__e2e__/per-field-auth-tests.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/per-field-auth-tests.e2e.test.ts index 36eaa15e3a..a20683688e 100644 --- a/packages/amplify-util-mock/src/__e2e__/per-field-auth-tests.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/per-field-auth-tests.e2e.test.ts @@ -12,7 +12,6 @@ import { signUpAddToGroupAndGetJwtToken } from './utils/cognito-utils'; jest.setTimeout(2000000); - let GRAPHQL_ENDPOINT = undefined; let ddbEmulator = null; let dbPath = null; @@ -145,10 +144,7 @@ beforeAll(async () => { Authorization: idToken, }); - const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [ - DEVS_GROUP_NAME, - INSTRUCTOR_GROUP_NAME, - ]); + const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME, INSTRUCTOR_GROUP_NAME]); GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2, }); @@ -553,9 +549,7 @@ test('AND per-field dynamic auth rule test', async () => { } `); logDebug(badUpdatePostResponse); - expect(badUpdatePostResponse.errors[0].errorType).toEqual( - 'DynamoDB:ConditionalCheckFailedException' - ); + expect(badUpdatePostResponse.errors[0].errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); const correctUpdatePostResponse = await GRAPHQL_CLIENT_1.query(`mutation UpdatePost { updatePost(input: {id: "${postID1}", text: "newText"}) { diff --git a/packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts index bc17df42c4..450895f15f 100644 --- a/packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts @@ -9,10 +9,10 @@ import gql from 'graphql-tag'; import 'isomorphic-fetch'; // To overcome of the way of how AmplifyJS picks up currentUserCredentials -const anyAWS = (AWS as any); +const anyAWS = AWS as any; if (anyAWS && anyAWS.config && anyAWS.config.credentials) { - delete anyAWS.config.credentials; + delete anyAWS.config.credentials; } // to deal with bug in cognito-identity-js @@ -26,7 +26,7 @@ let GRAPHQL_ENDPOINT = undefined; let ddbEmulator = null; let dbPath = null; let server; -const AWS_REGION = 'my-local-2' +const AWS_REGION = 'my-local-2'; let GRAPHQL_CLIENT_1: AWSAppSyncClient = undefined; @@ -46,32 +46,31 @@ const INSTRUCTOR_GROUP_NAME = 'Instructor'; * Interface Inputs */ interface CreateStudentInput { - id?: string, - name?: string, - email?: string, - ssn?: string, + id?: string; + name?: string; + email?: string; + ssn?: string; } interface UpdateStudentInput { - id: string, - name?: string, - email?: string, - ssn?: string, + id: string; + name?: string; + email?: string; + ssn?: string; } interface CreatePostInput { - id?: string, - title: string, - postOwner: string, + id?: string; + title: string; + postOwner: string; } interface DeleteTypeInput { - id: string, + id: string; } - beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Student @model @auth(rules: [ {allow: owner} @@ -93,305 +92,325 @@ beforeAll(async () => { postOwner: String } `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: 'AMAZON_COGNITO_USER_POOLS' - }, - additionalAuthenticationProviders: [], - } - }), - ] + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + + try { + const out = transformer.transform(validSchema); + + let ddbClient; + ({ dbPath, emulator: ddbEmulator, client: ddbClient } = await launchDDBLocal()); + + const result = await deploy(out, ddbClient); + server = result.simulator; + + GRAPHQL_ENDPOINT = server.url + '/graphql'; + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + // Configure Amplify, create users, and sign in. + const idToken1 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME1, USERNAME1, [INSTRUCTOR_GROUP_NAME]); + GRAPHQL_CLIENT_1 = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools', + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken1, + }, + }); + const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [INSTRUCTOR_GROUP_NAME]); + GRAPHQL_CLIENT_2 = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools', + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken2, + }, + }); + const idToken3 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME3, USERNAME3, []); + GRAPHQL_CLIENT_3 = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools', + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken3, + }, }); - try { - const out = transformer.transform(validSchema); - - let ddbClient; - ({ dbPath, emulator: ddbEmulator, client: ddbClient } = await launchDDBLocal()); - - const result = await deploy(out, ddbClient); - server = result.simulator; - - GRAPHQL_ENDPOINT = server.url + '/graphql'; - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy(); - // Configure Amplify, create users, and sign in. - const idToken1 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME1, USERNAME1, [ - INSTRUCTOR_GROUP_NAME - ]); - GRAPHQL_CLIENT_1 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'userPools' - }, - auth: { - type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, - jwtToken: idToken1, - }}) - const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [ - INSTRUCTOR_GROUP_NAME, - ]); - GRAPHQL_CLIENT_2 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'userPools' - }, - auth: { - type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, - jwtToken: idToken2, - }}) - const idToken3 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME3, USERNAME3, []); - GRAPHQL_CLIENT_3 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'userPools' - }, - auth: { - type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, - jwtToken: idToken3, - }}) - - // Wait for any propagation to avoid random - // "The security token included in the request is invalid" errors - await new Promise(res => setTimeout(() => res(), 5000)); - } catch (e) { - console.error(e); - expect(true).toEqual(false); - } -}) + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } +}); afterAll(async () => { - try { - if (server) { - await server.stop(); + try { + if (server) { + await server.stop(); } await terminateDDB(ddbEmulator, dbPath); - } catch (e) { - console.error(e); - throw e; - } -}) + } catch (e) { + console.error(e); + throw e; + } +}); /** * Tests */ test('Test that only authorized members are allowed to view subscriptions', async done => { - // subscribe to create students as user 2 - const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql` - subscription OnCreateStudent { - onCreateStudent { - id - name - email - ssn - owner - } - }`}) - console.log(observer) - let subscription = observer.subscribe( (event: any) => { - console.log('subscription event: ', event) - const student = event.data.onCreateStudent; - subscription.unsubscribe() - expect(student.name).toEqual('student1') - expect(student.email).toEqual('student1@domain.com') - expect(student.ssn).toBeNull() - done(); - }); - - await new Promise((res) => setTimeout(() => res(), 1000)) - - createStudent(GRAPHQL_CLIENT_1, - { - name: "student1", - email: "student1@domain.com", - ssn: "AAA-01-SSSS", - }) -}) + // subscribe to create students as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ + query: gql` + subscription OnCreateStudent { + onCreateStudent { + id + name + email + ssn + owner + } + } + `, + }); + console.log(observer); + let subscription = observer.subscribe((event: any) => { + console.log('subscription event: ', event); + const student = event.data.onCreateStudent; + subscription.unsubscribe(); + expect(student.name).toEqual('student1'); + expect(student.email).toEqual('student1@domain.com'); + expect(student.ssn).toBeNull(); + done(); + }); + + await new Promise(res => setTimeout(() => res(), 1000)); + + createStudent(GRAPHQL_CLIENT_1, { + name: 'student1', + email: 'student1@domain.com', + ssn: 'AAA-01-SSSS', + }); +}); test('Test that a user not in the group is not allowed to view the subscription', async done => { - // suscribe to create students as user 3 - const observer = GRAPHQL_CLIENT_3.subscribe({ query: gql` - subscription OnCreateStudent { - onCreateStudent { - id - name - email - ssn - owner - } - }`}) - observer.subscribe({ - error: (err: any) => { - console.log(err.graphQLErrors[0]) - expect(err.graphQLErrors[0].message).toEqual('Unauthorized') - done() + // suscribe to create students as user 3 + const observer = GRAPHQL_CLIENT_3.subscribe({ + query: gql` + subscription OnCreateStudent { + onCreateStudent { + id + name + email + ssn + owner } - }); - await new Promise((res) => setTimeout(() => res(), 1000)) - - createStudent(GRAPHQL_CLIENT_1, - { - name: "student2", - email: "student2@domain.com", - ssn: "BBB-00-SNSN" - }) -}) + } + `, + }); + observer.subscribe({ + error: (err: any) => { + console.log(err.graphQLErrors[0]); + expect(err.graphQLErrors[0].message).toEqual('Unauthorized'); + done(); + }, + }); + await new Promise(res => setTimeout(() => res(), 1000)); + + createStudent(GRAPHQL_CLIENT_1, { + name: 'student2', + email: 'student2@domain.com', + ssn: 'BBB-00-SNSN', + }); +}); test('Test a subscription on update', async done => { - // susbcribe to update students as user 2 - const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql` - subscription OnUpdateStudent { - onUpdateStudent { - id - name - email - ssn - owner - } - }` }) - let subscription = observer.subscribe( (event: any) => { - const student = event.data.onUpdateStudent; - subscription.unsubscribe() - expect(student.id).toEqual(student3ID) - expect(student.name).toEqual('student3') - expect(student.email).toEqual('emailChanged@domain.com') - expect(student.ssn).toBeNull() - done(); - }); - - const student3 = await createStudent(GRAPHQL_CLIENT_1, - { - name: "student3", - email: "changeThisEmail@domain.com", - ssn: "CCC-01-SNSN" - }) - expect(student3.data.createStudent).toBeDefined() - const student3ID = student3.data.createStudent.id - expect(student3.data.createStudent.name).toEqual('student3') - expect(student3.data.createStudent.email).toEqual('changeThisEmail@domain.com') - expect(student3.data.createStudent.ssn).toBeNull() - - updateStudent(GRAPHQL_CLIENT_1, - { - id: student3ID, - email: 'emailChanged@domain.com' - }) -}) - + // susbcribe to update students as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ + query: gql` + subscription OnUpdateStudent { + onUpdateStudent { + id + name + email + ssn + owner + } + } + `, + }); + let subscription = observer.subscribe((event: any) => { + const student = event.data.onUpdateStudent; + subscription.unsubscribe(); + expect(student.id).toEqual(student3ID); + expect(student.name).toEqual('student3'); + expect(student.email).toEqual('emailChanged@domain.com'); + expect(student.ssn).toBeNull(); + done(); + }); + + const student3 = await createStudent(GRAPHQL_CLIENT_1, { + name: 'student3', + email: 'changeThisEmail@domain.com', + ssn: 'CCC-01-SNSN', + }); + expect(student3.data.createStudent).toBeDefined(); + const student3ID = student3.data.createStudent.id; + expect(student3.data.createStudent.name).toEqual('student3'); + expect(student3.data.createStudent.email).toEqual('changeThisEmail@domain.com'); + expect(student3.data.createStudent.ssn).toBeNull(); + + updateStudent(GRAPHQL_CLIENT_1, { + id: student3ID, + email: 'emailChanged@domain.com', + }); +}); test('Test a subscription on delete', async done => { - // subscribe to onDelete as user 2 - const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql ` - subscription OnDeleteStudent { - onDeleteStudent { - id - name - email - ssn - owner - } - }`}) - let subscription = observer.subscribe( (event: any) => { - const student = event.data.onDeleteStudent; - subscription.unsubscribe() - expect(student.id).toEqual(student4ID) - expect(student.name).toEqual('student4') - expect(student.email).toEqual('plsDelete@domain.com') - expect(student.ssn).toBeNull() - done(); - }); - - const student4 = await createStudent(GRAPHQL_CLIENT_1, - { - name: "student4", - email: "plsDelete@domain.com", - ssn: "DDD-02-SNSN" - }) - expect(student4).toBeDefined() - const student4ID = student4.data.createStudent.id - expect(student4.data.createStudent.email).toEqual('plsDelete@domain.com') - expect(student4.data.createStudent.ssn).toBeNull() - - await deleteStudent(GRAPHQL_CLIENT_1, { id: student4ID }) -}) + // subscribe to onDelete as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ + query: gql` + subscription OnDeleteStudent { + onDeleteStudent { + id + name + email + ssn + owner + } + } + `, + }); + let subscription = observer.subscribe((event: any) => { + const student = event.data.onDeleteStudent; + subscription.unsubscribe(); + expect(student.id).toEqual(student4ID); + expect(student.name).toEqual('student4'); + expect(student.email).toEqual('plsDelete@domain.com'); + expect(student.ssn).toBeNull(); + done(); + }); + + const student4 = await createStudent(GRAPHQL_CLIENT_1, { + name: 'student4', + email: 'plsDelete@domain.com', + ssn: 'DDD-02-SNSN', + }); + expect(student4).toBeDefined(); + const student4ID = student4.data.createStudent.id; + expect(student4.data.createStudent.email).toEqual('plsDelete@domain.com'); + expect(student4.data.createStudent.ssn).toBeNull(); + + await deleteStudent(GRAPHQL_CLIENT_1, { id: student4ID }); +}); // ownerField Tests test('Test subscription onCreatePost with ownerField', async done => { - const observer = GRAPHQL_CLIENT_1.subscribe({ query: gql` + const observer = GRAPHQL_CLIENT_1.subscribe({ + query: gql` subscription OnCreatePost { onCreatePost(postOwner: "${USERNAME1}") { id title postOwner } - }`}) - let subscription = observer.subscribe( (event: any) => { - const post = event.data.onCreatePost; - subscription.unsubscribe() - expect(post.title).toEqual('someTitle') - expect(post.postOwner).toEqual(USERNAME1) - done(); - }); - await new Promise((res) => setTimeout(() => res(), 1000)) - - createPost(GRAPHQL_CLIENT_1, { - title: "someTitle", - postOwner: USERNAME1 - }) -}) + }`, + }); + let subscription = observer.subscribe((event: any) => { + const post = event.data.onCreatePost; + subscription.unsubscribe(); + expect(post.title).toEqual('someTitle'); + expect(post.postOwner).toEqual(USERNAME1); + done(); + }); + await new Promise(res => setTimeout(() => res(), 1000)); + + createPost(GRAPHQL_CLIENT_1, { + title: 'someTitle', + postOwner: USERNAME1, + }); +}); // mutations async function createStudent(client: AWSAppSyncClient, input: CreateStudentInput) { - const request = gql`mutation CreateStudent($input: CreateStudentInput!) { - createStudent(input: $input) { - id - name - email - ssn - owner - } + const request = gql` + mutation CreateStudent($input: CreateStudentInput!) { + createStudent(input: $input) { + id + name + email + ssn + owner + } } - `; - return await client.mutate({ mutation: request, variables: { input }}); + `; + return await client.mutate({ mutation: request, variables: { input } }); } async function updateStudent(client: AWSAppSyncClient, input: UpdateStudentInput) { - const request = gql`mutation UpdateStudent($input: UpdateStudentInput!) { - updateStudent(input: $input) { - id - name - email - ssn - owner - } - }` - return await client.mutate({ mutation: request, variables: { input }}); + const request = gql` + mutation UpdateStudent($input: UpdateStudentInput!) { + updateStudent(input: $input) { + id + name + email + ssn + owner + } + } + `; + return await client.mutate({ mutation: request, variables: { input } }); } async function deleteStudent(client: AWSAppSyncClient, input: DeleteTypeInput) { - const request = gql`mutation DeleteStudent($input: DeleteStudentInput!) { - deleteStudent(input: $input) { - id - name - email - ssn - owner - } - }` - return await client.mutate({ mutation: request, variables: { input }}); + const request = gql` + mutation DeleteStudent($input: DeleteStudentInput!) { + deleteStudent(input: $input) { + id + name + email + ssn + owner + } + } + `; + return await client.mutate({ mutation: request, variables: { input } }); } async function createPost(client: AWSAppSyncClient, input: CreatePostInput) { - const request = gql`mutation CreatePost($input: CreatePostInput!) { - createPost(input: $input) { - id - title - postOwner - } - }` - return await client.mutate({ mutation: request, variables: { input }}); -} \ No newline at end of file + const request = gql` + mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + postOwner + } + } + `; + return await client.mutate({ mutation: request, variables: { input } }); +} diff --git a/packages/amplify-util-mock/src/__e2e__/utils/graphql-client.ts b/packages/amplify-util-mock/src/__e2e__/utils/graphql-client.ts index 1c229cb694..0a0ab01bec 100644 --- a/packages/amplify-util-mock/src/__e2e__/utils/graphql-client.ts +++ b/packages/amplify-util-mock/src/__e2e__/utils/graphql-client.ts @@ -1,28 +1,30 @@ -import axios from 'axios' +import axios from 'axios'; export interface GraphQLLocation { - line: number; - column: number; + line: number; + column: number; } export interface GraphQLError { - message: string; - locations: GraphQLLocation[]; - path: string[] + message: string; + locations: GraphQLLocation[]; + path: string[]; } export interface GraphQLResponse { - data: any; - errors: GraphQLError[] + data: any; + errors: GraphQLError[]; } export class GraphQLClient { - constructor(private url: string, private headers: any) { } + constructor(private url: string, private headers: any) {} - async query(query: string, variables: any): Promise { - const axRes = await axios.post( - this.url, { - query, - variables - }, { headers: this.headers } - ) - return axRes.data - } -} \ No newline at end of file + async query(query: string, variables: any): Promise { + const axRes = await axios.post( + this.url, + { + query, + variables, + }, + { headers: this.headers } + ); + return axRes.data; + } +} diff --git a/packages/amplify-util-mock/src/__e2e__/utils/lambda-helper.ts b/packages/amplify-util-mock/src/__e2e__/utils/lambda-helper.ts index a6e85d6c0a..b4b85e1d91 100644 --- a/packages/amplify-util-mock/src/__e2e__/utils/lambda-helper.ts +++ b/packages/amplify-util-mock/src/__e2e__/utils/lambda-helper.ts @@ -2,14 +2,14 @@ import * as path from 'path'; import * as fs from 'fs-extra'; export function getFunctionDetails(fnName: string) { - const lambdaFolder = path.join(__dirname, 'lambda_functions'); - if(!fs.existsSync(path.join(lambdaFolder, `${fnName}.js`))) { - throw new Error(`Can not find lambda function ${fnName}`); - } + const lambdaFolder = path.join(__dirname, 'lambda_functions'); + if (!fs.existsSync(path.join(lambdaFolder, `${fnName}.js`))) { + throw new Error(`Can not find lambda function ${fnName}`); + } - return { - packageFolder: lambdaFolder, - fileName: `${fnName}.js`, - handler: 'handler' - } -} \ No newline at end of file + return { + packageFolder: lambdaFolder, + fileName: `${fnName}.js`, + handler: 'handler', + }; +} diff --git a/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/echoFunction.js b/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/echoFunction.js index 9682315472..edce029863 100644 --- a/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/echoFunction.js +++ b/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/echoFunction.js @@ -1,4 +1,4 @@ -exports.handler = async (event) => { - console.log(event); - return event; -}; \ No newline at end of file +exports.handler = async event => { + console.log(event); + return event; +}; diff --git a/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/hello.js b/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/hello.js index 78ccc18ce3..e9ac3b7246 100644 --- a/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/hello.js +++ b/packages/amplify-util-mock/src/__e2e__/utils/lambda_functions/hello.js @@ -1,4 +1,4 @@ -exports.handler = async (event) => { - console.log(event); - return "Hello, world!"; -}; \ No newline at end of file +exports.handler = async event => { + console.log(event); + return 'Hello, world!'; +}; diff --git a/packages/amplify-util-mock/src/__e2e__/utils/test-storage.ts b/packages/amplify-util-mock/src/__e2e__/utils/test-storage.ts index 154c2ac4af..4069410c44 100644 --- a/packages/amplify-util-mock/src/__e2e__/utils/test-storage.ts +++ b/packages/amplify-util-mock/src/__e2e__/utils/test-storage.ts @@ -1,28 +1,28 @@ export default class TestStorage { - private data: any - constructor() { - this.data = {} - } - // set item with the key - public setItem(key: string, value: string): string { - this.data[key] = value - return value - } - // get item with the key - public getItem(key: string): string { - return this.data[key] - } - // remove item with the key - public removeItem(key: string): void { - this.data[key] = undefined - } - // clear out the storage - public clear(): void { - this.data = {} - } - // If the storage operations are async(i.e AsyncStorage) - // Then you need to sync those items into the memory in this method - public sync(): Promise { - return Promise.resolve(this.data) - } -} \ No newline at end of file + private data: any; + constructor() { + this.data = {}; + } + // set item with the key + public setItem(key: string, value: string): string { + this.data[key] = value; + return value; + } + // get item with the key + public getItem(key: string): string { + return this.data[key]; + } + // remove item with the key + public removeItem(key: string): void { + this.data[key] = undefined; + } + // clear out the storage + public clear(): void { + this.data = {}; + } + // If the storage operations are async(i.e AsyncStorage) + // Then you need to sync those items into the memory in this method + public sync(): Promise { + return Promise.resolve(this.data); + } +} diff --git a/packages/amplify-util-mock/src/__e2e__/versioned-model-transformer.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/versioned-model-transformer.e2e.test.ts index fff7100f11..40a52d7931 100644 --- a/packages/amplify-util-mock/src/__e2e__/versioned-model-transformer.e2e.test.ts +++ b/packages/amplify-util-mock/src/__e2e__/versioned-model-transformer.e2e.test.ts @@ -24,10 +24,7 @@ beforeAll(async () => { try { const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new VersionedModelTransformer(), - ], + transformers: [new DynamoDBModelTransformer(), new VersionedModelTransformer()], }); const out = transformer.transform(validSchema); @@ -150,9 +147,7 @@ test('Test failed updatePost mutation with wrong version', async () => { {} ); expect(updateResponse.errors.length).toEqual(1); - expect((updateResponse.errors[0] as any).errorType).toEqual( - 'DynamoDB:ConditionalCheckFailedException' - ); + expect((updateResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); }); test('Test deletePost mutation', async () => { @@ -212,7 +207,5 @@ test('Test deletePost mutation with wrong version', async () => { {} ); expect(deleteResponse.errors.length).toEqual(1); - expect((deleteResponse.errors[0] as any).errorType).toEqual( - 'DynamoDB:ConditionalCheckFailedException' - ); + expect((deleteResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); }); diff --git a/packages/amplify-util-mock/src/__tests__/CFNParser/intrinsic-functions.test.ts b/packages/amplify-util-mock/src/__tests__/CFNParser/intrinsic-functions.test.ts index cb4c3a88b2..263c37eeb9 100644 --- a/packages/amplify-util-mock/src/__tests__/CFNParser/intrinsic-functions.test.ts +++ b/packages/amplify-util-mock/src/__tests__/CFNParser/intrinsic-functions.test.ts @@ -11,7 +11,7 @@ import { cfnAnd, cfnOr, cfnImportValue, - cfnCondition + cfnCondition, } from '../../CFNParser/intrinsic-functions'; import { CloudFormationParseContext } from '../../CFNParser/types'; describe('intrinsic-functions', () => { @@ -20,7 +20,7 @@ describe('intrinsic-functions', () => { params: {}, conditions: {}, resources: {}, - exports: {} + exports: {}, }; it('should join values', () => { const node: any = ['-', ['foo', 'bar', 'baz']]; @@ -52,7 +52,7 @@ describe('intrinsic-functions', () => { params: {}, conditions: {}, resources: {}, - exports: {} + exports: {}, }; it('should substitute variable', () => { const parseValue = jest.fn(val => val); @@ -77,10 +77,10 @@ describe('intrinsic-functions', () => { conditions: {}, resources: { FooResource: { - prop1: 'prop1 value' - } + prop1: 'prop1 value', + }, }, - exports: {} + exports: {}, }; it('should get attribute value from parsed resources', () => { const node = ['FooResource', 'prop1']; @@ -103,7 +103,7 @@ describe('intrinsic-functions', () => { params: {}, conditions: {}, resources: {}, - exports: {} + exports: {}, }; it('should split string', () => { const node = ['-', 'foo-bar-baz']; @@ -125,9 +125,9 @@ describe('intrinsic-functions', () => { conditions: {}, resources: { fromResource: { name: 'resource' }, - fromResource2: 'bar' + fromResource2: 'bar', }, - exports: {} + exports: {}, }; it('should get ref from params', () => { @@ -161,7 +161,7 @@ describe('intrinsic-functions', () => { params: {}, conditions: {}, resources: {}, - exports: {} + exports: {}, }; afterEach(() => { jest.resetAllMocks(); @@ -186,14 +186,13 @@ describe('intrinsic-functions', () => { bar: false, }, resources: {}, - exports: {} + exports: {}, }; let parseValue; beforeEach(() => { - parseValue = jest.fn((val) => val) - }) + parseValue = jest.fn(val => val); + }); it('should return true value section when condition is true', () => { - const node = ['foo', 'foo value', 'bar value']; expect(cfnIf(node, cfnContext, parseValue)).toEqual('foo value'); }); @@ -201,7 +200,7 @@ describe('intrinsic-functions', () => { const node = ['bar', 'foo value', 'bar value']; expect(cfnIf(node, cfnContext, parseValue)).toEqual('bar value'); }); - }) + }); describe('cfnEquals', () => { const cfnContext: CloudFormationParseContext = { @@ -211,65 +210,63 @@ describe('intrinsic-functions', () => { bar: false, }, resources: {}, - exports: {} + exports: {}, }; - const parseValue = jest.fn((val) => val) + const parseValue = jest.fn(val => val); it('should return true when equal', () => { - const node = ['foo', 'foo', ]; - expect(cfnEquals(node, cfnContext, parseValue)).toBeTruthy() + const node = ['foo', 'foo']; + expect(cfnEquals(node, cfnContext, parseValue)).toBeTruthy(); expect(parseValue).toHaveBeenCalled(); }); it('should return false when not equal', () => { - const node = ['foo', 'bar', ]; - expect(cfnEquals(node, cfnContext, (val) => val)).toBeFalsy() - }) + const node = ['foo', 'bar']; + expect(cfnEquals(node, cfnContext, val => val)).toBeFalsy(); + }); }); describe('cfnNot', () => { const cfnContext: CloudFormationParseContext = { params: {}, - conditions: { - }, + conditions: {}, resources: {}, - exports: {} + exports: {}, }; - const parseValue = jest.fn((val) => val) + const parseValue = jest.fn(val => val); it('should return false when Fn::Not(trueCondition)', () => { parseValue.mockReturnValueOnce(true); const node = ['trueCondition']; - expect(cfnNot(node, cfnContext, parseValue)).toBeFalsy() + expect(cfnNot(node, cfnContext, parseValue)).toBeFalsy(); expect(parseValue).toHaveBeenCalled(); }); it('should return true when {Fn::Not, falseCondition}', () => { parseValue.mockReturnValueOnce(false); const node = ['falseCondition']; - expect(cfnNot(node, cfnContext, parseValue)).toBeTruthy() + expect(cfnNot(node, cfnContext, parseValue)).toBeTruthy(); expect(parseValue).toHaveBeenCalled(); - }) + }); }); describe('cfnAnd', () => { const cfnContext: CloudFormationParseContext = { params: {}, - conditions: { - }, + conditions: {}, resources: {}, - exports: {} + exports: {}, }; let parseValue; beforeEach(() => { parseValue = jest.fn(); - }) + }); it('should return false when Fn::And encounter a false value', () => { parseValue.mockReturnValueOnce(true); parseValue.mockReturnValueOnce(true); parseValue.mockReturnValueOnce(false); parseValue.mockReturnValueOnce(true); const node = ['cond1', 'cond2', 'cond3', 'cond4']; - expect(cfnAnd(node, cfnContext, parseValue)).toBeFalsy() + expect(cfnAnd(node, cfnContext, parseValue)).toBeFalsy(); expect(parseValue).toHaveBeenCalled(); }); @@ -277,30 +274,29 @@ describe('intrinsic-functions', () => { parseValue.mockReturnValue(true); const node = ['cond1', 'cond2', 'cond3', 'cond4']; - expect(cfnAnd(node, cfnContext, parseValue)).toBeTruthy() + expect(cfnAnd(node, cfnContext, parseValue)).toBeTruthy(); expect(parseValue).toHaveBeenCalledTimes(4); - }) + }); }); describe('cfnOr', () => { const cfnContext: CloudFormationParseContext = { params: {}, - conditions: { - }, + conditions: {}, resources: {}, - exports: {} + exports: {}, }; let parseValue; beforeEach(() => { parseValue = jest.fn(); - }) + }); it('should return true when Fn::Or encounter at least one true value', () => { parseValue.mockReturnValueOnce(true); parseValue.mockReturnValueOnce(true); parseValue.mockReturnValueOnce(false); parseValue.mockReturnValueOnce(true); const node = ['cond1', 'cond2', 'cond3', 'cond4']; - expect(cfnOr(node, cfnContext, parseValue)).toBeTruthy() + expect(cfnOr(node, cfnContext, parseValue)).toBeTruthy(); expect(parseValue).toHaveBeenCalled(); }); @@ -308,24 +304,22 @@ describe('intrinsic-functions', () => { parseValue.mockReturnValue(false); const node = ['cond1', 'cond2', 'cond3', 'cond4']; - expect(cfnAnd(node, cfnContext, parseValue)).toBeFalsy() + expect(cfnAnd(node, cfnContext, parseValue)).toBeFalsy(); expect(parseValue).toHaveBeenCalledTimes(4); - }) + }); }); describe('cfnImportValue', () => { const cfnContext: CloudFormationParseContext = { params: {}, - conditions: { - }, + conditions: {}, resources: {}, - exports: {'foo' : 'fooValue'} + exports: { foo: 'fooValue' }, }; let parseValue; beforeEach(() => { - parseValue = jest.fn((val) => val); - }) - + parseValue = jest.fn(val => val); + }); it('should return value for key from exports', () => { const node = 'foo'; @@ -335,7 +329,7 @@ describe('intrinsic-functions', () => { it('should throw error if the value is not presnt in exports', () => { const node = 'bar'; - expect(() => cfnImportValue(node, cfnContext, parseValue)).toThrow() + expect(() => cfnImportValue(node, cfnContext, parseValue)).toThrow(); }); }); @@ -347,16 +341,15 @@ describe('intrinsic-functions', () => { bar: false, }, resources: {}, - exports: {} + exports: {}, }; it('should return condition value', () => { - expect(cfnCondition('bar', cfnContext, () => {})).toBeFalsy() - expect(cfnCondition('foo', cfnContext, () => {})).toBeTruthy() - - }) + expect(cfnCondition('bar', cfnContext, () => {})).toBeFalsy(); + expect(cfnCondition('foo', cfnContext, () => {})).toBeTruthy(); + }); it('should throw if the condition is missing', () => { - expect(() => cfnCondition('missing-condition', cfnContext, ()=>{})).toThrow() - }) + expect(() => cfnCondition('missing-condition', cfnContext, () => {})).toThrow(); + }); }); }); diff --git a/packages/amplify-util-mock/src/amplify-plugin-index.ts b/packages/amplify-util-mock/src/amplify-plugin-index.ts index 6c826a36c5..440d3d87b7 100644 --- a/packages/amplify-util-mock/src/amplify-plugin-index.ts +++ b/packages/amplify-util-mock/src/amplify-plugin-index.ts @@ -12,4 +12,4 @@ export async function executeAmplifyCommand(context: any) { export async function handleAmplifyEvent(context: any, args: any) { context.print.info(`${pluginName} handleAmplifyEvent to be implemented`); context.print.info(`Received event args ${args}`); -} \ No newline at end of file +} diff --git a/packages/amplify-util-mock/src/api/api.ts b/packages/amplify-util-mock/src/api/api.ts index 380b2ecba7..88b6bff48b 100644 --- a/packages/amplify-util-mock/src/api/api.ts +++ b/packages/amplify-util-mock/src/api/api.ts @@ -82,24 +82,14 @@ export class APITest { await this.ensureDDBTables(config); config = this.configureDDBDataSource(config); this.transformerResult = this.configureLambdaDataSource(context, config); - const overriddenTemplates = await this.resolverOverrideManager.sync( - this.transformerResult.mappingTemplates - ); + const overriddenTemplates = await this.resolverOverrideManager.sync(this.transformerResult.mappingTemplates); return { ...this.transformerResult, mappingTemplates: overriddenTemplates }; } private async generateCode(context, transformerOutput = null) { try { context.print.info('Running GraphQL codegen'); const { projectPath } = context.amplify.getEnvInfo(); - const schemaPath = path.join( - projectPath, - 'amplify', - 'backend', - 'api', - this.apiName, - 'build', - 'schema.graphql' - ); + const schemaPath = path.join(projectPath, 'amplify', 'backend', 'api', this.apiName, 'build', 'schema.graphql'); if (transformerOutput) { fs.writeFileSync(schemaPath, transformerOutput.schema); } @@ -119,9 +109,7 @@ export class APITest { const inputSchemaPath = path.join(apiDir, 'schema'); try { let shouldReload; - if ( - this.resolverOverrideManager.isTemplateFile(filePath, action === 'unlink' ? true : false) - ) { + if (this.resolverOverrideManager.isTemplateFile(filePath, action === 'unlink' ? true : false)) { switch (action) { case 'add': shouldReload = this.resolverOverrideManager.onAdd(filePath); @@ -136,9 +124,7 @@ export class APITest { if (shouldReload) { context.print.info('Mapping template change detected. Reloading...'); - const mappingTemplates = this.resolverOverrideManager.sync( - this.transformerResult.mappingTemplates - ); + const mappingTemplates = this.resolverOverrideManager.sync(this.transformerResult.mappingTemplates); await this.appSyncSimulator.reload({ ...this.transformerResult, mappingTemplates, @@ -175,10 +161,7 @@ export class APITest { if (lambdaDataSources.length === 0) { return config; } - const provisionedLambdas = getAllLambdaFunctions( - context, - path.join(this.projectRoot, 'amplify', 'backend') - ); + const provisionedLambdas = getAllLambdaFunctions(context, path.join(this.projectRoot, 'amplify', 'backend')); return { ...config, @@ -193,17 +176,13 @@ export class APITest { functionName = functionName.replace('-${env}', ''); const lambdaConfig = provisionedLambdas.find(fn => fn.name === functionName); if (!lambdaConfig) { - throw new Error( - `Lambda function ${functionName} does not exist in your project. \nPlease run amplify add function` - ); + throw new Error(`Lambda function ${functionName} does not exist in your project. \nPlease run amplify add function`); } const [fileName, handlerFn] = lambdaConfig.handler.split('.'); const lambdaPath = path.join(lambdaConfig.basePath, `${fileName}.js`); if (!fs.existsSync(lambdaPath)) { - throw new Error( - `Lambda function ${functionName} does not exist in your project. \nPlease run amplify add function` - ); + throw new Error(`Lambda function ${functionName} does not exist in your project. \nPlease run amplify add function`); } return { ...d, diff --git a/packages/amplify-util-mock/src/api/resolver-overrides.ts b/packages/amplify-util-mock/src/api/resolver-overrides.ts index e83d596526..96d6bd6709 100644 --- a/packages/amplify-util-mock/src/api/resolver-overrides.ts +++ b/packages/amplify-util-mock/src/api/resolver-overrides.ts @@ -85,9 +85,7 @@ export class ResolverOverrides { // Files that are in the disk used by resolvers created by custom stack will exist in resolver folder // include them in the resolver output const resolversCreatedByTransformer = result.map(r => r.path); - const customResolverTemplates = Array.from(this.overrides.values()).filter( - o => !resolversCreatedByTransformer.includes(o) - ); + const customResolverTemplates = Array.from(this.overrides.values()).filter(o => !resolversCreatedByTransformer.includes(o)); customResolverTemplates.forEach(templateName => { result.push({ path: templateName, diff --git a/packages/amplify-util-mock/src/api/run-graphql-transformer.ts b/packages/amplify-util-mock/src/api/run-graphql-transformer.ts index e086f04cfa..6201a7c83a 100644 --- a/packages/amplify-util-mock/src/api/run-graphql-transformer.ts +++ b/packages/amplify-util-mock/src/api/run-graphql-transformer.ts @@ -1,15 +1,10 @@ export async function runTransformer(context: any) { - const transformerOutput = await context.amplify.executeProviderUtils( - context, - 'awscloudformation', - 'compileSchema', - { - noConfig: true, - forceCompile: true, - dryRun: true, - disableResolverOverrides: true, - } - ); + const transformerOutput = await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { + noConfig: true, + forceCompile: true, + dryRun: true, + disableResolverOverrides: true, + }); const stack = Object.values(transformerOutput.stacks).reduce( (prev, stack: any) => { return { ...prev, ...stack.Resources }; diff --git a/packages/amplify-util-mock/src/index.ts b/packages/amplify-util-mock/src/index.ts index b3a5570a1e..df3e2511b7 100644 --- a/packages/amplify-util-mock/src/index.ts +++ b/packages/amplify-util-mock/src/index.ts @@ -1,10 +1,7 @@ import * as storage from './storage'; import * as api from './api'; -export * from './amplify-plugin-index'; +export * from './amplify-plugin-index'; export { mockAllCategories } from './mockAll'; -export { - api, - storage, -}; +export { api, storage }; diff --git a/packages/amplify-util-mock/src/mockAll.ts b/packages/amplify-util-mock/src/mockAll.ts index 779faa755c..f93b60faca 100644 --- a/packages/amplify-util-mock/src/mockAll.ts +++ b/packages/amplify-util-mock/src/mockAll.ts @@ -6,10 +6,9 @@ export async function mockAllCategories(context: any) { const mockableResources = resources.allResources.filter( resource => resource.service && MOCK_SUPPORTED_CATEGORY.includes(resource.service) ); - const resourceToBePushed = [ - ...resources.resourcesToBeUpdated, - ...resources.resourcesToBeCreated, - ].filter(resource => resource.service && !MOCK_SUPPORTED_CATEGORY.includes(resource.service)); + const resourceToBePushed = [...resources.resourcesToBeUpdated, ...resources.resourcesToBeCreated].filter( + resource => resource.service && !MOCK_SUPPORTED_CATEGORY.includes(resource.service) + ); if (mockableResources.length) { if (resourceToBePushed.length) { try { @@ -17,24 +16,13 @@ export async function mockAllCategories(context: any) { context.print.info( 'Some resources have changed locally and these resources are not mockable. The resources listed below need to be pushed to the cloud before starting the mock server.' ); - const didPush = await context.amplify.pushResources( - context, - undefined, - undefined, - resourceToBePushed - ); + const didPush = await context.amplify.pushResources(context, undefined, undefined, resourceToBePushed); if (!didPush) { - context.print.info( - '\n\nMocking may not work as expected since some of the changed resources were not pushed.' - ); + context.print.info('\n\nMocking may not work as expected since some of the changed resources were not pushed.'); } } catch (e) { - context.print.info( - `Pushing to the cloud failed with the following error \n${e.message}\n\n` - ); - const startServer = await await context.amplify.confirmPrompt.run( - 'Do you still want to start the mock server?' - ); + context.print.info(`Pushing to the cloud failed with the following error \n${e.message}\n\n`); + const startServer = await await context.amplify.confirmPrompt.run('Do you still want to start the mock server?'); if (!startServer) { return; } diff --git a/packages/amplify-util-mock/src/storage/index.ts b/packages/amplify-util-mock/src/storage/index.ts index cb49233750..41aec58021 100644 --- a/packages/amplify-util-mock/src/storage/index.ts +++ b/packages/amplify-util-mock/src/storage/index.ts @@ -3,30 +3,30 @@ const MOCK_SUPPORTED_CATEGORY = ['S3']; const RESOURCE_NEEDS_PUSH = ['Cognito']; export async function start(context) { - const resources = await context.amplify.getResourceStatus(); - const mockableResources = resources.allResources.filter( - resource => resource.service && MOCK_SUPPORTED_CATEGORY.includes(resource.service) - ); - const resourceToBePushed = [...resources.resourcesToBeCreated].filter( - resource => resource.service && RESOURCE_NEEDS_PUSH.includes(resource.service) - ); + const resources = await context.amplify.getResourceStatus(); + const mockableResources = resources.allResources.filter( + resource => resource.service && MOCK_SUPPORTED_CATEGORY.includes(resource.service) + ); + const resourceToBePushed = [...resources.resourcesToBeCreated].filter( + resource => resource.service && RESOURCE_NEEDS_PUSH.includes(resource.service) + ); - if (mockableResources.length) { - if (resourceToBePushed.length) { - context.print.info( - 'Storage Mocking needs Auth resources to be pushed to the cloud. Please run `amplify auth push` before running storage mock' - ); - return Promise.resolve(false); - } - const mockStorage = new StorageTest(); - try { - await mockStorage.start(context); - // call s3 trigger - mockStorage.trigger(context); - } catch (e) { - console.log(e); - // Sending term signal so we clean up after ourself - process.kill(process.pid, 'SIGTERM'); - } + if (mockableResources.length) { + if (resourceToBePushed.length) { + context.print.info( + 'Storage Mocking needs Auth resources to be pushed to the cloud. Please run `amplify auth push` before running storage mock' + ); + return Promise.resolve(false); } + const mockStorage = new StorageTest(); + try { + await mockStorage.start(context); + // call s3 trigger + mockStorage.trigger(context); + } catch (e) { + console.log(e); + // Sending term signal so we clean up after ourself + process.kill(process.pid, 'SIGTERM'); + } + } } diff --git a/packages/amplify-util-mock/src/storage/storage.ts b/packages/amplify-util-mock/src/storage/storage.ts index 99d4838818..f2be975926 100644 --- a/packages/amplify-util-mock/src/storage/storage.ts +++ b/packages/amplify-util-mock/src/storage/storage.ts @@ -64,12 +64,7 @@ export class StorageTest { const existingStorage = meta.storage; let backendPath = context.amplify.pathManager.getBackendDirPath(); const resourceName = Object.keys(existingStorage)[0]; - const CFNFilePath = path.join( - backendPath, - 'storage', - resourceName, - 's3-cloudformation-template.json' - ); + const CFNFilePath = path.join(backendPath, 'storage', resourceName, 's3-cloudformation-template.json'); const storageParams = context.amplify.readJsonFile(CFNFilePath); const lambdaConfig = storageParams.Resources.S3Bucket.Properties.NotificationConfiguration && diff --git a/packages/amplify-util-mock/src/utils/ddb-utils.ts b/packages/amplify-util-mock/src/utils/ddb-utils.ts index f19d7ae6f5..7ff872e27c 100644 --- a/packages/amplify-util-mock/src/utils/ddb-utils.ts +++ b/packages/amplify-util-mock/src/utils/ddb-utils.ts @@ -19,16 +19,16 @@ export function configureDDBDataSource(config, ddbConfig) { if (d.type !== 'AMAZON_DYNAMODB') { return d; } - return { - ...d, - config: { - ...d.config, - endpoint: ddbConfig.endpoint, - region: ddbConfig.region, - accessKeyId: ddbConfig.accessKeyId, - secretAccessKey: ddbConfig.secretAccessKey, - }, - }; + return { + ...d, + config: { + ...d.config, + endpoint: ddbConfig.endpoint, + region: ddbConfig.region, + accessKeyId: ddbConfig.accessKeyId, + secretAccessKey: ddbConfig.secretAccessKey, + }, + }; }), }; } diff --git a/packages/amplify-util-mock/src/utils/lambda/execute.ts b/packages/amplify-util-mock/src/utils/lambda/execute.ts index 9192eedb02..54e909d529 100644 --- a/packages/amplify-util-mock/src/utils/lambda/execute.ts +++ b/packages/amplify-util-mock/src/utils/lambda/execute.ts @@ -71,10 +71,7 @@ function invokeFunction(options: InvokeOptions) { try { if (!lambda[options.handler]) { context.fail( - `handler ${options.handler} does not exist in the lambda function ${path.join( - options.packageFolder, - options.fileName - )}` + `handler ${options.handler} does not exist in the lambda function ${path.join(options.packageFolder, options.fileName)}` ); return; } diff --git a/packages/amplify-util-mock/src/utils/lambda/invoke.ts b/packages/amplify-util-mock/src/utils/lambda/invoke.ts index ae021931be..0c9f1e30ba 100644 --- a/packages/amplify-util-mock/src/utils/lambda/invoke.ts +++ b/packages/amplify-util-mock/src/utils/lambda/invoke.ts @@ -2,23 +2,23 @@ import { fork } from 'child_process'; import * as path from 'path'; export function invoke(options) { - return new Promise((resolve, reject) => { - try { - // XXX: Make the path work in both e2e and - const lambdaFn = fork(path.join(__dirname, '../../../lib/utils/lambda', 'execute.js'), [], { - execArgv: [], - env: options.environment || {} - }); - lambdaFn.on('message', msg => { - const result = JSON.parse(msg); - if (result.error) { - reject(result.error); - } - resolve(result.result); - }); - lambdaFn.send(JSON.stringify(options)); - } catch (e) { - reject(e); + return new Promise((resolve, reject) => { + try { + // XXX: Make the path work in both e2e and + const lambdaFn = fork(path.join(__dirname, '../../../lib/utils/lambda', 'execute.js'), [], { + execArgv: [], + env: options.environment || {}, + }); + lambdaFn.on('message', msg => { + const result = JSON.parse(msg); + if (result.error) { + reject(result.error); } - }); + resolve(result.result); + }); + lambdaFn.send(JSON.stringify(options)); + } catch (e) { + reject(e); + } + }); } diff --git a/packages/amplify-util-mock/src/utils/lambda/load.ts b/packages/amplify-util-mock/src/utils/lambda/load.ts index 6df3df8695..c142570e6c 100644 --- a/packages/amplify-util-mock/src/utils/lambda/load.ts +++ b/packages/amplify-util-mock/src/utils/lambda/load.ts @@ -17,14 +17,8 @@ export function getAllLambdaFunctions(context, backendPath: string): LambdaFunct const cfnParams = path.join(lambdaDir, 'function-parameters.json'); try { const lambdaCfn = JSON.parse(fs.readFileSync(cfnPath, 'utf-8')); - const lambdaCfnParams = fs.existsSync(cfnParams) - ? JSON.parse(fs.readFileSync(cfnParams, 'utf-8')) - : {}; - const lambdaConfig = processResources( - lambdaCfn.Resources, - {}, - { ...lambdaCfnParams, env: 'NONE' } - ); + const lambdaCfnParams = fs.existsSync(cfnParams) ? JSON.parse(fs.readFileSync(cfnParams, 'utf-8')) : {}; + const lambdaConfig = processResources(lambdaCfn.Resources, {}, { ...lambdaCfnParams, env: 'NONE' }); lambdaConfig.basePath = path.join(lambdaDir, 'src'); lambdas.push(lambdaConfig); } catch (e) { diff --git a/packages/amplify-velocity-template/bin/build-tpl.js b/packages/amplify-velocity-template/bin/build-tpl.js index 1b912d1141..461186312c 100644 --- a/packages/amplify-velocity-template/bin/build-tpl.js +++ b/packages/amplify-velocity-template/bin/build-tpl.js @@ -1 +1 @@ -module.exports = {content}; +module.exports = { content }; diff --git a/packages/amplify-velocity-template/bin/velocity-cli.js b/packages/amplify-velocity-template/bin/velocity-cli.js index 6099093932..633b4069e5 100644 --- a/packages/amplify-velocity-template/bin/velocity-cli.js +++ b/packages/amplify-velocity-template/bin/velocity-cli.js @@ -1,5 +1,5 @@ -var path = require('path'); -var fs = require('fs'); +var path = require('path'); +var fs = require('fs'); var exists = fs.existsSync || path.existsSync; var currentPath = process.cwd(); @@ -9,13 +9,13 @@ var parse = Velocity.parse; var Structure = Velocity.Helper.Structure; var Jsonify = Velocity.Helper.Jsonify; -function escapeIt(buf){ - var str = Buffer.isBuffer(buf) ? buf.toString(): buf; +function escapeIt(buf) { + var str = Buffer.isBuffer(buf) ? buf.toString() : buf; var len = str.length; var ret = ''; for (var i = 0; i < len; i++) { var bit = str.charCodeAt(i); - if (bit < 0 || bit > 255){ + if (bit < 0 || bit > 255) { ret += '\\u' + bit.toString(16); } else { ret += str[i]; @@ -25,12 +25,10 @@ function escapeIt(buf){ return ret; } -function buildAst(files, prefix){ +function buildAst(files, prefix) { var _template = fs.readFileSync(__dirname + '/build-tpl.js').toString(); - files.forEach(function(file){ - + files.forEach(function(file) { if (path.extname(file) === '.vm') { - console.log('read file ' + file); var template = _template; @@ -42,14 +40,11 @@ function buildAst(files, prefix){ console.log('read js ' + file); fs.writeFileSync(currentPath + '/' + file.replace('.vm', '.js'), template); - } - }); } -function parseVelocity(argv){ - +function parseVelocity(argv) { var vmfile = argv[0]; if (vmfile && vmfile.indexOf('.vm')) { vmfile = path.resolve(process.cwd(), vmfile); @@ -65,7 +60,7 @@ function parseVelocity(argv){ if (ext === '.json') { if (exists(dataFile)) data = fs.readFileSync(dataFile).toString(); - } else if(ext ==='.js') { + } else if (ext === '.js') { if (exists(dataFile)) data = require(dataFile); } @@ -77,7 +72,7 @@ function parseVelocity(argv){ } } -function showHelp(){ +function showHelp() { console.log(fs.readFileSync(__dirname + '/help.txt').toString()); } @@ -86,8 +81,7 @@ function showVersion() { console.log('v' + JSON.parse(data).version); } -function jsonify(file){ - +function jsonify(file) { file = process.cwd() + '/' + file; var pwd = process.cwd() + '/'; @@ -107,14 +101,13 @@ function jsonify(file){ } } -function getVTL(file, context, macros){ +function getVTL(file, context, macros) { var asts = Parser.parse(fs.readFileSync(file).toString()); var makeup = new Jsonify(asts, context, macros); return makeup.toVTL(); } -function showMakeup(file){ - +function showMakeup(file) { file = process.cwd() + '/' + file; if (!fs.existsSync(file)) { @@ -126,7 +119,6 @@ function showMakeup(file){ var makeup = new Structure(asts); console.log(makeup.context); //console.log(JSON.stringify(makeup.context, true, 2)); - } module.exports = { @@ -135,5 +127,5 @@ module.exports = { showHelp: showHelp, showVersion: showVersion, showMakeup: showMakeup, - jsonify: jsonify + jsonify: jsonify, }; diff --git a/packages/amplify-velocity-template/examples/app.js b/packages/amplify-velocity-template/examples/app.js index b98f860cc1..c15692b614 100644 --- a/packages/amplify-velocity-template/examples/app.js +++ b/packages/amplify-velocity-template/examples/app.js @@ -2,8 +2,8 @@ import { parse, Compile } from 'velocityjs'; const asts = parse(document.querySelector('#tmpl').innerHTML); const data = { - items: [{a:'1'},{a: 'successed'}] + items: [{ a: '1' }, { a: 'successed' }], }; -const s = (new Compile(asts)).render(data); +const s = new Compile(asts).render(data); document.querySelector('.foo').innerHTML = s; diff --git a/packages/amplify-velocity-template/examples/test.js b/packages/amplify-velocity-template/examples/test.js index 92614b988c..771dff8016 100644 --- a/packages/amplify-velocity-template/examples/test.js +++ b/packages/amplify-velocity-template/examples/test.js @@ -19,9 +19,9 @@ null #end }`; -var vm = '#foreach($product in $products)$product.name#if($foreach.hasNext()),#end#end' -var data = {products: {product1: {name: "hanwen1"}, product2: {name: "hanwen2"}, product3: {name: "hanwen3"}}}; -const ast = Velocity.parse(vm) +var vm = '#foreach($product in $products)$product.name#if($foreach.hasNext()),#end#end'; +var data = { products: { product1: { name: 'hanwen1' }, product2: { name: 'hanwen2' }, product3: { name: 'hanwen3' } } }; +const ast = Velocity.parse(vm); const compiler = new Velocity.Compile(ast); const result = compiler.render(data); -console.log(result); \ No newline at end of file +console.log(result); diff --git a/packages/amplify-velocity-template/examples/webpack.config.js b/packages/amplify-velocity-template/examples/webpack.config.js index c12a464542..1968e0d66a 100644 --- a/packages/amplify-velocity-template/examples/webpack.config.js +++ b/packages/amplify-velocity-template/examples/webpack.config.js @@ -1,7 +1,6 @@ module.exports = { entry: './app.js', output: { - filename: 'bundle.js' - } + filename: 'bundle.js', + }, }; - diff --git a/packages/amplify-velocity-template/src/parse/index.js b/packages/amplify-velocity-template/src/parse/index.js index e754a71f1d..3df5fbf38e 100644 --- a/packages/amplify-velocity-template/src/parse/index.js +++ b/packages/amplify-velocity-template/src/parse/index.js @@ -71,389 +71,1472 @@ recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) } */ -var index = (function(){ -var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,8],$V1=[1,9],$V2=[1,20],$V3=[1,10],$V4=[1,25],$V5=[1,26],$V6=[1,24],$V7=[4,10,11,21,36,37,48,84],$V8=[1,30],$V9=[1,34],$Va=[1,33],$Vb=[4,10,11,21,24,36,37,40,48,51,52,53,56,57,58,59,60,61,62,63,64,65,66,67,68,84,86,96],$Vc=[1,52],$Vd=[1,57],$Ve=[1,58],$Vf=[1,75],$Vg=[1,74],$Vh=[1,87],$Vi=[1,82],$Vj=[1,90],$Vk=[1,98],$Vl=[1,88],$Vm=[1,93],$Vn=[1,97],$Vo=[1,94],$Vp=[1,95],$Vq=[4,10,11,21,24,36,37,40,48,51,52,53,56,57,58,59,60,61,62,63,64,65,66,67,68,77,82,84,85,86,96],$Vr=[1,110],$Vs=[1,124],$Vt=[1,120],$Vu=[1,121],$Vv=[1,135],$Vw=[24,52,86],$Vx=[2,99],$Vy=[24,40,51,52,86],$Vz=[24,40,51,52,56,57,58,59,60,61,62,63,64,65,66,67,68,84,86],$VA=[24,40,51,52,56,57,58,59,60,61,62,63,64,65,66,67,68,84,86,98],$VB=[2,112],$VC=[24,40,51,52,56,57,58,59,60,61,62,63,64,65,66,67,68,84,86,96],$VD=[2,115],$VE=[1,144],$VF=[1,150],$VG=[24,51,52],$VH=[1,155],$VI=[1,156],$VJ=[1,157],$VK=[1,158],$VL=[1,159],$VM=[1,160],$VN=[1,161],$VO=[1,162],$VP=[1,163],$VQ=[1,164],$VR=[1,165],$VS=[1,166],$VT=[1,167],$VU=[24,56,57,58,59,60,61,62,63,64,65,66,67,68],$VV=[52,86],$VW=[2,116],$VX=[24,36],$VY=[1,218],$VZ=[1,217],$V_=[40,52],$V$=[24,56,57],$V01=[24,56,57,58,59,63,64,65,66,67,68],$V11=[24,56,57,63,64,65,66,67,68]; -var parser = {trace: function trace () { }, -yy: {}, -symbols_: {"error":2,"root":3,"EOF":4,"statements":5,"statement":6,"references":7,"directives":8,"content":9,"RAW":10,"COMMENT":11,"set":12,"return":13,"if":14,"elseif":15,"else":16,"end":17,"foreach":18,"break":19,"define":20,"HASH":21,"NOESCAPE":22,"PARENTHESIS":23,"CLOSE_PARENTHESIS":24,"macro":25,"macro_call":26,"macro_body":27,"SET":28,"equal":29,"IF":30,"expression":31,"ELSEIF":32,"ELSE":33,"END":34,"FOREACH":35,"DOLLAR":36,"ID":37,"IN":38,"MAP_BEGIN":39,"MAP_END":40,"array":41,"BREAK":42,"RETURN":43,"DEFINE":44,"MACRO":45,"macro_args":46,"macro_call_args_all":47,"MACRO_BODY":48,"macro_call_args":49,"literals":50,"SPACE":51,"COMMA":52,"EQUAL":53,"map":54,"math":55,"||":56,"&&":57,"+":58,"-":59,"*":60,"/":61,"%":62,">":63,"<":64,"==":65,">=":66,"<=":67,"!=":68,"parenthesis":69,"!":70,"literal":71,"brace_begin":72,"attributes":73,"brace_end":74,"methodbd":75,"VAR_BEGIN":76,"VAR_END":77,"attribute":78,"method":79,"index":80,"property":81,"DOT":82,"params":83,"CONTENT":84,"BRACKET":85,"CLOSE_BRACKET":86,"string":87,"number":88,"BOOL":89,"integer":90,"INTEGER":91,"DECIMAL_POINT":92,"STRING":93,"EVAL_STRING":94,"range":95,"RANGE":96,"map_item":97,"MAP_SPLIT":98,"$accept":0,"$end":1}, -terminals_: {2:"error",4:"EOF",10:"RAW",11:"COMMENT",21:"HASH",22:"NOESCAPE",23:"PARENTHESIS",24:"CLOSE_PARENTHESIS",28:"SET",30:"IF",32:"ELSEIF",33:"ELSE",34:"END",35:"FOREACH",36:"DOLLAR",37:"ID",38:"IN",39:"MAP_BEGIN",40:"MAP_END",42:"BREAK",43:"RETURN",44:"DEFINE",45:"MACRO",48:"MACRO_BODY",51:"SPACE",52:"COMMA",53:"EQUAL",56:"||",57:"&&",58:"+",59:"-",60:"*",61:"/",62:"%",63:">",64:"<",65:"==",66:">=",67:"<=",68:"!=",70:"!",76:"VAR_BEGIN",77:"VAR_END",82:"DOT",84:"CONTENT",85:"BRACKET",86:"CLOSE_BRACKET",89:"BOOL",91:"INTEGER",92:"DECIMAL_POINT",93:"STRING",94:"EVAL_STRING",96:"RANGE",98:"MAP_SPLIT"}, -productions_: [0,[3,1],[3,2],[5,1],[5,2],[6,1],[6,1],[6,1],[6,1],[6,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,4],[8,1],[8,1],[8,1],[12,5],[14,5],[15,5],[16,2],[17,2],[18,8],[18,10],[18,8],[18,10],[19,2],[13,5],[13,2],[20,6],[25,6],[25,5],[46,1],[46,2],[26,5],[26,4],[27,5],[27,4],[49,1],[49,1],[49,3],[49,3],[49,3],[49,3],[47,1],[47,2],[47,3],[47,2],[29,3],[31,1],[31,1],[31,1],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,3],[55,1],[55,2],[55,2],[55,1],[55,1],[69,3],[7,5],[7,3],[7,5],[7,3],[7,2],[7,4],[7,2],[7,4],[72,1],[72,1],[74,1],[74,1],[73,1],[73,2],[78,1],[78,1],[78,1],[79,2],[75,4],[75,3],[83,1],[83,1],[83,1],[83,3],[83,3],[81,2],[81,2],[80,3],[80,3],[80,3],[80,2],[80,2],[71,1],[71,1],[71,1],[88,1],[88,3],[88,4],[90,1],[90,2],[87,1],[87,1],[50,1],[50,1],[50,1],[41,3],[41,1],[41,2],[95,5],[95,5],[95,5],[95,5],[54,3],[54,2],[97,3],[97,3],[97,2],[97,5],[97,5],[9,1],[9,1],[9,2],[9,3],[9,3],[9,2]], -performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { -/* this == yyval */ +var index = (function() { + var o = function(k, v, o, l) { + for (o = o || {}, l = k.length; l--; o[k[l]] = v); + return o; + }, + $V0 = [1, 8], + $V1 = [1, 9], + $V2 = [1, 20], + $V3 = [1, 10], + $V4 = [1, 25], + $V5 = [1, 26], + $V6 = [1, 24], + $V7 = [4, 10, 11, 21, 36, 37, 48, 84], + $V8 = [1, 30], + $V9 = [1, 34], + $Va = [1, 33], + $Vb = [4, 10, 11, 21, 24, 36, 37, 40, 48, 51, 52, 53, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 84, 86, 96], + $Vc = [1, 52], + $Vd = [1, 57], + $Ve = [1, 58], + $Vf = [1, 75], + $Vg = [1, 74], + $Vh = [1, 87], + $Vi = [1, 82], + $Vj = [1, 90], + $Vk = [1, 98], + $Vl = [1, 88], + $Vm = [1, 93], + $Vn = [1, 97], + $Vo = [1, 94], + $Vp = [1, 95], + $Vq = [4, 10, 11, 21, 24, 36, 37, 40, 48, 51, 52, 53, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 77, 82, 84, 85, 86, 96], + $Vr = [1, 110], + $Vs = [1, 124], + $Vt = [1, 120], + $Vu = [1, 121], + $Vv = [1, 135], + $Vw = [24, 52, 86], + $Vx = [2, 99], + $Vy = [24, 40, 51, 52, 86], + $Vz = [24, 40, 51, 52, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 84, 86], + $VA = [24, 40, 51, 52, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 84, 86, 98], + $VB = [2, 112], + $VC = [24, 40, 51, 52, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 84, 86, 96], + $VD = [2, 115], + $VE = [1, 144], + $VF = [1, 150], + $VG = [24, 51, 52], + $VH = [1, 155], + $VI = [1, 156], + $VJ = [1, 157], + $VK = [1, 158], + $VL = [1, 159], + $VM = [1, 160], + $VN = [1, 161], + $VO = [1, 162], + $VP = [1, 163], + $VQ = [1, 164], + $VR = [1, 165], + $VS = [1, 166], + $VT = [1, 167], + $VU = [24, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68], + $VV = [52, 86], + $VW = [2, 116], + $VX = [24, 36], + $VY = [1, 218], + $VZ = [1, 217], + $V_ = [40, 52], + $V$ = [24, 56, 57], + $V01 = [24, 56, 57, 58, 59, 63, 64, 65, 66, 67, 68], + $V11 = [24, 56, 57, 63, 64, 65, 66, 67, 68]; + var parser = { + trace: function trace() {}, + yy: {}, + symbols_: { + error: 2, + root: 3, + EOF: 4, + statements: 5, + statement: 6, + references: 7, + directives: 8, + content: 9, + RAW: 10, + COMMENT: 11, + set: 12, + return: 13, + if: 14, + elseif: 15, + else: 16, + end: 17, + foreach: 18, + break: 19, + define: 20, + HASH: 21, + NOESCAPE: 22, + PARENTHESIS: 23, + CLOSE_PARENTHESIS: 24, + macro: 25, + macro_call: 26, + macro_body: 27, + SET: 28, + equal: 29, + IF: 30, + expression: 31, + ELSEIF: 32, + ELSE: 33, + END: 34, + FOREACH: 35, + DOLLAR: 36, + ID: 37, + IN: 38, + MAP_BEGIN: 39, + MAP_END: 40, + array: 41, + BREAK: 42, + RETURN: 43, + DEFINE: 44, + MACRO: 45, + macro_args: 46, + macro_call_args_all: 47, + MACRO_BODY: 48, + macro_call_args: 49, + literals: 50, + SPACE: 51, + COMMA: 52, + EQUAL: 53, + map: 54, + math: 55, + '||': 56, + '&&': 57, + '+': 58, + '-': 59, + '*': 60, + '/': 61, + '%': 62, + '>': 63, + '<': 64, + '==': 65, + '>=': 66, + '<=': 67, + '!=': 68, + parenthesis: 69, + '!': 70, + literal: 71, + brace_begin: 72, + attributes: 73, + brace_end: 74, + methodbd: 75, + VAR_BEGIN: 76, + VAR_END: 77, + attribute: 78, + method: 79, + index: 80, + property: 81, + DOT: 82, + params: 83, + CONTENT: 84, + BRACKET: 85, + CLOSE_BRACKET: 86, + string: 87, + number: 88, + BOOL: 89, + integer: 90, + INTEGER: 91, + DECIMAL_POINT: 92, + STRING: 93, + EVAL_STRING: 94, + range: 95, + RANGE: 96, + map_item: 97, + MAP_SPLIT: 98, + $accept: 0, + $end: 1, + }, + terminals_: { + 2: 'error', + 4: 'EOF', + 10: 'RAW', + 11: 'COMMENT', + 21: 'HASH', + 22: 'NOESCAPE', + 23: 'PARENTHESIS', + 24: 'CLOSE_PARENTHESIS', + 28: 'SET', + 30: 'IF', + 32: 'ELSEIF', + 33: 'ELSE', + 34: 'END', + 35: 'FOREACH', + 36: 'DOLLAR', + 37: 'ID', + 38: 'IN', + 39: 'MAP_BEGIN', + 40: 'MAP_END', + 42: 'BREAK', + 43: 'RETURN', + 44: 'DEFINE', + 45: 'MACRO', + 48: 'MACRO_BODY', + 51: 'SPACE', + 52: 'COMMA', + 53: 'EQUAL', + 56: '||', + 57: '&&', + 58: '+', + 59: '-', + 60: '*', + 61: '/', + 62: '%', + 63: '>', + 64: '<', + 65: '==', + 66: '>=', + 67: '<=', + 68: '!=', + 70: '!', + 76: 'VAR_BEGIN', + 77: 'VAR_END', + 82: 'DOT', + 84: 'CONTENT', + 85: 'BRACKET', + 86: 'CLOSE_BRACKET', + 89: 'BOOL', + 91: 'INTEGER', + 92: 'DECIMAL_POINT', + 93: 'STRING', + 94: 'EVAL_STRING', + 96: 'RANGE', + 98: 'MAP_SPLIT', + }, + productions_: [ + 0, + [3, 1], + [3, 2], + [5, 1], + [5, 2], + [6, 1], + [6, 1], + [6, 1], + [6, 1], + [6, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 4], + [8, 1], + [8, 1], + [8, 1], + [12, 5], + [14, 5], + [15, 5], + [16, 2], + [17, 2], + [18, 8], + [18, 10], + [18, 8], + [18, 10], + [19, 2], + [13, 5], + [13, 2], + [20, 6], + [25, 6], + [25, 5], + [46, 1], + [46, 2], + [26, 5], + [26, 4], + [27, 5], + [27, 4], + [49, 1], + [49, 1], + [49, 3], + [49, 3], + [49, 3], + [49, 3], + [47, 1], + [47, 2], + [47, 3], + [47, 2], + [29, 3], + [31, 1], + [31, 1], + [31, 1], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 3], + [55, 1], + [55, 2], + [55, 2], + [55, 1], + [55, 1], + [69, 3], + [7, 5], + [7, 3], + [7, 5], + [7, 3], + [7, 2], + [7, 4], + [7, 2], + [7, 4], + [72, 1], + [72, 1], + [74, 1], + [74, 1], + [73, 1], + [73, 2], + [78, 1], + [78, 1], + [78, 1], + [79, 2], + [75, 4], + [75, 3], + [83, 1], + [83, 1], + [83, 1], + [83, 3], + [83, 3], + [81, 2], + [81, 2], + [80, 3], + [80, 3], + [80, 3], + [80, 2], + [80, 2], + [71, 1], + [71, 1], + [71, 1], + [88, 1], + [88, 3], + [88, 4], + [90, 1], + [90, 2], + [87, 1], + [87, 1], + [50, 1], + [50, 1], + [50, 1], + [41, 3], + [41, 1], + [41, 2], + [95, 5], + [95, 5], + [95, 5], + [95, 5], + [54, 3], + [54, 2], + [97, 3], + [97, 3], + [97, 2], + [97, 5], + [97, 5], + [9, 1], + [9, 1], + [9, 2], + [9, 3], + [9, 3], + [9, 2], + ], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { + /* this == yyval */ -var $0 = $$.length - 1; -switch (yystate) { -case 1: - return []; -break; -case 2: - return $$[$0-1]; -break; -case 3: case 38: case 44: case 45: case 89: case 97: case 99: - this.$ = [$$[$0]]; -break; -case 4: case 39: case 90: - this.$ = [].concat($$[$0-1], $$[$0]); -break; -case 5: - $$[$0]['prue'] = true; $$[$0].pos = this._$; this.$ = $$[$0]; -break; -case 6: - $$[$0].pos = this._$; this.$ = $$[$0]; -break; -case 7: case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 20: case 21: case 22: case 50: case 51: case 55: case 56: case 57: case 71: case 74: case 75: case 85: case 86: case 87: case 88: case 94: case 102: case 109: case 110: case 115: case 121: case 123: case 136: case 137: - this.$ = $$[$0]; -break; -case 8: - this.$ = {type: 'raw', value: $$[$0] }; -break; -case 9: - this.$ = {type: 'comment', value: $$[$0] }; -break; -case 19: - this.$ = { type: 'noescape' }; -break; -case 23: - this.$ = {type: 'set', equal: $$[$0-1] }; -break; -case 24: - this.$ = {type: 'if', condition: $$[$0-1] }; -break; -case 25: - this.$ = {type: 'elseif', condition: $$[$0-1] }; -break; -case 26: - this.$ = {type: 'else' }; -break; -case 27: - this.$ = {type: 'end' }; -break; -case 28: case 30: - this.$ = {type: 'foreach', to: $$[$0-3], from: $$[$0-1] }; -break; -case 29: case 31: - this.$ = {type: 'foreach', to: $$[$0-4], from: $$[$0-1] }; -break; -case 32: - this.$ = {type: $$[$0] }; -break; -case 33: - this.$ = {type: 'return', value: $$[$0-1] }; -break; -case 34: - this.$ = {type: 'return', value: null }; -break; -case 35: - this.$ = {type: 'define', id: $$[$0-1] }; -break; -case 36: - this.$ = {type: 'macro', id: $$[$0-2], args: $$[$0-1] }; -break; -case 37: - this.$ = {type: 'macro', id: $$[$0-1] }; -break; -case 40: - this.$ = { type:"macro_call", id: $$[$0-3].replace(/^\s+|\s+$/g, ''), args: $$[$0-1] }; -break; -case 41: - this.$ = { type:"macro_call", id: $$[$0-2].replace(/^\s+|\s+$/g, '') }; -break; -case 42: - this.$ = {type: 'macro_body', id: $$[$0-3], args: $$[$0-1] }; -break; -case 43: - this.$ = {type: 'macro_body', id: $$[$0-2] }; -break; -case 46: case 47: case 48: case 49: case 100: case 101: - this.$ = [].concat($$[$0-2], $$[$0]); -break; -case 52: case 53: case 104: case 105: - this.$ = $$[$0-1]; -break; -case 54: - this.$ = [$$[$0-2], $$[$0]]; -break; -case 58: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '||' }; -break; -case 59: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '&&' }; -break; -case 60: case 61: case 62: case 63: case 64: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: $$[$0-1] }; -break; -case 65: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '>' }; -break; -case 66: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '<' }; -break; -case 67: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '==' }; -break; -case 68: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '>=' }; -break; -case 69: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '<=' }; -break; -case 70: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: '!=' }; -break; -case 72: - this.$ = {type: 'math', expression: [$$[$0]], operator: 'minus' }; -break; -case 73: - this.$ = {type: 'math', expression: [$$[$0]], operator: 'not' }; -break; -case 76: - this.$ = {type: 'math', expression: [$$[$0-1]], operator: 'parenthesis' }; -break; -case 77: - this.$ = {type: "references", id: $$[$0-2], path: $$[$0-1], isWraped: true, leader: $$[$0-4] }; -break; -case 78: - this.$ = {type: "references", id: $$[$0-1], path: $$[$0], leader: $$[$0-2] }; -break; -case 79: - this.$ = {type: "references", id: $$[$0-2].id, path: $$[$0-1], isWraped: true, leader: $$[$0-4], args: $$[$0-2].args }; -break; -case 80: - this.$ = {type: "references", id: $$[$0-1].id, path: $$[$0], leader: $$[$0-2], args: $$[$0-1].args }; -break; -case 81: - this.$ = {type: "references", id: $$[$0], leader: $$[$0-1] }; -break; -case 82: - this.$ = {type: "references", id: $$[$0-1], isWraped: true, leader: $$[$0-3] }; -break; -case 83: - this.$ = {type: "references", id: $$[$0].id, leader: $$[$0-1], args: $$[$0].args }; -break; -case 84: - this.$ = {type: "references", id: $$[$0-1].id, isWraped: true, args: $$[$0-1].args, leader: $$[$0-3] }; -break; -case 91: - this.$ = {type:"method", id: $$[$0].id, args: $$[$0].args }; -break; -case 92: - this.$ = {type: "index", id: $$[$0] }; -break; -case 93: - this.$ = {type: "property", id: $$[$0] }; if ($$[$0].type === 'content') this.$ = $$[$0]; -break; -case 95: - this.$ = {id: $$[$0-3], args: $$[$0-1] }; -break; -case 96: - this.$ = {id: $$[$0-2], args: false }; -break; -case 98: - this.$ = [ { type: 'runt', value: $$[$0] } ]; -break; -case 103: - this.$ = {type: 'content', value: $$[$0-1] + $$[$0] }; -break; -case 106: - this.$ = {type: "content", value: $$[$0-2] + $$[$0-1].value + $$[$0] }; -break; -case 107: case 108: - this.$ = {type: "content", value: $$[$0-1] + $$[$0] }; -break; -case 111: - this.$ = {type: 'bool', value: $$[$0] }; -break; -case 112: - this.$ = {type: "integer", value: $$[$0]}; -break; -case 113: - this.$ = {type: "decimal", value: + ($$[$0-2] + '.' + $$[$0]) }; -break; -case 114: - this.$ = {type: "decimal", value: - ($$[$0-2] + '.' + $$[$0]) }; -break; -case 116: - this.$ = - parseInt($$[$0], 10); -break; -case 117: - this.$ = {type: 'string', value: $$[$0] }; -break; -case 118: - this.$ = {type: 'string', value: $$[$0], isEval: true }; -break; -case 119: case 120: - this.$ = $$[$0]; -break; -case 122: - this.$ = {type: 'array', value: $$[$0-1] }; -break; -case 124: - this.$ = {type: 'array', value: [] }; -break; -case 125: case 126: case 127: case 128: - this.$ = {type: 'array', isRange: true, value: [$$[$0-3], $$[$0-1]]}; -break; -case 129: - this.$ = {type: 'map', value: $$[$0-1] }; -break; -case 130: - this.$ = {type: 'map'}; -break; -case 131: case 132: - this.$ = {}; this.$[$$[$0-2].value] = $$[$0]; -break; -case 133: - this.$ = {}; this.$[$$[$0-1].value] = $$[$01]; -break; -case 134: case 135: - this.$ = $$[$0-4]; this.$[$$[$0-2].value] = $$[$0]; -break; -case 138: case 141: - this.$ = $$[$0-1] + $$[$0]; -break; -case 139: - this.$ = $$[$0-2] + $$[$0-1] + $$[$0]; -break; -case 140: - this.$ = $$[$0-2] + $$[$0-1]; -break; -} -}, -table: [{3:1,4:[1,2],5:3,6:4,7:5,8:6,9:7,10:$V0,11:$V1,12:11,13:12,14:13,15:14,16:15,17:16,18:17,19:18,20:19,21:$V2,25:21,26:22,27:23,36:$V3,37:$V4,48:$V5,84:$V6},{1:[3]},{1:[2,1]},{4:[1,27],6:28,7:5,8:6,9:7,10:$V0,11:$V1,12:11,13:12,14:13,15:14,16:15,17:16,18:17,19:18,20:19,21:$V2,25:21,26:22,27:23,36:$V3,37:$V4,48:$V5,84:$V6},o($V7,[2,3]),o($V7,[2,5]),o($V7,[2,6]),o($V7,[2,7]),o($V7,[2,8]),o($V7,[2,9]),{37:$V8,39:$V9,72:29,75:31,76:$Va,84:[1,32]},o($V7,[2,10]),o($V7,[2,11]),o($V7,[2,12]),o($V7,[2,13]),o($V7,[2,14]),o($V7,[2,15]),o($V7,[2,16]),o($V7,[2,17]),o($V7,[2,18]),{22:[1,35],28:[1,38],30:[1,40],32:[1,41],33:[1,42],34:[1,43],35:[1,44],37:[1,37],42:[1,45],43:[1,39],44:[1,46],45:[1,47],84:[1,36]},o($V7,[2,20]),o($V7,[2,21]),o($V7,[2,22]),o($V7,[2,136]),o($V7,[2,137]),{37:[1,48]},{1:[2,2]},o($V7,[2,4]),{37:[1,49],75:50},o($Vb,[2,81],{73:51,78:53,79:54,80:55,81:56,23:$Vc,82:$Vd,85:$Ve}),o($Vb,[2,83],{78:53,79:54,80:55,81:56,73:59,82:$Vd,85:$Ve}),o($V7,[2,141]),{37:[2,85]},{37:[2,86]},{23:[1,60]},o($V7,[2,138]),{4:[1,62],23:[1,63],84:[1,61]},{23:[1,64]},o($V7,[2,34],{23:[1,65]}),{23:[1,66]},{23:[1,67]},o($V7,[2,26]),o($V7,[2,27]),{23:[1,68]},o($V7,[2,32]),{23:[1,69]},{23:[1,70]},{23:[1,71]},{23:$Vc,40:$Vf,73:72,74:73,77:$Vg,78:53,79:54,80:55,81:56,82:$Vd,85:$Ve},{40:$Vf,73:76,74:77,77:$Vg,78:53,79:54,80:55,81:56,82:$Vd,85:$Ve},o($Vb,[2,78],{79:54,80:55,81:56,78:78,82:$Vd,85:$Ve}),{7:83,24:[1,80],36:$Vh,37:$Vi,39:$Vj,41:84,50:81,54:85,59:$Vk,71:86,83:79,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},o($Vq,[2,89]),o($Vq,[2,91]),o($Vq,[2,92]),o($Vq,[2,93]),{37:[1,100],75:99,84:[1,101]},{7:103,36:$Vh,59:$Vk,71:102,84:[1,104],86:[1,105],87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},o($Vb,[2,80],{79:54,80:55,81:56,78:78,82:$Vd,85:$Ve}),{24:[1,106]},o($V7,[2,139]),o($V7,[2,140]),{7:112,24:[1,108],36:$Vh,39:$Vj,41:84,47:107,49:109,50:111,51:$Vr,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{7:114,29:113,36:$Vh},{7:122,23:$Vs,31:115,36:$Vh,39:$Vj,41:116,54:117,55:118,59:$Vt,69:119,70:$Vu,71:123,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{7:122,23:$Vs,31:125,36:$Vh,39:$Vj,41:116,54:117,55:118,59:$Vt,69:119,70:$Vu,71:123,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{7:122,23:$Vs,31:126,36:$Vh,39:$Vj,41:116,54:117,55:118,59:$Vt,69:119,70:$Vu,71:123,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{36:[1,127]},{36:[1,128]},{37:[1,129]},{7:112,24:[1,131],36:$Vh,39:$Vj,41:84,47:130,49:109,50:111,51:$Vr,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{40:$Vf,74:132,77:$Vg,78:78,79:54,80:55,81:56,82:$Vd,85:$Ve},o($Vb,[2,82]),o($Vb,[2,87]),o($Vb,[2,88]),{40:$Vf,74:133,77:$Vg,78:78,79:54,80:55,81:56,82:$Vd,85:$Ve},o($Vb,[2,84]),o($Vq,[2,90]),{24:[1,134],52:$Vv},o($Vq,[2,96]),o($Vw,[2,97]),o($Vw,[2,98]),o([24,52],$Vx),o($Vy,[2,119]),o($Vy,[2,120]),o($Vy,[2,121]),{37:$V8,39:$V9,72:29,75:31,76:$Va},{7:139,36:$Vh,37:$Vi,39:$Vj,41:84,50:81,54:85,59:$Vk,71:86,83:136,85:$Vl,86:[1,137],87:91,88:92,89:$Vm,90:138,91:$Vn,93:$Vo,94:$Vp,95:89},o($Vy,[2,123]),{40:[1,141],87:142,93:$Vo,94:$Vp,97:140},o($Vz,[2,109]),o($Vz,[2,110]),o($Vz,[2,111]),o($VA,[2,117]),o($VA,[2,118]),o($Vz,$VB),o($VC,$VD,{92:[1,143]}),{91:$VE},o($Vq,[2,94]),o($Vq,[2,102],{23:$Vc}),o($Vq,[2,103]),{84:[1,146],86:[1,145]},{86:[1,147]},o($Vq,[2,107]),o($Vq,[2,108]),o($V7,[2,19]),{24:[1,148]},o($V7,[2,41]),{24:[2,50],51:[1,149],52:$VF},{7:112,36:$Vh,39:$Vj,41:84,49:151,50:111,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},o($VG,[2,44]),o($VG,[2,45]),{24:[1,152]},{53:[1,153]},{24:[1,154]},{24:[2,55]},{24:[2,56]},{24:[2,57],56:$VH,57:$VI,58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN,63:$VO,64:$VP,65:$VQ,66:$VR,67:$VS,68:$VT},o($VU,[2,71]),{23:$Vs,69:168,91:$VE},{7:122,23:$Vs,36:$Vh,55:169,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},o($VU,[2,74]),o($VU,[2,75]),{7:122,23:$Vs,36:$Vh,55:170,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{24:[1,171]},{24:[1,172]},{37:[1,173],39:[1,174]},{37:[1,175]},{7:178,24:[1,177],36:$Vh,46:176},{24:[1,179]},o($V7,[2,43]),o($Vb,[2,77]),o($Vb,[2,79]),o($Vq,[2,95]),{7:181,36:$Vh,39:$Vj,41:84,50:180,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{52:$Vv,86:[1,182]},o($Vy,[2,124]),o($VV,$VB,{96:[1,183]}),o($VV,$Vx,{96:[1,184]}),{40:[1,185],52:[1,186]},o($Vy,[2,130]),{98:[1,187]},{91:[1,188]},o($VC,$VW,{92:[1,189]}),o($Vq,[2,104]),o($Vq,[2,106]),o($Vq,[2,105]),o($V7,[2,40]),{7:191,24:[2,53],36:$Vh,39:$Vj,41:84,50:190,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{7:193,36:$Vh,39:$Vj,41:84,50:192,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{24:[2,51],51:[1,194],52:$VF},o($V7,[2,23]),{7:122,23:$Vs,31:195,36:$Vh,39:$Vj,41:116,54:117,55:118,59:$Vt,69:119,70:$Vu,71:123,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},o($V7,[2,33]),{7:122,23:$Vs,36:$Vh,55:196,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:197,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:198,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:199,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:200,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:201,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:202,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:203,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:204,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:205,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:206,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:207,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},{7:122,23:$Vs,36:$Vh,55:208,59:$Vt,69:119,70:$Vu,71:123,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp},o($VU,[2,72]),o($VU,[2,73]),{24:[1,209],56:$VH,57:$VI,58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN,63:$VO,64:$VP,65:$VQ,66:$VR,67:$VS,68:$VT},o($V7,[2,24]),o($V7,[2,25]),{38:[1,210]},{37:[1,211]},{24:[1,212]},{7:214,24:[1,213],36:$Vh},o($V7,[2,37]),o($VX,[2,38]),o($V7,[2,42]),o($Vw,[2,100]),o($Vw,[2,101]),o($Vy,[2,122]),{7:216,36:$Vh,59:$VY,90:215,91:$VZ},{7:220,36:$Vh,59:$VY,90:219,91:$VZ},o($Vy,[2,129]),{87:221,93:$Vo,94:$Vp},o($V_,[2,133],{41:84,54:85,71:86,95:89,87:91,88:92,90:96,50:222,7:223,36:$Vh,39:$Vj,59:$Vk,85:$Vl,89:$Vm,91:$Vn,93:$Vo,94:$Vp}),o($Vz,[2,113]),{91:[1,224]},o($VG,[2,46]),o($VG,[2,49]),o($VG,[2,47]),o($VG,[2,48]),{7:191,24:[2,52],36:$Vh,39:$Vj,41:84,50:190,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},{24:[2,54]},o($V$,[2,58],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN,63:$VO,64:$VP,65:$VQ,66:$VR,67:$VS,68:$VT}),o($V$,[2,59],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN,63:$VO,64:$VP,65:$VQ,66:$VR,67:$VS,68:$VT}),o($V01,[2,60],{60:$VL,61:$VM,62:$VN}),o($V01,[2,61],{60:$VL,61:$VM,62:$VN}),o($VU,[2,62]),o($VU,[2,63]),o($VU,[2,64]),o($V11,[2,65],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN}),o($V11,[2,66],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN}),o($V11,[2,67],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN}),o($V11,[2,68],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN}),o($V11,[2,69],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN}),o($V11,[2,70],{58:$VJ,59:$VK,60:$VL,61:$VM,62:$VN}),o($VU,[2,76]),{7:225,36:$Vh,41:226,85:$Vl,95:89},{40:[1,227]},o($V7,[2,35]),o($V7,[2,36]),o($VX,[2,39]),{86:[1,228]},{86:[1,229]},{86:$VD},{91:[1,230]},{86:[1,231]},{86:[1,232]},{98:[1,233]},o($V_,[2,131]),o($V_,[2,132]),o($Vz,[2,114]),{24:[1,234]},{24:[1,235]},{38:[1,236]},o($Vy,[2,125]),o($Vy,[2,127]),{86:$VW},o($Vy,[2,126]),o($Vy,[2,128]),{7:237,36:$Vh,39:$Vj,41:84,50:238,54:85,59:$Vk,71:86,85:$Vl,87:91,88:92,89:$Vm,90:96,91:$Vn,93:$Vo,94:$Vp,95:89},o($V7,[2,28]),o($V7,[2,30]),{7:239,36:$Vh,41:240,85:$Vl,95:89},o($V_,[2,134]),o($V_,[2,135]),{24:[1,241]},{24:[1,242]},o($V7,[2,29]),o($V7,[2,31])], -defaultActions: {2:[2,1],27:[2,2],33:[2,85],34:[2,86],116:[2,55],117:[2,56],195:[2,54],217:[2,115],230:[2,116]}, -parseError: function parseError (str, hash) { - if (hash.recoverable) { + var $0 = $$.length - 1; + switch (yystate) { + case 1: + return []; + break; + case 2: + return $$[$0 - 1]; + break; + case 3: + case 38: + case 44: + case 45: + case 89: + case 97: + case 99: + this.$ = [$$[$0]]; + break; + case 4: + case 39: + case 90: + this.$ = [].concat($$[$0 - 1], $$[$0]); + break; + case 5: + $$[$0]['prue'] = true; + $$[$0].pos = this._$; + this.$ = $$[$0]; + break; + case 6: + $$[$0].pos = this._$; + this.$ = $$[$0]; + break; + case 7: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 20: + case 21: + case 22: + case 50: + case 51: + case 55: + case 56: + case 57: + case 71: + case 74: + case 75: + case 85: + case 86: + case 87: + case 88: + case 94: + case 102: + case 109: + case 110: + case 115: + case 121: + case 123: + case 136: + case 137: + this.$ = $$[$0]; + break; + case 8: + this.$ = { type: 'raw', value: $$[$0] }; + break; + case 9: + this.$ = { type: 'comment', value: $$[$0] }; + break; + case 19: + this.$ = { type: 'noescape' }; + break; + case 23: + this.$ = { type: 'set', equal: $$[$0 - 1] }; + break; + case 24: + this.$ = { type: 'if', condition: $$[$0 - 1] }; + break; + case 25: + this.$ = { type: 'elseif', condition: $$[$0 - 1] }; + break; + case 26: + this.$ = { type: 'else' }; + break; + case 27: + this.$ = { type: 'end' }; + break; + case 28: + case 30: + this.$ = { type: 'foreach', to: $$[$0 - 3], from: $$[$0 - 1] }; + break; + case 29: + case 31: + this.$ = { type: 'foreach', to: $$[$0 - 4], from: $$[$0 - 1] }; + break; + case 32: + this.$ = { type: $$[$0] }; + break; + case 33: + this.$ = { type: 'return', value: $$[$0 - 1] }; + break; + case 34: + this.$ = { type: 'return', value: null }; + break; + case 35: + this.$ = { type: 'define', id: $$[$0 - 1] }; + break; + case 36: + this.$ = { type: 'macro', id: $$[$0 - 2], args: $$[$0 - 1] }; + break; + case 37: + this.$ = { type: 'macro', id: $$[$0 - 1] }; + break; + case 40: + this.$ = { type: 'macro_call', id: $$[$0 - 3].replace(/^\s+|\s+$/g, ''), args: $$[$0 - 1] }; + break; + case 41: + this.$ = { type: 'macro_call', id: $$[$0 - 2].replace(/^\s+|\s+$/g, '') }; + break; + case 42: + this.$ = { type: 'macro_body', id: $$[$0 - 3], args: $$[$0 - 1] }; + break; + case 43: + this.$ = { type: 'macro_body', id: $$[$0 - 2] }; + break; + case 46: + case 47: + case 48: + case 49: + case 100: + case 101: + this.$ = [].concat($$[$0 - 2], $$[$0]); + break; + case 52: + case 53: + case 104: + case 105: + this.$ = $$[$0 - 1]; + break; + case 54: + this.$ = [$$[$0 - 2], $$[$0]]; + break; + case 58: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '||' }; + break; + case 59: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '&&' }; + break; + case 60: + case 61: + case 62: + case 63: + case 64: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: $$[$0 - 1] }; + break; + case 65: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '>' }; + break; + case 66: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '<' }; + break; + case 67: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '==' }; + break; + case 68: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '>=' }; + break; + case 69: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '<=' }; + break; + case 70: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: '!=' }; + break; + case 72: + this.$ = { type: 'math', expression: [$$[$0]], operator: 'minus' }; + break; + case 73: + this.$ = { type: 'math', expression: [$$[$0]], operator: 'not' }; + break; + case 76: + this.$ = { type: 'math', expression: [$$[$0 - 1]], operator: 'parenthesis' }; + break; + case 77: + this.$ = { type: 'references', id: $$[$0 - 2], path: $$[$0 - 1], isWraped: true, leader: $$[$0 - 4] }; + break; + case 78: + this.$ = { type: 'references', id: $$[$0 - 1], path: $$[$0], leader: $$[$0 - 2] }; + break; + case 79: + this.$ = { type: 'references', id: $$[$0 - 2].id, path: $$[$0 - 1], isWraped: true, leader: $$[$0 - 4], args: $$[$0 - 2].args }; + break; + case 80: + this.$ = { type: 'references', id: $$[$0 - 1].id, path: $$[$0], leader: $$[$0 - 2], args: $$[$0 - 1].args }; + break; + case 81: + this.$ = { type: 'references', id: $$[$0], leader: $$[$0 - 1] }; + break; + case 82: + this.$ = { type: 'references', id: $$[$0 - 1], isWraped: true, leader: $$[$0 - 3] }; + break; + case 83: + this.$ = { type: 'references', id: $$[$0].id, leader: $$[$0 - 1], args: $$[$0].args }; + break; + case 84: + this.$ = { type: 'references', id: $$[$0 - 1].id, isWraped: true, args: $$[$0 - 1].args, leader: $$[$0 - 3] }; + break; + case 91: + this.$ = { type: 'method', id: $$[$0].id, args: $$[$0].args }; + break; + case 92: + this.$ = { type: 'index', id: $$[$0] }; + break; + case 93: + this.$ = { type: 'property', id: $$[$0] }; + if ($$[$0].type === 'content') this.$ = $$[$0]; + break; + case 95: + this.$ = { id: $$[$0 - 3], args: $$[$0 - 1] }; + break; + case 96: + this.$ = { id: $$[$0 - 2], args: false }; + break; + case 98: + this.$ = [{ type: 'runt', value: $$[$0] }]; + break; + case 103: + this.$ = { type: 'content', value: $$[$0 - 1] + $$[$0] }; + break; + case 106: + this.$ = { type: 'content', value: $$[$0 - 2] + $$[$0 - 1].value + $$[$0] }; + break; + case 107: + case 108: + this.$ = { type: 'content', value: $$[$0 - 1] + $$[$0] }; + break; + case 111: + this.$ = { type: 'bool', value: $$[$0] }; + break; + case 112: + this.$ = { type: 'integer', value: $$[$0] }; + break; + case 113: + this.$ = { type: 'decimal', value: +($$[$0 - 2] + '.' + $$[$0]) }; + break; + case 114: + this.$ = { type: 'decimal', value: -($$[$0 - 2] + '.' + $$[$0]) }; + break; + case 116: + this.$ = -parseInt($$[$0], 10); + break; + case 117: + this.$ = { type: 'string', value: $$[$0] }; + break; + case 118: + this.$ = { type: 'string', value: $$[$0], isEval: true }; + break; + case 119: + case 120: + this.$ = $$[$0]; + break; + case 122: + this.$ = { type: 'array', value: $$[$0 - 1] }; + break; + case 124: + this.$ = { type: 'array', value: [] }; + break; + case 125: + case 126: + case 127: + case 128: + this.$ = { type: 'array', isRange: true, value: [$$[$0 - 3], $$[$0 - 1]] }; + break; + case 129: + this.$ = { type: 'map', value: $$[$0 - 1] }; + break; + case 130: + this.$ = { type: 'map' }; + break; + case 131: + case 132: + this.$ = {}; + this.$[$$[$0 - 2].value] = $$[$0]; + break; + case 133: + this.$ = {}; + this.$[$$[$0 - 1].value] = $$[$01]; + break; + case 134: + case 135: + this.$ = $$[$0 - 4]; + this.$[$$[$0 - 2].value] = $$[$0]; + break; + case 138: + case 141: + this.$ = $$[$0 - 1] + $$[$0]; + break; + case 139: + this.$ = $$[$0 - 2] + $$[$0 - 1] + $$[$0]; + break; + case 140: + this.$ = $$[$0 - 2] + $$[$0 - 1]; + break; + } + }, + table: [ + { + 3: 1, + 4: [1, 2], + 5: 3, + 6: 4, + 7: 5, + 8: 6, + 9: 7, + 10: $V0, + 11: $V1, + 12: 11, + 13: 12, + 14: 13, + 15: 14, + 16: 15, + 17: 16, + 18: 17, + 19: 18, + 20: 19, + 21: $V2, + 25: 21, + 26: 22, + 27: 23, + 36: $V3, + 37: $V4, + 48: $V5, + 84: $V6, + }, + { 1: [3] }, + { 1: [2, 1] }, + { + 4: [1, 27], + 6: 28, + 7: 5, + 8: 6, + 9: 7, + 10: $V0, + 11: $V1, + 12: 11, + 13: 12, + 14: 13, + 15: 14, + 16: 15, + 17: 16, + 18: 17, + 19: 18, + 20: 19, + 21: $V2, + 25: 21, + 26: 22, + 27: 23, + 36: $V3, + 37: $V4, + 48: $V5, + 84: $V6, + }, + o($V7, [2, 3]), + o($V7, [2, 5]), + o($V7, [2, 6]), + o($V7, [2, 7]), + o($V7, [2, 8]), + o($V7, [2, 9]), + { 37: $V8, 39: $V9, 72: 29, 75: 31, 76: $Va, 84: [1, 32] }, + o($V7, [2, 10]), + o($V7, [2, 11]), + o($V7, [2, 12]), + o($V7, [2, 13]), + o($V7, [2, 14]), + o($V7, [2, 15]), + o($V7, [2, 16]), + o($V7, [2, 17]), + o($V7, [2, 18]), + { + 22: [1, 35], + 28: [1, 38], + 30: [1, 40], + 32: [1, 41], + 33: [1, 42], + 34: [1, 43], + 35: [1, 44], + 37: [1, 37], + 42: [1, 45], + 43: [1, 39], + 44: [1, 46], + 45: [1, 47], + 84: [1, 36], + }, + o($V7, [2, 20]), + o($V7, [2, 21]), + o($V7, [2, 22]), + o($V7, [2, 136]), + o($V7, [2, 137]), + { 37: [1, 48] }, + { 1: [2, 2] }, + o($V7, [2, 4]), + { 37: [1, 49], 75: 50 }, + o($Vb, [2, 81], { 73: 51, 78: 53, 79: 54, 80: 55, 81: 56, 23: $Vc, 82: $Vd, 85: $Ve }), + o($Vb, [2, 83], { 78: 53, 79: 54, 80: 55, 81: 56, 73: 59, 82: $Vd, 85: $Ve }), + o($V7, [2, 141]), + { 37: [2, 85] }, + { 37: [2, 86] }, + { 23: [1, 60] }, + o($V7, [2, 138]), + { 4: [1, 62], 23: [1, 63], 84: [1, 61] }, + { 23: [1, 64] }, + o($V7, [2, 34], { 23: [1, 65] }), + { 23: [1, 66] }, + { 23: [1, 67] }, + o($V7, [2, 26]), + o($V7, [2, 27]), + { 23: [1, 68] }, + o($V7, [2, 32]), + { 23: [1, 69] }, + { 23: [1, 70] }, + { 23: [1, 71] }, + { 23: $Vc, 40: $Vf, 73: 72, 74: 73, 77: $Vg, 78: 53, 79: 54, 80: 55, 81: 56, 82: $Vd, 85: $Ve }, + { 40: $Vf, 73: 76, 74: 77, 77: $Vg, 78: 53, 79: 54, 80: 55, 81: 56, 82: $Vd, 85: $Ve }, + o($Vb, [2, 78], { 79: 54, 80: 55, 81: 56, 78: 78, 82: $Vd, 85: $Ve }), + { + 7: 83, + 24: [1, 80], + 36: $Vh, + 37: $Vi, + 39: $Vj, + 41: 84, + 50: 81, + 54: 85, + 59: $Vk, + 71: 86, + 83: 79, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + o($Vq, [2, 89]), + o($Vq, [2, 91]), + o($Vq, [2, 92]), + o($Vq, [2, 93]), + { 37: [1, 100], 75: 99, 84: [1, 101] }, + { 7: 103, 36: $Vh, 59: $Vk, 71: 102, 84: [1, 104], 86: [1, 105], 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + o($Vb, [2, 80], { 79: 54, 80: 55, 81: 56, 78: 78, 82: $Vd, 85: $Ve }), + { 24: [1, 106] }, + o($V7, [2, 139]), + o($V7, [2, 140]), + { + 7: 112, + 24: [1, 108], + 36: $Vh, + 39: $Vj, + 41: 84, + 47: 107, + 49: 109, + 50: 111, + 51: $Vr, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { 7: 114, 29: 113, 36: $Vh }, + { + 7: 122, + 23: $Vs, + 31: 115, + 36: $Vh, + 39: $Vj, + 41: 116, + 54: 117, + 55: 118, + 59: $Vt, + 69: 119, + 70: $Vu, + 71: 123, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { + 7: 122, + 23: $Vs, + 31: 125, + 36: $Vh, + 39: $Vj, + 41: 116, + 54: 117, + 55: 118, + 59: $Vt, + 69: 119, + 70: $Vu, + 71: 123, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { + 7: 122, + 23: $Vs, + 31: 126, + 36: $Vh, + 39: $Vj, + 41: 116, + 54: 117, + 55: 118, + 59: $Vt, + 69: 119, + 70: $Vu, + 71: 123, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { 36: [1, 127] }, + { 36: [1, 128] }, + { 37: [1, 129] }, + { + 7: 112, + 24: [1, 131], + 36: $Vh, + 39: $Vj, + 41: 84, + 47: 130, + 49: 109, + 50: 111, + 51: $Vr, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { 40: $Vf, 74: 132, 77: $Vg, 78: 78, 79: 54, 80: 55, 81: 56, 82: $Vd, 85: $Ve }, + o($Vb, [2, 82]), + o($Vb, [2, 87]), + o($Vb, [2, 88]), + { 40: $Vf, 74: 133, 77: $Vg, 78: 78, 79: 54, 80: 55, 81: 56, 82: $Vd, 85: $Ve }, + o($Vb, [2, 84]), + o($Vq, [2, 90]), + { 24: [1, 134], 52: $Vv }, + o($Vq, [2, 96]), + o($Vw, [2, 97]), + o($Vw, [2, 98]), + o([24, 52], $Vx), + o($Vy, [2, 119]), + o($Vy, [2, 120]), + o($Vy, [2, 121]), + { 37: $V8, 39: $V9, 72: 29, 75: 31, 76: $Va }, + { + 7: 139, + 36: $Vh, + 37: $Vi, + 39: $Vj, + 41: 84, + 50: 81, + 54: 85, + 59: $Vk, + 71: 86, + 83: 136, + 85: $Vl, + 86: [1, 137], + 87: 91, + 88: 92, + 89: $Vm, + 90: 138, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + o($Vy, [2, 123]), + { 40: [1, 141], 87: 142, 93: $Vo, 94: $Vp, 97: 140 }, + o($Vz, [2, 109]), + o($Vz, [2, 110]), + o($Vz, [2, 111]), + o($VA, [2, 117]), + o($VA, [2, 118]), + o($Vz, $VB), + o($VC, $VD, { 92: [1, 143] }), + { 91: $VE }, + o($Vq, [2, 94]), + o($Vq, [2, 102], { 23: $Vc }), + o($Vq, [2, 103]), + { 84: [1, 146], 86: [1, 145] }, + { 86: [1, 147] }, + o($Vq, [2, 107]), + o($Vq, [2, 108]), + o($V7, [2, 19]), + { 24: [1, 148] }, + o($V7, [2, 41]), + { 24: [2, 50], 51: [1, 149], 52: $VF }, + { + 7: 112, + 36: $Vh, + 39: $Vj, + 41: 84, + 49: 151, + 50: 111, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + o($VG, [2, 44]), + o($VG, [2, 45]), + { 24: [1, 152] }, + { 53: [1, 153] }, + { 24: [1, 154] }, + { 24: [2, 55] }, + { 24: [2, 56] }, + { 24: [2, 57], 56: $VH, 57: $VI, 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN, 63: $VO, 64: $VP, 65: $VQ, 66: $VR, 67: $VS, 68: $VT }, + o($VU, [2, 71]), + { 23: $Vs, 69: 168, 91: $VE }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 169, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + o($VU, [2, 74]), + o($VU, [2, 75]), + { 7: 122, 23: $Vs, 36: $Vh, 55: 170, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 24: [1, 171] }, + { 24: [1, 172] }, + { 37: [1, 173], 39: [1, 174] }, + { 37: [1, 175] }, + { 7: 178, 24: [1, 177], 36: $Vh, 46: 176 }, + { 24: [1, 179] }, + o($V7, [2, 43]), + o($Vb, [2, 77]), + o($Vb, [2, 79]), + o($Vq, [2, 95]), + { + 7: 181, + 36: $Vh, + 39: $Vj, + 41: 84, + 50: 180, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { 52: $Vv, 86: [1, 182] }, + o($Vy, [2, 124]), + o($VV, $VB, { 96: [1, 183] }), + o($VV, $Vx, { 96: [1, 184] }), + { 40: [1, 185], 52: [1, 186] }, + o($Vy, [2, 130]), + { 98: [1, 187] }, + { 91: [1, 188] }, + o($VC, $VW, { 92: [1, 189] }), + o($Vq, [2, 104]), + o($Vq, [2, 106]), + o($Vq, [2, 105]), + o($V7, [2, 40]), + { + 7: 191, + 24: [2, 53], + 36: $Vh, + 39: $Vj, + 41: 84, + 50: 190, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { + 7: 193, + 36: $Vh, + 39: $Vj, + 41: 84, + 50: 192, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { 24: [2, 51], 51: [1, 194], 52: $VF }, + o($V7, [2, 23]), + { + 7: 122, + 23: $Vs, + 31: 195, + 36: $Vh, + 39: $Vj, + 41: 116, + 54: 117, + 55: 118, + 59: $Vt, + 69: 119, + 70: $Vu, + 71: 123, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + o($V7, [2, 33]), + { 7: 122, 23: $Vs, 36: $Vh, 55: 196, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 197, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 198, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 199, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 200, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 201, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 202, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 203, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 204, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 205, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 206, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 207, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + { 7: 122, 23: $Vs, 36: $Vh, 55: 208, 59: $Vt, 69: 119, 70: $Vu, 71: 123, 87: 91, 88: 92, 89: $Vm, 90: 96, 91: $Vn, 93: $Vo, 94: $Vp }, + o($VU, [2, 72]), + o($VU, [2, 73]), + { 24: [1, 209], 56: $VH, 57: $VI, 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN, 63: $VO, 64: $VP, 65: $VQ, 66: $VR, 67: $VS, 68: $VT }, + o($V7, [2, 24]), + o($V7, [2, 25]), + { 38: [1, 210] }, + { 37: [1, 211] }, + { 24: [1, 212] }, + { 7: 214, 24: [1, 213], 36: $Vh }, + o($V7, [2, 37]), + o($VX, [2, 38]), + o($V7, [2, 42]), + o($Vw, [2, 100]), + o($Vw, [2, 101]), + o($Vy, [2, 122]), + { 7: 216, 36: $Vh, 59: $VY, 90: 215, 91: $VZ }, + { 7: 220, 36: $Vh, 59: $VY, 90: 219, 91: $VZ }, + o($Vy, [2, 129]), + { 87: 221, 93: $Vo, 94: $Vp }, + o($V_, [2, 133], { + 41: 84, + 54: 85, + 71: 86, + 95: 89, + 87: 91, + 88: 92, + 90: 96, + 50: 222, + 7: 223, + 36: $Vh, + 39: $Vj, + 59: $Vk, + 85: $Vl, + 89: $Vm, + 91: $Vn, + 93: $Vo, + 94: $Vp, + }), + o($Vz, [2, 113]), + { 91: [1, 224] }, + o($VG, [2, 46]), + o($VG, [2, 49]), + o($VG, [2, 47]), + o($VG, [2, 48]), + { + 7: 191, + 24: [2, 52], + 36: $Vh, + 39: $Vj, + 41: 84, + 50: 190, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + { 24: [2, 54] }, + o($V$, [2, 58], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN, 63: $VO, 64: $VP, 65: $VQ, 66: $VR, 67: $VS, 68: $VT }), + o($V$, [2, 59], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN, 63: $VO, 64: $VP, 65: $VQ, 66: $VR, 67: $VS, 68: $VT }), + o($V01, [2, 60], { 60: $VL, 61: $VM, 62: $VN }), + o($V01, [2, 61], { 60: $VL, 61: $VM, 62: $VN }), + o($VU, [2, 62]), + o($VU, [2, 63]), + o($VU, [2, 64]), + o($V11, [2, 65], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN }), + o($V11, [2, 66], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN }), + o($V11, [2, 67], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN }), + o($V11, [2, 68], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN }), + o($V11, [2, 69], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN }), + o($V11, [2, 70], { 58: $VJ, 59: $VK, 60: $VL, 61: $VM, 62: $VN }), + o($VU, [2, 76]), + { 7: 225, 36: $Vh, 41: 226, 85: $Vl, 95: 89 }, + { 40: [1, 227] }, + o($V7, [2, 35]), + o($V7, [2, 36]), + o($VX, [2, 39]), + { 86: [1, 228] }, + { 86: [1, 229] }, + { 86: $VD }, + { 91: [1, 230] }, + { 86: [1, 231] }, + { 86: [1, 232] }, + { 98: [1, 233] }, + o($V_, [2, 131]), + o($V_, [2, 132]), + o($Vz, [2, 114]), + { 24: [1, 234] }, + { 24: [1, 235] }, + { 38: [1, 236] }, + o($Vy, [2, 125]), + o($Vy, [2, 127]), + { 86: $VW }, + o($Vy, [2, 126]), + o($Vy, [2, 128]), + { + 7: 237, + 36: $Vh, + 39: $Vj, + 41: 84, + 50: 238, + 54: 85, + 59: $Vk, + 71: 86, + 85: $Vl, + 87: 91, + 88: 92, + 89: $Vm, + 90: 96, + 91: $Vn, + 93: $Vo, + 94: $Vp, + 95: 89, + }, + o($V7, [2, 28]), + o($V7, [2, 30]), + { 7: 239, 36: $Vh, 41: 240, 85: $Vl, 95: 89 }, + o($V_, [2, 134]), + o($V_, [2, 135]), + { 24: [1, 241] }, + { 24: [1, 242] }, + o($V7, [2, 29]), + o($V7, [2, 31]), + ], + defaultActions: { + 2: [2, 1], + 27: [2, 2], + 33: [2, 85], + 34: [2, 86], + 116: [2, 55], + 117: [2, 56], + 195: [2, 54], + 217: [2, 115], + 230: [2, 116], + }, + parseError: function parseError(str, hash) { + if (hash.recoverable) { this.trace(str); - } else { + } else { var error = new Error(str); error.hash = hash; throw error; - } -}, -parse: function parse(input) { - var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; - var args = lstack.slice.call(arguments, 1); - var lexer = Object.create(this.lexer); - var sharedState = { yy: {} }; - for (var k in this.yy) { + } + }, + parse: function parse(input) { + var self = this, + stack = [0], + tstack = [], + vstack = [null], + lstack = [], + table = this.table, + yytext = '', + yylineno = 0, + yyleng = 0, + recovering = 0, + TERROR = 2, + EOF = 1; + var args = lstack.slice.call(arguments, 1); + var lexer = Object.create(this.lexer); + var sharedState = { yy: {} }; + for (var k in this.yy) { if (Object.prototype.hasOwnProperty.call(this.yy, k)) { - sharedState.yy[k] = this.yy[k]; + sharedState.yy[k] = this.yy[k]; } - } - lexer.setInput(input, sharedState.yy); - sharedState.yy.lexer = lexer; - sharedState.yy.parser = this; - if (typeof lexer.yylloc == 'undefined') { + } + lexer.setInput(input, sharedState.yy); + sharedState.yy.lexer = lexer; + sharedState.yy.parser = this; + if (typeof lexer.yylloc == 'undefined') { lexer.yylloc = {}; - } - var yyloc = lexer.yylloc; - lstack.push(yyloc); - var ranges = lexer.options && lexer.options.ranges; - if (typeof sharedState.yy.parseError === 'function') { + } + var yyloc = lexer.yylloc; + lstack.push(yyloc); + var ranges = lexer.options && lexer.options.ranges; + if (typeof sharedState.yy.parseError === 'function') { this.parseError = sharedState.yy.parseError; - } else { + } else { this.parseError = Object.getPrototypeOf(this).parseError; - } - function popStack(n) { + } + function popStack(n) { stack.length = stack.length - 2 * n; vstack.length = vstack.length - n; lstack.length = lstack.length - n; - } - _token_stack: - var lex = function () { - var token; - token = lexer.lex() || EOF; - if (typeof token !== 'number') { - token = self.symbols_[token] || token; - } - return token; - }; - var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; - while (true) { + } + _token_stack: var lex = function() { + var token; + token = lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + }; + var symbol, + preErrorSymbol, + state, + action, + a, + r, + yyval = {}, + p, + len, + newState, + expected; + while (true) { state = stack[stack.length - 1]; if (this.defaultActions[state]) { - action = this.defaultActions[state]; + action = this.defaultActions[state]; } else { - if (symbol === null || typeof symbol == 'undefined') { - symbol = lex(); - } - action = table[state] && table[state][symbol]; + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + action = table[state] && table[state][symbol]; } - if (typeof action === 'undefined' || !action.length || !action[0]) { - var errStr = ''; - expected = []; - for (p in table[state]) { - if (this.terminals_[p] && p > TERROR) { - expected.push('\'' + this.terminals_[p] + '\''); - } - } - if (lexer.showPosition) { - errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; - } else { - errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); - } - this.parseError(errStr, { - text: lexer.match, - token: this.terminals_[symbol] || symbol, - line: lexer.yylineno, - loc: yyloc, - expected: expected - }); + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push("'" + this.terminals_[p] + "'"); } + } + if (lexer.showPosition) { + errStr = + 'Parse error on line ' + + (yylineno + 1) + + ':\n' + + lexer.showPosition() + + '\nExpecting ' + + expected.join(', ') + + ", got '" + + (this.terminals_[symbol] || symbol) + + "'"; + } else { + errStr = + 'Parse error on line ' + + (yylineno + 1) + + ': Unexpected ' + + (symbol == EOF ? 'end of input' : "'" + (this.terminals_[symbol] || symbol) + "'"); + } + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected, + }); + } if (action[0] instanceof Array && action.length > 1) { - throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); + throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); } switch (action[0]) { - case 1: + case 1: stack.push(symbol); vstack.push(lexer.yytext); lstack.push(lexer.yylloc); stack.push(action[1]); symbol = null; if (!preErrorSymbol) { - yyleng = lexer.yyleng; - yytext = lexer.yytext; - yylineno = lexer.yylineno; - yyloc = lexer.yylloc; - if (recovering > 0) { - recovering--; - } + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + if (recovering > 0) { + recovering--; + } } else { - symbol = preErrorSymbol; - preErrorSymbol = null; + symbol = preErrorSymbol; + preErrorSymbol = null; } break; - case 2: + case 2: len = this.productions_[action[1]][1]; yyval.$ = vstack[vstack.length - len]; yyval._$ = { - first_line: lstack[lstack.length - (len || 1)].first_line, - last_line: lstack[lstack.length - 1].last_line, - first_column: lstack[lstack.length - (len || 1)].first_column, - last_column: lstack[lstack.length - 1].last_column + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column, }; if (ranges) { - yyval._$.range = [ - lstack[lstack.length - (len || 1)].range[0], - lstack[lstack.length - 1].range[1] - ]; + yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; } - r = this.performAction.apply(yyval, [ - yytext, - yyleng, - yylineno, - sharedState.yy, - action[1], - vstack, - lstack - ].concat(args)); + r = this.performAction.apply(yyval, [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args)); if (typeof r !== 'undefined') { - return r; + return r; } if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); } stack.push(this.productions_[action[1]][0]); vstack.push(yyval.$); @@ -461,28 +1544,28 @@ parse: function parse(input) { newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; stack.push(newState); break; - case 3: + case 3: return true; } - } - return true; -}}; -/* generated by jison-lex 0.3.4 */ -var lexer = (function(){ -var lexer = ({ - -EOF:1, + } + return true; + }, + }; + /* generated by jison-lex 0.3.4 */ + var lexer = (function() { + var lexer = { + EOF: 1, -parseError:function parseError(str, hash) { + parseError: function parseError(str, hash) { if (this.yy.parser) { - this.yy.parser.parseError(str, hash); + this.yy.parser.parseError(str, hash); } else { - throw new Error(str); + throw new Error(str); } - }, + }, -// resets the lexer, sets new input -setInput:function (input, yy) { + // resets the lexer, sets new input + setInput: function(input, yy) { this.yy = yy || this.yy || {}; this._input = input; this._more = this._backtrack = this.done = false; @@ -490,20 +1573,20 @@ setInput:function (input, yy) { this.yytext = this.matched = this.match = ''; this.conditionStack = ['INITIAL']; this.yylloc = { - first_line: 1, - first_column: 0, - last_line: 1, - last_column: 0 + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0, }; if (this.options.ranges) { - this.yylloc.range = [0,0]; + this.yylloc.range = [0, 0]; } this.offset = 0; return this; - }, + }, -// consumes and returns one char from the input -input:function () { + // consumes and returns one char from the input + input: function() { var ch = this._input[0]; this.yytext += ch; this.yyleng++; @@ -512,21 +1595,21 @@ input:function () { this.matched += ch; var lines = ch.match(/(?:\r\n?|\n).*/g); if (lines) { - this.yylineno++; - this.yylloc.last_line++; + this.yylineno++; + this.yylloc.last_line++; } else { - this.yylloc.last_column++; + this.yylloc.last_column++; } if (this.options.ranges) { - this.yylloc.range[1]++; + this.yylloc.range[1]++; } this._input = this._input.slice(1); return ch; - }, + }, -// unshifts one char (or a string) into the input -unput:function (ch) { + // unshifts one char (or a string) into the input + unput: function(ch) { var len = ch.length; var lines = ch.split(/(?:\r\n?|\n)/g); @@ -539,126 +1622,130 @@ unput:function (ch) { this.matched = this.matched.substr(0, this.matched.length - 1); if (lines.length - 1) { - this.yylineno -= lines.length - 1; + this.yylineno -= lines.length - 1; } var r = this.yylloc.range; this.yylloc = { - first_line: this.yylloc.first_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) - + oldLines[oldLines.length - lines.length].length - lines[0].length : - this.yylloc.first_column - len + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines + ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - + lines[0].length + : this.yylloc.first_column - len, }; if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; } this.yyleng = this.yytext.length; return this; - }, + }, -// When called from action, caches matched text and appends it on next action -more:function () { + // When called from action, caches matched text and appends it on next action + more: function() { this._more = true; return this; - }, + }, -// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. -reject:function () { + // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. + reject: function() { if (this.options.backtrack_lexer) { - this._backtrack = true; + this._backtrack = true; } else { - return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { - text: "", - token: null, - line: this.yylineno - }); - + return this.parseError( + 'Lexical error on line ' + + (this.yylineno + 1) + + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + + this.showPosition(), + { + text: '', + token: null, + line: this.yylineno, + } + ); } return this; - }, + }, -// retain first n characters of the match -less:function (n) { + // retain first n characters of the match + less: function(n) { this.unput(this.match.slice(n)); - }, + }, -// displays already matched input, i.e. for error messages -pastInput:function () { + // displays already matched input, i.e. for error messages + pastInput: function() { var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, + return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, ''); + }, -// displays upcoming input, i.e. for error messages -upcomingInput:function () { + // displays upcoming input, i.e. for error messages + upcomingInput: function() { var next = this.match; if (next.length < 20) { - next += this._input.substr(0, 20-next.length); + next += this._input.substr(0, 20 - next.length); } - return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); - }, + return (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ''); + }, -// displays the character position where the lexing error occurred, i.e. for error messages -showPosition:function () { + // displays the character position where the lexing error occurred, i.e. for error messages + showPosition: function() { var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c + "^"; - }, + var c = new Array(pre.length + 1).join('-'); + return pre + this.upcomingInput() + '\n' + c + '^'; + }, -// test the lexed token: return FALSE when not a match, otherwise return token -test_match:function(match, indexed_rule) { - var token, - lines, - backup; + // test the lexed token: return FALSE when not a match, otherwise return token + test_match: function(match, indexed_rule) { + var token, lines, backup; if (this.options.backtrack_lexer) { - // save context - backup = { - yylineno: this.yylineno, - yylloc: { - first_line: this.yylloc.first_line, - last_line: this.last_line, - first_column: this.yylloc.first_column, - last_column: this.yylloc.last_column - }, - yytext: this.yytext, - match: this.match, - matches: this.matches, - matched: this.matched, - yyleng: this.yyleng, - offset: this.offset, - _more: this._more, - _input: this._input, - yy: this.yy, - conditionStack: this.conditionStack.slice(0), - done: this.done - }; - if (this.options.ranges) { - backup.yylloc.range = this.yylloc.range.slice(0); - } + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column, + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done, + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } } lines = match[0].match(/(?:\r\n?|\n).*/g); if (lines) { - this.yylineno += lines.length; + this.yylineno += lines.length; } this.yylloc = { - first_line: this.yylloc.last_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.last_column, - last_column: lines ? - lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : - this.yylloc.last_column + match[0].length + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines + ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length + : this.yylloc.last_column + match[0].length, }; this.yytext += match[0]; this.match += match[0]; this.matches = match; this.yyleng = this.yytext.length; if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; + this.yylloc.range = [this.offset, (this.offset += this.yyleng)]; } this._more = false; this._backtrack = false; @@ -666,362 +1753,654 @@ test_match:function(match, indexed_rule) { this.matched += match[0]; token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); if (this.done && this._input) { - this.done = false; + this.done = false; } if (token) { - return token; + return token; } else if (this._backtrack) { - // recover context - for (var k in backup) { - this[k] = backup[k]; - } - return false; // rule action called reject() implying the next rule should be tested instead. + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. } return false; - }, + }, -// return next match in input -next:function () { + // return next match in input + next: function() { if (this.done) { - return this.EOF; + return this.EOF; } if (!this._input) { - this.done = true; + this.done = true; } - var token, - match, - tempMatch, - index; + var token, match, tempMatch, index; if (!this._more) { - this.yytext = ''; - this.match = ''; + this.yytext = ''; + this.match = ''; } var rules = this._currentRules(); for (var i = 0; i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (this.options.backtrack_lexer) { - token = this.test_match(tempMatch, rules[i]); - if (token !== false) { - return token; - } else if (this._backtrack) { - match = false; - continue; // rule action called reject() implying a rule MISmatch. - } else { - // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) - return false; - } - } else if (!this.options.flex) { - break; - } + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; } + } } if (match) { - token = this.test_match(match, rules[index]); - if (token !== false) { - return token; - } - // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) - return false; + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; } - if (this._input === "") { - return this.EOF; + if (this._input === '') { + return this.EOF; } else { - return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { - text: "", - token: null, - line: this.yylineno - }); + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: '', + token: null, + line: this.yylineno, + }); } - }, + }, -// return next match that has a token -lex:function lex () { + // return next match that has a token + lex: function lex() { var r = this.next(); if (r) { - return r; + return r; } else { - return this.lex(); + return this.lex(); } - }, + }, -// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) -begin:function begin (condition) { + // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) + begin: function begin(condition) { this.conditionStack.push(condition); - }, + }, -// pop the previously active lexer condition state off the condition stack -popState:function popState () { + // pop the previously active lexer condition state off the condition stack + popState: function popState() { var n = this.conditionStack.length - 1; if (n > 0) { - return this.conditionStack.pop(); + return this.conditionStack.pop(); } else { - return this.conditionStack[0]; + return this.conditionStack[0]; } - }, + }, -// produce the lexer rule set which is active for the currently active lexer condition state -_currentRules:function _currentRules () { + // produce the lexer rule set which is active for the currently active lexer condition state + _currentRules: function _currentRules() { if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { - return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; } else { - return this.conditions["INITIAL"].rules; + return this.conditions['INITIAL'].rules; } - }, + }, -// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available -topState:function topState (n) { + // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available + topState: function topState(n) { n = this.conditionStack.length - 1 - Math.abs(n || 0); if (n >= 0) { - return this.conditionStack[n]; + return this.conditionStack[n]; } else { - return "INITIAL"; + return 'INITIAL'; } - }, + }, -// alias for begin(condition) -pushState:function pushState (condition) { + // alias for begin(condition) + pushState: function pushState(condition) { this.begin(condition); - }, + }, -// return the number of states currently on the stack -stateStackSize:function stateStackSize() { + // return the number of states currently on the stack + stateStackSize: function stateStackSize() { return this.conditionStack.length; - }, -options: {}, -performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { -var YYSTATE=YY_START; -switch($avoiding_name_collisions) { -case 0: + }, + options: {}, + performAction: function anonymous(yy, yy_, $avoiding_name_collisions, YY_START) { + var YYSTATE = YY_START; + switch ($avoiding_name_collisions) { + case 0: + var _reg = /\\+$/; + var _esc = yy_.yytext.match(_reg); + var _num = _esc ? _esc[0].length : null; + /*转义实现,非常恶心,暂时没有好的解决方案*/ + if (!_num || !(_num % 2)) { + this.begin('mu'); + } else { + yy_.yytext = yy_.yytext.replace(/\\$/, ''); + this.begin('esc'); + } + if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); + if (yy_.yytext) return 84; + + break; + case 1: + var _reg = /\\+$/; + var _esc = yy_.yytext.match(_reg); + var _num = _esc ? _esc[0].length : null; + if (!_num || !(_num % 2)) { + this.begin('h'); + } else { + yy_.yytext = yy_.yytext.replace(/\\$/, ''); + this.begin('esc'); + } + if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); + if (yy_.yytext) return 84; + + break; + case 2: + return 84; + break; + case 3: + this.popState(); + return 11; + break; + case 4: + this.popState(); + yy_.yytext = yy_.yytext.replace(/^#\[\[|\]\]#$/g, ''); + return 10; + break; + case 5: + this.popState(); + return 11; + break; + case 6: + return 48; + break; + case 7: + return 21; + break; + case 8: + return 28; + break; + case 9: + return 30; + break; + case 10: + return 32; + break; + case 11: + this.popState(); + return 33; + break; + case 12: + this.popState(); + return 33; + break; + case 13: + this.popState(); + return 34; + break; + case 14: + this.popState(); + return 34; + break; + case 15: + this.popState(); + return 42; + break; + case 16: + return 43; + break; + case 17: + return 35; + break; + case 18: + return 22; + break; + case 19: + return 44; + break; + case 20: + return 45; + break; + case 21: + return 38; + break; + case 22: + return yy_.yytext; + break; + case 23: + return yy_.yytext; + break; + case 24: + return yy_.yytext; + break; + case 25: + return yy_.yytext; + break; + case 26: + return yy_.yytext; + break; + case 27: + return yy_.yytext; + break; + case 28: + return yy_.yytext; + break; + case 29: + return yy_.yytext; + break; + case 30: + return 56; + break; + case 31: + return yy_.yytext; + break; + case 32: + return 57; + break; + case 33: + return yy_.yytext; + break; + case 34: + return 70; + break; + case 35: + return 36; + break; + case 36: + return 36; + break; + case 37: + return yy_.yytext; + break; + case 38: + return 53; + break; + case 39: + var len = this.stateStackSize(); + if (len >= 2 && this.topState() === 'c' && this.topState(1) === 'run') { + return 51; + } - var _reg = /\\+$/; - var _esc = yy_.yytext.match(_reg); - var _num = _esc ? _esc[0].length: null; - /*转义实现,非常恶心,暂时没有好的解决方案*/ - if (!_num || !(_num % 2)) { - this.begin("mu"); - } else { - yy_.yytext = yy_.yytext.replace(/\\$/, ''); - this.begin('esc'); - } - if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); - if(yy_.yytext) return 84; - -break; -case 1: - var _reg = /\\+$/; - var _esc = yy_.yytext.match(_reg); - var _num = _esc ? _esc[0].length: null; - if (!_num || !(_num % 2)) { - this.begin("h"); - } else { - yy_.yytext = yy_.yytext.replace(/\\$/, ''); - this.begin('esc'); - } - if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); - if(yy_.yytext) return 84; - -break; -case 2: return 84; -break; -case 3: this.popState(); return 11; -break; -case 4: this.popState(); yy_.yytext = yy_.yytext.replace(/^#\[\[|\]\]#$/g, ''); return 10 -break; -case 5: this.popState(); return 11; -break; -case 6: return 48; -break; -case 7: return 21; -break; -case 8: return 28; -break; -case 9: return 30; -break; -case 10: return 32; -break; -case 11: this.popState(); return 33; -break; -case 12: this.popState(); return 33; -break; -case 13: this.popState(); return 34; -break; -case 14: this.popState(); return 34; -break; -case 15: this.popState(); return 42; -break; -case 16: return 43; -break; -case 17: return 35; -break; -case 18: return 22; -break; -case 19: return 44; -break; -case 20: return 45; -break; -case 21: return 38; -break; -case 22: return yy_.yytext; -break; -case 23: return yy_.yytext; -break; -case 24: return yy_.yytext; -break; -case 25: return yy_.yytext; -break; -case 26: return yy_.yytext; -break; -case 27: return yy_.yytext; -break; -case 28: return yy_.yytext; -break; -case 29: return yy_.yytext; -break; -case 30: return 56; -break; -case 31: return yy_.yytext; -break; -case 32: return 57; -break; -case 33: return yy_.yytext; -break; -case 34: return 70; -break; -case 35: return 36; -break; -case 36: return 36; -break; -case 37: return yy_.yytext; -break; -case 38: return 53; -break; -case 39: - var len = this.stateStackSize(); - if (len >= 2 && this.topState() === 'c' && this.topState(1) === 'run') { - return 51; - } - -break; -case 40: /*ignore whitespace*/ -break; -case 41: return 39; -break; -case 42: return 40; -break; -case 43: return 98; -break; -case 44: yy.begin = true; return 76; -break; -case 45: this.popState(); if (yy.begin === true) { yy.begin = false; return 77;} else { return 84; } -break; -case 46: this.begin("c"); return 23; -break; -case 47: - if (this.popState() === "c") { - var len = this.stateStackSize(); + break; + case 40 /*ignore whitespace*/: + break; + case 41: + return 39; + break; + case 42: + return 40; + break; + case 43: + return 98; + break; + case 44: + yy.begin = true; + return 76; + break; + case 45: + this.popState(); + if (yy.begin === true) { + yy.begin = false; + return 77; + } else { + return 84; + } + break; + case 46: + this.begin('c'); + return 23; + break; + case 47: + if (this.popState() === 'c') { + var len = this.stateStackSize(); - if (this.topState() === 'run') { - this.popState(); - len = len - 1; - } + if (this.topState() === 'run') { + this.popState(); + len = len - 1; + } - var tailStack = this.topState(len - 2); - /** 遇到#set(a = b)括号结束后结束状态h*/ - if (len === 2 && tailStack === "h"){ - this.popState(); - } else if (len === 3 && tailStack === "mu" && this.topState(len - 3) === "h") { - // issue#7 $foo#if($a)...#end - this.popState(); - this.popState(); - } + var tailStack = this.topState(len - 2); + /** 遇到#set(a = b)括号结束后结束状态h*/ + if (len === 2 && tailStack === 'h') { + this.popState(); + } else if (len === 3 && tailStack === 'mu' && this.topState(len - 3) === 'h') { + // issue#7 $foo#if($a)...#end + this.popState(); + this.popState(); + } - return 24; - } else { - return 84; - } - -break; -case 48: this.begin("i"); return 85; -break; -case 49: - if (this.popState() === "i") { - return 86; - } else { - return 84; - } - -break; -case 50: return 96; -break; -case 51: return 82; -break; -case 52: return 92; -break; -case 53: return 52; -break; -case 54: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2).replace(/\\"/g,'"'); return 94; -break; -case 55: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2).replace(/\\'/g,"'"); return 93; -break; -case 56: return 89; -break; -case 57: return 89; -break; -case 58: return 89; -break; -case 59: return 91; -break; -case 60: /* return ID */ return 37; -break; -case 61: this.begin("run"); return 37; -break; -case 62: this.popState(); yy_.yytext = '#'; return 84; -break; -case 63: this.begin('h'); return 21; -break; -case 64: this.popState(); return 84; -break; -case 65: this.popState(); return 84; -break; -case 66: this.popState(); return 84; -break; -case 67: this.popState(); return 4; -break; -case 68: return 4; -break; -} -}, -rules: [/^(?:[^#]*?(?=\$))/,/^(?:[^\$]*?(?=#))/,/^(?:[^\x00]+)/,/^(?:#\*[\s\S]+?\*#)/,/^(?:#\[\[[\s\S]+?\]\]#)/,/^(?:##[^\n]*)/,/^(?:#@)/,/^(?:#(?=[a-zA-Z{]))/,/^(?:set[ ]*(?=[^a-zA-Z0-9_]+))/,/^(?:if[ ]*(?=[^a-zA-Z0-9_]+))/,/^(?:elseif[ ]*(?=[^a-zA-Z0-9_]+))/,/^(?:else\b)/,/^(?:\{else\})/,/^(?:end\b)/,/^(?:\{end\})/,/^(?:break\b)/,/^(?:return[ ]*(?=[^a-zA-Z0-9_]+))/,/^(?:foreach[ ]*(?=[^a-zA-Z0-9_]+))/,/^(?:noescape(?=[^a-zA-Z0-9_]+))/,/^(?:define[ ]*(?=[^a-zA-Z0-9_]+))/,/^(?:macro[ ]*(?=[^a-zA-Z0-9_]+))/,/^(?:in\b)/,/^(?:[%\+\-\*\/])/,/^(?:<=)/,/^(?:>=)/,/^(?:[><])/,/^(?:==)/,/^(?:>)/,/^(?:<)/,/^(?:\|\|)/,/^(?:or\b)/,/^(?:&&)/,/^(?:and\b)/,/^(?:!=)/,/^(?:not\b)/,/^(?:\$!(?=[{a-zA-Z_]))/,/^(?:\$(?=[{a-zA-Z_]))/,/^(?:!)/,/^(?:=)/,/^(?:[ ]+(?=[^,]))/,/^(?:\s+)/,/^(?:\{)/,/^(?:\})/,/^(?::[\s]*)/,/^(?:\{)/,/^(?:\})/,/^(?:\([\s]*(?=[$'"\[\{\-0-9\w()!]))/,/^(?:\))/,/^(?:\[[\s]*(?=[\-$"'0-9{\[\]]+))/,/^(?:\])/,/^(?:\.\.)/,/^(?:\.(?=[a-zA-Z_]))/,/^(?:\.(?=[\d]))/,/^(?:,[ ]*)/,/^(?:"(\\"|[^\"])*")/,/^(?:'(\\'|[^\'])*')/,/^(?:null\b)/,/^(?:false\b)/,/^(?:true\b)/,/^(?:[0-9]+)/,/^(?:[_a-zA-Z][a-zA-Z0-9_-]*)/,/^(?:[_a-zA-Z][a-zA-Z0-9_\-]*[ ]*(?=\())/,/^(?:\\#)/,/^(?:#)/,/^(?:.)/,/^(?:\s+)/,/^(?:[\$#])/,/^(?:$)/,/^(?:$)/], -conditions: {"mu":{"rules":[5,35,36,44,45,46,47,48,49,51,60,62,63,64,65,67],"inclusive":false},"c":{"rules":[21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,46,47,48,49,51,52,53,54,55,56,57,58,59,60],"inclusive":false},"i":{"rules":[21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,40,41,41,42,42,43,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60],"inclusive":false},"h":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,35,36,37,38,43,46,47,48,49,51,59,61,62,64,65,67],"inclusive":false},"esc":{"rules":[66],"inclusive":false},"run":{"rules":[35,36,37,39,40,41,42,43,46,47,48,49,51,52,53,54,55,56,57,58,59,60,62,64,65,67],"inclusive":false},"INITIAL":{"rules":[0,1,2,68],"inclusive":true}} -}); -return lexer; -})(); -parser.lexer = lexer; -function Parser () { - this.yy = {}; -} -Parser.prototype = parser;parser.Parser = Parser; -return new Parser; -})(); + return 24; + } else { + return 84; + } + + break; + case 48: + this.begin('i'); + return 85; + break; + case 49: + if (this.popState() === 'i') { + return 86; + } else { + return 84; + } + break; + case 50: + return 96; + break; + case 51: + return 82; + break; + case 52: + return 92; + break; + case 53: + return 52; + break; + case 54: + yy_.yytext = yy_.yytext.substr(1, yy_.yyleng - 2).replace(/\\"/g, '"'); + return 94; + break; + case 55: + yy_.yytext = yy_.yytext.substr(1, yy_.yyleng - 2).replace(/\\'/g, "'"); + return 93; + break; + case 56: + return 89; + break; + case 57: + return 89; + break; + case 58: + return 89; + break; + case 59: + return 91; + break; + case 60: + /* return ID */ return 37; + break; + case 61: + this.begin('run'); + return 37; + break; + case 62: + this.popState(); + yy_.yytext = '#'; + return 84; + break; + case 63: + this.begin('h'); + return 21; + break; + case 64: + this.popState(); + return 84; + break; + case 65: + this.popState(); + return 84; + break; + case 66: + this.popState(); + return 84; + break; + case 67: + this.popState(); + return 4; + break; + case 68: + return 4; + break; + } + }, + rules: [ + /^(?:[^#]*?(?=\$))/, + /^(?:[^\$]*?(?=#))/, + /^(?:[^\x00]+)/, + /^(?:#\*[\s\S]+?\*#)/, + /^(?:#\[\[[\s\S]+?\]\]#)/, + /^(?:##[^\n]*)/, + /^(?:#@)/, + /^(?:#(?=[a-zA-Z{]))/, + /^(?:set[ ]*(?=[^a-zA-Z0-9_]+))/, + /^(?:if[ ]*(?=[^a-zA-Z0-9_]+))/, + /^(?:elseif[ ]*(?=[^a-zA-Z0-9_]+))/, + /^(?:else\b)/, + /^(?:\{else\})/, + /^(?:end\b)/, + /^(?:\{end\})/, + /^(?:break\b)/, + /^(?:return[ ]*(?=[^a-zA-Z0-9_]+))/, + /^(?:foreach[ ]*(?=[^a-zA-Z0-9_]+))/, + /^(?:noescape(?=[^a-zA-Z0-9_]+))/, + /^(?:define[ ]*(?=[^a-zA-Z0-9_]+))/, + /^(?:macro[ ]*(?=[^a-zA-Z0-9_]+))/, + /^(?:in\b)/, + /^(?:[%\+\-\*\/])/, + /^(?:<=)/, + /^(?:>=)/, + /^(?:[><])/, + /^(?:==)/, + /^(?:>)/, + /^(?:<)/, + /^(?:\|\|)/, + /^(?:or\b)/, + /^(?:&&)/, + /^(?:and\b)/, + /^(?:!=)/, + /^(?:not\b)/, + /^(?:\$!(?=[{a-zA-Z_]))/, + /^(?:\$(?=[{a-zA-Z_]))/, + /^(?:!)/, + /^(?:=)/, + /^(?:[ ]+(?=[^,]))/, + /^(?:\s+)/, + /^(?:\{)/, + /^(?:\})/, + /^(?::[\s]*)/, + /^(?:\{)/, + /^(?:\})/, + /^(?:\([\s]*(?=[$'"\[\{\-0-9\w()!]))/, + /^(?:\))/, + /^(?:\[[\s]*(?=[\-$"'0-9{\[\]]+))/, + /^(?:\])/, + /^(?:\.\.)/, + /^(?:\.(?=[a-zA-Z_]))/, + /^(?:\.(?=[\d]))/, + /^(?:,[ ]*)/, + /^(?:"(\\"|[^\"])*")/, + /^(?:'(\\'|[^\'])*')/, + /^(?:null\b)/, + /^(?:false\b)/, + /^(?:true\b)/, + /^(?:[0-9]+)/, + /^(?:[_a-zA-Z][a-zA-Z0-9_-]*)/, + /^(?:[_a-zA-Z][a-zA-Z0-9_\-]*[ ]*(?=\())/, + /^(?:\\#)/, + /^(?:#)/, + /^(?:.)/, + /^(?:\s+)/, + /^(?:[\$#])/, + /^(?:$)/, + /^(?:$)/, + ], + conditions: { + mu: { rules: [5, 35, 36, 44, 45, 46, 47, 48, 49, 51, 60, 62, 63, 64, 65, 67], inclusive: false }, + c: { + rules: [ + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 46, + 47, + 48, + 49, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + ], + inclusive: false, + }, + i: { + rules: [ + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 41, + 41, + 42, + 42, + 43, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + ], + inclusive: false, + }, + h: { + rules: [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 35, + 36, + 37, + 38, + 43, + 46, + 47, + 48, + 49, + 51, + 59, + 61, + 62, + 64, + 65, + 67, + ], + inclusive: false, + }, + esc: { rules: [66], inclusive: false }, + run: { + rules: [35, 36, 37, 39, 40, 41, 42, 43, 46, 47, 48, 49, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 62, 64, 65, 67], + inclusive: false, + }, + INITIAL: { rules: [0, 1, 2, 68], inclusive: true }, + }, + }; + return lexer; + })(); + parser.lexer = lexer; + function Parser() { + this.yy = {}; + } + Parser.prototype = parser; + parser.Parser = Parser; + return new Parser(); +})(); if (typeof require !== 'undefined' && typeof exports !== 'undefined') { -exports.parser = index; -exports.Parser = index.Parser; -exports.parse = function () { return index.parse.apply(index, arguments); }; -exports.main = function commonjsMain (args) { + exports.parser = index; + exports.Parser = index.Parser; + exports.parse = function() { + return index.parse.apply(index, arguments); + }; + exports.main = function commonjsMain(args) { if (!args[1]) { - console.log('Usage: '+args[0]+' FILE'); - process.exit(1); + console.log('Usage: ' + args[0] + ' FILE'); + process.exit(1); } - var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); + var source = require('fs').readFileSync(require('path').normalize(args[1]), 'utf8'); return exports.parser.parse(source); -}; -if (typeof module !== 'undefined' && require.main === module) { - exports.main(process.argv.slice(1)); + }; + if (typeof module !== 'undefined' && require.main === module) { + exports.main(process.argv.slice(1)); + } } -} \ No newline at end of file diff --git a/packages/amplify-velocity-template/tests/comment.test.js b/packages/amplify-velocity-template/tests/comment.test.js index f23478fc4c..afbdc5aa6e 100644 --- a/packages/amplify-velocity-template/tests/comment.test.js +++ b/packages/amplify-velocity-template/tests/comment.test.js @@ -1,4 +1,4 @@ -var Velocity = require('../src/velocity') +var Velocity = require('../src/velocity'); describe('comment render', function() { it('fix #66', function() { diff --git a/packages/amplify-velocity-template/tests/compile.js b/packages/amplify-velocity-template/tests/compile.js index 7a136374a8..9ac2d75fc3 100644 --- a/packages/amplify-velocity-template/tests/compile.js +++ b/packages/amplify-velocity-template/tests/compile.js @@ -1,122 +1,131 @@ 'use strict'; -var Velocity = require('../src/velocity') -var assert = require("assert") -var parse = Velocity.parse -var Compile = Velocity.Compile +var Velocity = require('../src/velocity'); +var assert = require('assert'); +var parse = Velocity.parse; +var Compile = Velocity.Compile; describe('Compile', function() { - var render = Velocity.render; function getContext(str, context, macros) { - var compile = new Compile(parse(str)) - compile.render(context, macros) - return compile.context + var compile = new Compile(parse(str)); + compile.render(context, macros); + return compile.context; } describe('References', function() { - it('get/is method', function() { - var vm = '$customer.getAddress() $customer.getaddress()' - var vm1 = '$customer.get("address") $customer.get("Address")' - var vm3 = '$customer.isAddress() $customer.isaddress()' - - assert.equal('bar bar', render(vm3, {customer: {Address: "bar"}})) - - assert.equal('bar bar', render(vm, {customer: {address: "bar"}})) - assert.equal('foo bar', render(vm, { - customer: { - address: 'bar', - Address: 'foo' - } - })) - - assert.equal('bar bar', render(vm1, {customer: {address: "bar"}})) - assert.equal('foo bar', render(vm1, { - customer: { - Address: 'bar', - address: 'foo' - } - })) - }) + var vm = '$customer.getAddress() $customer.getaddress()'; + var vm1 = '$customer.get("address") $customer.get("Address")'; + var vm3 = '$customer.isAddress() $customer.isaddress()'; + + assert.equal('bar bar', render(vm3, { customer: { Address: 'bar' } })); + + assert.equal('bar bar', render(vm, { customer: { address: 'bar' } })); + assert.equal( + 'foo bar', + render(vm, { + customer: { + address: 'bar', + Address: 'foo', + }, + }) + ); + + assert.equal('bar bar', render(vm1, { customer: { address: 'bar' } })); + assert.equal( + 'foo bar', + render(vm1, { + customer: { + Address: 'bar', + address: 'foo', + }, + }) + ); + }); it('method with attribute', function() { - var vm = '$foo().bar\n${foo().bar}' - assert.equal('hello\nhello', render(vm, { - foo: function() { - return { bar: 'hello' } - } - })) - - assert.equal('foo', render('${foo()}', { - foo: function() { - return 'foo' - } - })) - }) + var vm = '$foo().bar\n${foo().bar}'; + assert.equal( + 'hello\nhello', + render(vm, { + foo: function() { + return { bar: 'hello' }; + }, + }) + ); + + assert.equal( + 'foo', + render('${foo()}', { + foo: function() { + return 'foo'; + }, + }) + ); + }); it('index notation', function() { - var vm = '$foo[0] $foo[$i] $foo.get(1) $xx["oo"]' - assert.equal('bar haha haha oo', render(vm, {foo: ["bar", "haha"], i: 1, xx: { oo: 'oo' }})) - }) + var vm = '$foo[0] $foo[$i] $foo.get(1) $xx["oo"]'; + assert.equal('bar haha haha oo', render(vm, { foo: ['bar', 'haha'], i: 1, xx: { oo: 'oo' } })); + }); it('set method', function() { - var vm = '$page.setTitle("My Home Page").setname("haha")' + - '$page.Title $page.name' - assert.equal('My Home Page haha', render(vm, {page: {}})) - }) + var vm = '$page.setTitle("My Home Page").setname("haha")' + '$page.Title $page.name'; + assert.equal('My Home Page haha', render(vm, { page: {} })); + }); it('runt type support', function() { - var vm = '$page.header(page)' - assert.equal('$page.header(page)', render(vm, {page: {}})) - }) + var vm = '$page.header(page)'; + assert.equal('$page.header(page)', render(vm, { page: {} })); + }); it('size method', function() { - var vm = '$foo.bar.size()' - assert.equal('2', render(vm, {foo: {bar: [1, 2]}})) - assert.equal('2', render(vm, {foo: {bar: {a: 1, b: 3}}})) + var vm = '$foo.bar.size()'; + assert.equal('2', render(vm, { foo: { bar: [1, 2] } })); + assert.equal('2', render(vm, { foo: { bar: { a: 1, b: 3 } } })); - var vm2 = '#if($foo.bar.size()) ok #{else} nosize #end' - assert.equal(' nosize ', render(vm2, {foo: {bar: 123}})) - assert.equal(' nosize ', render(vm2, {foo: {}})) + var vm2 = '#if($foo.bar.size()) ok #{else} nosize #end'; + assert.equal(' nosize ', render(vm2, { foo: { bar: 123 } })); + assert.equal(' nosize ', render(vm2, { foo: {} })); var vm3 = '$foo.size()'; function fooSize() { return 3; } - assert.equal('3', render(vm3, { foo: { size: fooSize } })) - }) + assert.equal('3', render(vm3, { foo: { size: fooSize } })); + }); it('quiet reference', function() { - var vm = 'my email is $email' - var vmquiet = 'my email is $!email.xxx' - assert.equal(vm, render(vm)) - assert.equal('my email is ', render(vmquiet)) - }) + var vm = 'my email is $email'; + var vmquiet = 'my email is $!email.xxx'; + assert.equal(vm, render(vm)); + assert.equal('my email is ', render(vmquiet)); + }); it('silence all reference', function() { - var vm = 'my email is $email' + var vm = 'my email is $email'; - var compile = new Compile(parse(vm)) - assert.equal('my email is ', compile.render(null, null, true)) - }) + var compile = new Compile(parse(vm)); + assert.equal('my email is ', compile.render(null, null, true)); + }); it('this context keep correct, see #16', function() { - var data = 'a = $a.get()' + var data = 'a = $a.get()'; function B(c) { - this.c = c + this.c = c; } B.prototype.get = function() { - var t = this.eval(" hello $name", {name: 'hanwen'}) - return this.c + t - } + var t = this.eval(' hello $name', { name: 'hanwen' }); + return this.c + t; + }; - assert.equal('a = 1 hello hanwen', render(data, {a: new B(1)})) - }) + assert.equal('a = 1 hello hanwen', render(data, { a: new B(1) })); + }); it('this context should keep corrent in macro', function() { - var data = '#parse()' + var data = '#parse()'; var Macro = function(name) { this.name = name; }; @@ -125,83 +134,88 @@ describe('Compile', function() { return this.name; }; - assert.equal('hanwen', render(data, {}, new Macro('hanwen'))) - }) + assert.equal('hanwen', render(data, {}, new Macro('hanwen'))); + }); it('get variable form text', function() { - var vm = 'hello $user.getName().getFullName("hanwen")' - var data = { '$user.getName().getFullName("hanwen")': 'world' } - assert.equal('hello world', render(vm, data)) - }) + var vm = 'hello $user.getName().getFullName("hanwen")'; + var data = { '$user.getName().getFullName("hanwen")': 'world' }; + assert.equal('hello world', render(vm, data)); + }); it('escape default', function() { - var vm = '$name $name2 $cn $cn1' + var vm = '$name $name2 $cn $cn1'; var data = { name: 'hello world', name2: '&a', cn: '中文', - cn1: '中文' - } + cn1: '中文', + }; - var ret = 'hello world <i>&a 中文 <i>中文' - assert.equal(ret, render(vm, data)) - }) + var ret = 'hello world <i>&a 中文 <i>中文'; + assert.equal(ret, render(vm, data)); + }); it('add custom ignore escape function', function() { - var vm = '$noIgnore($name), $ignore($name)' - var expected = '<i>, ' + var vm = '$noIgnore($name), $ignore($name)'; + var expected = '<i>, '; - var compile = new Compile(parse(vm)) - compile.addIgnoreEscpape('ignore') + var compile = new Compile(parse(vm)); + compile.addIgnoreEscpape('ignore'); var context = { name: '', noIgnore: function(name) { - return name + return name; }, ignore: function(name) { return name; - } - } + }, + }; - var ret = compile.render(context) - assert.equal(expected, ret) - }) + var ret = compile.render(context); + assert.equal(expected, ret); + }); it('config support', function() { - var vm = '$foo($name)' - var expected = '' + var vm = '$foo($name)'; + var expected = ''; - var compile = new Compile(parse(vm), { escape: false }) + var compile = new Compile(parse(vm), { escape: false }); var context = { name: '', foo: function(name) { return name; - } - } + }, + }; - var ret = compile.render(context) - assert.equal(expected, ret) + var ret = compile.render(context); + assert.equal(expected, ret); - compile = new Compile(parse(vm), { unescape: { foo: true } }) - ret = compile.render(context) - assert.equal(expected, ret) + compile = new Compile(parse(vm), { unescape: { foo: true } }); + ret = compile.render(context); + assert.equal(expected, ret); - compile = new Compile(parse(vm)) - ret = compile.render(context) - assert.equal('<i>', ret) - }) + compile = new Compile(parse(vm)); + ret = compile.render(context); + assert.equal('<i>', ret); + }); - it ('valueMapper support', () => { + it('valueMapper support', () => { const values = []; - const vm = '#set($foo = "bar")\n$foo' - const ret = render(vm, {}, {}, { - valueMapper: (value) => { - values.push(value); - return 'foo'; - }, - }); + const vm = '#set($foo = "bar")\n$foo'; + const ret = render( + vm, + {}, + {}, + { + valueMapper: value => { + values.push(value); + return 'foo'; + }, + } + ); assert.deepEqual(values, ['bar']); assert.equal(ret.trim(), 'foo'); }); @@ -209,302 +223,284 @@ describe('Compile', function() { describe('env', function() { it('should throw on property when parent is null', function() { var vm = '$foo.bar'; - var compile = new Compile(parse(vm), { env: 'development' }) + var compile = new Compile(parse(vm), { env: 'development' }); function foo() { - compile.render() - }; + compile.render(); + } foo.should.throw(/get property bar of undefined/); }); it('should throw on index when parent is null', function() { var vm = '$foo[1]'; - var compile = new Compile(parse(vm), { env: 'development' }) + var compile = new Compile(parse(vm), { env: 'development' }); function foo() { - compile.render() - }; + compile.render(); + } foo.should.throw(/get property 1 of undefined/); }); it('should throw on function when parent is null', function() { var vm = '$foo.xx()'; - var compile = new Compile(parse(vm), { env: 'development' }) + var compile = new Compile(parse(vm), { env: 'development' }); function foo() { - compile.render() - }; + compile.render(); + } foo.should.throw(/get property xx of undefined/); }); it('should throw when mult level', function() { var vm = '$foo.bar.xx.bar1'; - var compile = new Compile(parse(vm), { env: 'development' }) + var compile = new Compile(parse(vm), { env: 'development' }); function foo() { - compile.render({ foo: { bar: {} }}); - }; + compile.render({ foo: { bar: {} } }); + } foo.should.throw(/get property bar1 of undefined/); }); it('not function', function() { var vm = '$foo.bar.xx()'; - var compile = new Compile(parse(vm), { env: 'development' }) + var compile = new Compile(parse(vm), { env: 'development' }); function foo() { - return compile.render({ foo: { bar: {} }}); - }; + return compile.render({ foo: { bar: {} } }); + } foo.should.throw(/xx is not method/); }); }); - }) - + }); describe('Literals', function() { - - it("eval string value", function() { - var vm = '#set( $directoryRoot = "www" )' + - '#set( $templateName = "index.vm")' + - '#set( $template = "$directoryRoot/$templateName" )' + - '$template' - - assert.equal('www/index.vm', render(vm)) - }) + it('eval string value', function() { + var vm = + '#set( $directoryRoot = "www" )' + + '#set( $templateName = "index.vm")' + + '#set( $template = "$directoryRoot/$templateName" )' + + '$template'; + + assert.equal('www/index.vm', render(vm)); + }); it('not eval string', function() { - var vm = "#set( $blargh = '$foo' )$blargh" - assert.equal('$foo', render(vm)) - }) + var vm = "#set( $blargh = '$foo' )$blargh"; + assert.equal('$foo', render(vm)); + }); it('not parse #[[ ]]#', function() { - var vm = '#foreach ($woogie in $boogie) nothing to $woogie #end' - assert.equal(vm, render('#[[' + vm + ']]#')) - }) + var vm = '#foreach ($woogie in $boogie) nothing to $woogie #end'; + assert.equal(vm, render('#[[' + vm + ']]#')); + }); it('Range Operator', function() { - var vm1 = '#set($foo = [-1..2])' - var vm2 = '#set($foo = [-1..$bar])' - var vm3 = '#set($foo = [$bar..2])' - assert.deepEqual([-1, 0, 1, 2], getContext(vm1).foo) - assert.deepEqual([-1, 0, 1, 2], getContext(vm2, {bar: 2}).foo) - assert.deepEqual([-1, 0, 1, 2], getContext(vm3, {bar: -1}).foo) - assert.deepEqual([], getContext('#set($foo = [$bar..1])').foo) - }) + var vm1 = '#set($foo = [-1..2])'; + var vm2 = '#set($foo = [-1..$bar])'; + var vm3 = '#set($foo = [$bar..2])'; + assert.deepEqual([-1, 0, 1, 2], getContext(vm1).foo); + assert.deepEqual([-1, 0, 1, 2], getContext(vm2, { bar: 2 }).foo); + assert.deepEqual([-1, 0, 1, 2], getContext(vm3, { bar: -1 }).foo); + assert.deepEqual([], getContext('#set($foo = [$bar..1])').foo); + }); it('map and array nest', function() { - var vm1 = '' + - '#set($a = [\n' + - ' {"name": 1},\n' + - ' {"name": 2}\n' + - '])\n' + - ' ' - - var vm2 = '' + + var vm1 = '' + '#set($a = [\n' + ' {"name": 1},\n' + ' {"name": 2}\n' + '])\n' + ' '; + + var vm2 = + '' + '#set($a = {\n' + ' "a": [1, 2, ["1", "a"], {"a": 1}],\n' + ' "b": "12",\n' + ' "d": null,\n' + ' "c": false\n' + '})\n' + - '' + ''; - assert.deepEqual([{name: 1}, { name: 2 }], getContext(vm1).a) + assert.deepEqual([{ name: 1 }, { name: 2 }], getContext(vm1).a); var expected = { - a: [1, 2, ["1", "a"], {a: 1}], - b: "12", d: null, c: false + a: [1, 2, ['1', 'a'], { a: 1 }], + b: '12', + d: null, + c: false, }; - assert.deepEqual(expected, getContext(vm2).a) - }) - }) + assert.deepEqual(expected, getContext(vm2).a); + }); + }); describe('Conditionals', function() { - it('#if', function() { - var vm = '#if($foo)Velocity#end' - assert.equal('Velocity', render(vm, {foo: 1})) - }) + var vm = '#if($foo)Velocity#end'; + assert.equal('Velocity', render(vm, { foo: 1 })); + }); it('#if not work', function() { - var vm = '#if($!css_pureui)hello world#end' - assert.equal('', render(vm)) - }) + var vm = '#if($!css_pureui)hello world#end'; + assert.equal('', render(vm)); + }); it('#elseif & #else', function() { - var vm = '#if($foo < 5)Go North#elseif($foo == 8)' + - 'Go East#{else}Go South#end' - assert.equal('Go North', render(vm, {foo: 4})) - assert.equal('Go East', render(vm, {foo: 8})) - assert.equal('Go South', render(vm, {foo: 9})) - }) + var vm = '#if($foo < 5)Go North#elseif($foo == 8)' + 'Go East#{else}Go South#end'; + assert.equal('Go North', render(vm, { foo: 4 })); + assert.equal('Go East', render(vm, { foo: 8 })); + assert.equal('Go South', render(vm, { foo: 9 })); + }); it('#if with arguments', function() { - var vm = '#if($foo.isTrue(true))true#{end}' + var vm = '#if($foo.isTrue(true))true#{end}'; var foo = { isTrue: function(str) { - return !!str - } - } - - assert.equal('true', render(vm, {foo: foo})) - }) + return !!str; + }, + }; - }) + assert.equal('true', render(vm, { foo: foo })); + }); + }); describe('Velocimacros', function() { - it('simple #macro', function() { - var vm = '#macro( d )#end #d()' - assert.equal(' ', render(vm)) - }) + var vm = '#macro( d )#end #d()'; + assert.equal(' ', render(vm)); + }); it('compex #macro', function() { - var vm = '#macro( d $name)$!name#end #d($foo)' - var vm1 = '#macro( d $a $b)#if($b)$a#end#end #d ( $foo $bar )' - - assert.equal(' hanwen', render(vm, {foo: 'hanwen'})) - assert.equal(' ', render(vm)) - assert.equal(' ', render(vm1, {foo: "haha"})) - assert.equal(' ', render(vm1, {foo: "haha", bar: false})) - assert.equal(' haha', render(vm1, {foo: "haha", bar: true})) - }) + var vm = '#macro( d $name)$!name#end #d($foo)'; + var vm1 = '#macro( d $a $b)#if($b)$a#end#end #d ( $foo $bar )'; + + assert.equal(' hanwen', render(vm, { foo: 'hanwen' })); + assert.equal(' ', render(vm)); + assert.equal(' ', render(vm1, { foo: 'haha' })); + assert.equal(' ', render(vm1, { foo: 'haha', bar: false })); + assert.equal(' haha', render(vm1, { foo: 'haha', bar: true })); + }); it('#macro call arguments', function() { - var vm = '#macro( d $a $b $d)$a$b$!d#end #d ( $foo , $bar, $dd )' - assert.equal(' ab', render(vm, {foo: 'a', bar: 'b'})) - assert.equal(' abd', render(vm, {foo: 'a', bar: 'b', dd: 'd'})) - }) + var vm = '#macro( d $a $b $d)$a$b$!d#end #d ( $foo , $bar, $dd )'; + assert.equal(' ab', render(vm, { foo: 'a', bar: 'b' })); + assert.equal(' abd', render(vm, { foo: 'a', bar: 'b', dd: 'd' })); + }); it('#macro map argument', function() { - var vm = '#macro( d $a)#foreach($_item in $a.entrySet())' + - '$_item.key = $_item.value #end#end #d ( {"foo": $foo,"bar":$bar} )' - assert.equal(' foo = a bar = b ', render(vm, {foo: 'a', bar: 'b'})) - }) + var vm = '#macro( d $a)#foreach($_item in $a.entrySet())' + '$_item.key = $_item.value #end#end #d ( {"foo": $foo,"bar":$bar} )'; + assert.equal(' foo = a bar = b ', render(vm, { foo: 'a', bar: 'b' })); + }); it('#noescape', function() { - var vm = '#noescape()$hello#end' - assert.equal('hello world', render(vm, {hello: 'hello world'})) - }) - - }) + var vm = '#noescape()$hello#end'; + assert.equal('hello world', render(vm, { hello: 'hello world' })); + }); + }); describe('Escaping', function() { it('escape slash', function() { - var vm = '#set( $email = "foo" )$email \\$email' - assert.equal('foo $email', render(vm)) - }) + var vm = '#set( $email = "foo" )$email \\$email'; + assert.equal('foo $email', render(vm)); + }); it('double slash', function() { - var vm = '#set( $email = "foo" )\\\\$email \\\\\\$email' - assert.equal("\\foo \\$email", render(vm)) - }) - - }) + var vm = '#set( $email = "foo" )\\\\$email \\\\\\$email'; + assert.equal('\\foo \\$email', render(vm)); + }); + }); describe('Error condiction', function() { - it('css color render', function() { - var vm = 'color: #666 height: 39px' - assert.equal(vm, render(vm)) - }) + var vm = 'color: #666 height: 39px'; + assert.equal(vm, render(vm)); + }); it('jquery parse', function() { - var vm = '$(function() { $("a").click() $.post() })' - assert.equal(vm, render(vm)) - }) + var vm = '$(function() { $("a").click() $.post() })'; + assert.equal(vm, render(vm)); + }); it('issue #7: $ meet with #', function() { - var vm = '$bar.foo()#if(1>0)...#end' - assert.equal('$bar.foo()...', render(vm)) - }) + var vm = '$bar.foo()#if(1>0)...#end'; + assert.equal('$bar.foo()...', render(vm)); + }); it('issue #15', function() { - var vm = '#macro(a $b $list)' + - '#foreach($a in $list)${a}#end $b #end #a("hello", [1, 2])' - assert.equal(' 12 hello ', render(vm)) - }) + var vm = '#macro(a $b $list)' + '#foreach($a in $list)${a}#end $b #end #a("hello", [1, 2])'; + assert.equal(' 12 hello ', render(vm)); + }); it('issue #18', function() { - var vm = '$!tradeDetailModel.goodsInfoModel.goodsTitle' + - '[商品页面]' - var ret = '[商品页面]' - assert.equal(ret, render(vm)) - }) + var vm = + '$!tradeDetailModel.goodsInfoModel.goodsTitle' + + "[商品页面]'; + var ret = '[商品页面]'; + assert.equal(ret, render(vm)); + }); it('issue #18, condiction 2', function() { - var vm = '$!a(**** **** **** $stringUtil.right($!b,4))' - var ret = '(**** **** **** $stringUtil.right($!b,4))' - assert.equal(ret, render(vm)) - }) + var vm = '$!a(**** **** **** $stringUtil.right($!b,4))'; + var ret = '(**** **** **** $stringUtil.right($!b,4))'; + assert.equal(ret, render(vm)); + }); it('# meet with css property', function() { - var vm = '#margin-top:2px' - assert.equal(vm, render(vm)) - }) + var vm = '#margin-top:2px'; + assert.equal(vm, render(vm)); + }); it('var end must in condiction var begin', function() { - var vm = 'stepFareNo:{$!result.getStepFareNo()}' - assert.equal('stepFareNo:{}', render(vm)) - }) + var vm = 'stepFareNo:{$!result.getStepFareNo()}'; + assert.equal('stepFareNo:{}', render(vm)); + }); it('empty string condiction', function() { - assert.equal('', render('')) - assert.equal('', render('##hello')) - assert.equal('hello', render('hello')) - }) - - }) + assert.equal('', render('')); + assert.equal('', render('##hello')); + assert.equal('hello', render('hello')); + }); + }); describe('throw friendly error message', function() { it('print right posiont when error throw', function() { - var vm = '111\nsdfs\n$foo($name)' + var vm = '111\nsdfs\n$foo($name)'; - var compile = new Compile(parse(vm), { escape: false }) + var compile = new Compile(parse(vm), { escape: false }); var context = { name: '', foo: function() { - throw new Error('Run error') - } - } + throw new Error('Run error'); + }, + }; assert.throws(function() { - compile.render(context) - }, /\$foo\(\$name\)/) + compile.render(context); + }, /\$foo\(\$name\)/); assert.throws(function() { - compile.render(context) - }, /Line number 3:0/) - }) + compile.render(context); + }, /Line number 3:0/); + }); it('print error stack of user-defined macro', function() { - var vm = '111\n\n#foo($name)' - var vm1 = '\nhello#parse("vm.vm")' + var vm = '111\n\n#foo($name)'; + var vm1 = '\nhello#parse("vm.vm")'; var files = { 'vm.vm': vm, 'vm1.vm': vm1 }; - var compile = new Compile(parse('\n\n#parse("vm1.vm")')) + var compile = new Compile(parse('\n\n#parse("vm1.vm")')); var macros = { foo: function() { - throw new Error('Run error') + throw new Error('Run error'); }, parse: function(name) { return this.eval(files[name]); - } - } + }, + }; - var expected = '' + - 'Run error\n' + - ' at #foo($name) L/N 3:0\n' + - ' at #parse("vm.vm") L/N 2:5\n' + - ' at #parse("vm1.vm") L/N 3:0'; + var expected = + '' + 'Run error\n' + ' at #foo($name) L/N 3:0\n' + ' at #parse("vm.vm") L/N 2:5\n' + ' at #parse("vm1.vm") L/N 3:0'; try { - compile.render({}, macros) + compile.render({}, macros); } catch (e) { assert.equal(expected, e.message); } - }) - }) - + }); + }); describe('self defined function', function() { - it('$control.setTemplate', function() { - var Control = function() { this.__temp = {}; }; @@ -513,10 +509,8 @@ describe('Compile', function() { constructor: Control, setTemplate: function(vm) { - this.vm = vm; return this; - }, toString: function() { return this.eval(this.vm, this.__temp); @@ -524,110 +518,105 @@ describe('Compile', function() { setParameter: function(key, value) { this.__temp[key] = value; return this; - } + }, }; - var str = 'hello $who, welcome to $where' + var str = 'hello $who, welcome to $where'; - var vm = '$control.setTemplate($str).setParameter("who", "Blob")' + - '.setParameter("where", "China")' + var vm = '$control.setTemplate($str).setParameter("who", "Blob")' + '.setParameter("where", "China")'; var expected = 'hello Blob, welcome to China'; - assert.equal(render(vm, {str: str, control: new Control()}), expected) - - }) - - }) + assert.equal(render(vm, { str: str, control: new Control() }), expected); + }); + }); describe('issues', function() { it('#29', function() { - var vm = '#set($total = 0) #foreach($i in [1,2,3])' + - ' #set($total = $total + $i) #end $total' - assert.equal(render(vm).trim(), "6") - }) + var vm = '#set($total = 0) #foreach($i in [1,2,3])' + ' #set($total = $total + $i) #end $total'; + assert.equal(render(vm).trim(), '6'); + }); it('#30', function() { - var vm = '$foo.setName' - assert.equal(render(vm, { foo: { setName: "foo" }}).trim(), "foo") - }) + var vm = '$foo.setName'; + assert.equal(render(vm, { foo: { setName: 'foo' } }).trim(), 'foo'); + }); it('#54', function() { - var vm = '$a.b.c' - assert.equal(render(vm, { a: { b: null }}).trim(), "$a.b.c") + var vm = '$a.b.c'; + assert.equal(render(vm, { a: { b: null } }).trim(), '$a.b.c'); - vm = '$a.b.c()' - assert.equal(render(vm, { a: { b: null }}).trim(), "$a.b.c()") + vm = '$a.b.c()'; + assert.equal(render(vm, { a: { b: null } }).trim(), '$a.b.c()'); }); - }) + }); describe('multiline', function() { it('#set multiline', function() { - var vm = "$bar.foo()\n#set($foo=$bar)\n..." - assert.equal("$bar.foo()\n...", render(vm)) - }) + var vm = '$bar.foo()\n#set($foo=$bar)\n...'; + assert.equal('$bar.foo()\n...', render(vm)); + }); it('#if multiline', function() { - var vm = "$bar.foo()\n#if(1>0)\n...#end" - assert.equal("$bar.foo()\n...", render(vm)) - }) + var vm = '$bar.foo()\n#if(1>0)\n...#end'; + assert.equal('$bar.foo()\n...', render(vm)); + }); it('#set #set', function() { - var vm = "$bar.foo()\n...\n#set($foo=$bar)\n#set($foo=$bar)" - assert.equal("$bar.foo()\n...\n", render(vm)) - }) + var vm = '$bar.foo()\n...\n#set($foo=$bar)\n#set($foo=$bar)'; + assert.equal('$bar.foo()\n...\n', render(vm)); + }); it('#if multiline #set', function() { - var vm = "$bar.foo()\n#if(1>0)\n#set($foo=$bar)\n...#end" - assert.equal("$bar.foo()\n...", render(vm)) - }) + var vm = '$bar.foo()\n#if(1>0)\n#set($foo=$bar)\n...#end'; + assert.equal('$bar.foo()\n...', render(vm)); + }); it('#if multiline #set #end', function() { - var vm = "$bar.foo()\n#if(1>0)...\n#set($foo=$bar)\n#end" - assert.equal("$bar.foo()\n...\n", render(vm)) - }) + var vm = '$bar.foo()\n#if(1>0)...\n#set($foo=$bar)\n#end'; + assert.equal('$bar.foo()\n...\n', render(vm)); + }); it('with references', function() { - var vm = ['a', - '#foreach($b in $nums)', - '#if($b) ', - 'b', - 'e $b.alm', - '#end', - '#end', - 'c'].join("\n"); - var expected = ['a', 'b', 'e 1', 'b', 'e 2', 'b', 'e 3', 'c'].join("\n") - - var data = {nums:[{alm:1}, {alm:2}, {alm:3}], bar:""}; - assert.equal(expected, render(vm, data)) - }) + var vm = ['a', '#foreach($b in $nums)', '#if($b) ', 'b', 'e $b.alm', '#end', '#end', 'c'].join('\n'); + var expected = ['a', 'b', 'e 1', 'b', 'e 2', 'b', 'e 3', 'c'].join('\n'); + + var data = { nums: [{ alm: 1 }, { alm: 2 }, { alm: 3 }], bar: '' }; + assert.equal(expected, render(vm, data)); + }); it('multiple newlines after statement', function() { - var vm = '#if(1>0)\n\nb#end' - assert.equal('\nb', render(vm)) - }) - }) + var vm = '#if(1>0)\n\nb#end'; + assert.equal('\nb', render(vm)); + }); + }); describe('define support', function() { it('basic', function() { - var vm = '#define($block)\nHello $who#end\n#set($who = "World!")\n$block' - assert.equal('Hello World!', render(vm)) - }) - }) + var vm = '#define($block)\nHello $who#end\n#set($who = "World!")\n$block'; + assert.equal('Hello World!', render(vm)); + }); + }); describe('raw content render', function() { it('simple', function() { var vm = '#[[\nThis content is ignored. $val\n]]#'; - assert.equal('\nThis content is ignored. $val\n', render(vm, { - val: 'foo' - })); + assert.equal( + '\nThis content is ignored. $val\n', + render(vm, { + val: 'foo', + }) + ); }); it('newline after', function() { var vm = '#[[This content is ignored. $val]]#\na'; - assert.equal('This content is ignored. $val\na', render(vm, { - val: 'foo' - })); + assert.equal( + 'This content is ignored. $val\na', + render(vm, { + val: 'foo', + }) + ); }); }); - describe('assignment via .put', function () { + describe('assignment via .put', function() { it('should set a key to an object', function() { var vm = ` #set($foo = {}) @@ -635,24 +624,27 @@ describe('Compile', function() { $foo["foo"] `; var expected = 'bar'; - assert.equal(render(vm).trim(), expected) + assert.equal(render(vm).trim(), expected); }); it('should set a key to an object', function() { var vm = ` $foo.put() `; var expected = 'bar'; - assert.equal(render(vm, { - foo: { - put: function() { - return 'bar'; - } - } - }).trim(), expected) + assert.equal( + render(vm, { + foo: { + put: function() { + return 'bar'; + }, + }, + }).trim(), + expected + ); }); }); - describe('Add into empty array', function () { + describe('Add into empty array', function() { it('should add item to array', function() { var vm = ` #set($foo = []) @@ -660,7 +652,7 @@ describe('Compile', function() { $foo `; var expected = '[foo]'; - assert.equal(render(vm).trim(), expected) + assert.equal(render(vm).trim(), expected); }); it('should add object to array', function() { @@ -670,7 +662,7 @@ describe('Compile', function() { $foo `; var expected = '[{foo=bar}]'; - assert.equal(render(vm).trim(), expected) + assert.equal(render(vm).trim(), expected); }); it('should not add when is object', function() { @@ -680,7 +672,7 @@ describe('Compile', function() { $foo `; var expected = '{}'; - assert.equal(render(vm).trim(), expected) + assert.equal(render(vm).trim(), expected); }); }); @@ -692,7 +684,7 @@ describe('Compile', function() { $bar `; var expected = '[]'; - assert.equal(render(vm).trim(), expected) + assert.equal(render(vm).trim(), expected); }); it('should return a single item', function() { @@ -702,7 +694,7 @@ describe('Compile', function() { $bar `; var expected = '[1]'; - assert.equal(render(vm).trim(), expected) + assert.equal(render(vm).trim(), expected); }); it('should return multiple items', function() { @@ -712,7 +704,7 @@ describe('Compile', function() { $bar `; var expected = '[2, 3]'; - assert.equal(render(vm).trim(), expected) + assert.equal(render(vm).trim(), expected); }); }); @@ -720,13 +712,13 @@ describe('Compile', function() { it('simple object', function() { var vm = '$data'; var expected = '{k=v, k2=v2}'; - assert.equal(render(vm, {data: {k: "v", k2: "v2"}}), expected) + assert.equal(render(vm, { data: { k: 'v', k2: 'v2' } }), expected); }); it('object.keySet()', function() { var vm = '$data.keySet()'; var expected = '[k, k2]'; - assert.equal(render(vm, {data: {k: "v", k2: "v2"}}), expected) + assert.equal(render(vm, { data: { k: 'v', k2: 'v2' } }), expected); }); it('object.keySet() with object that has keySet method', function() { @@ -735,13 +727,13 @@ describe('Compile', function() { function keySet() { return ['k', 'k2']; } - assert.equal(render(vm, {data: { keySet: keySet }}), expected) + assert.equal(render(vm, { data: { keySet: keySet } }), expected); }); it('object.entrySet()', function() { var vm = '$data.entrySet()'; var expected = '[{key=k, value=v}, {key=k2, value=v2}]'; - assert.equal(render(vm, {data: {k: "v", k2: "v2"}}), expected) + assert.equal(render(vm, { data: { k: 'v', k2: 'v2' } }), expected); }); it('object.entrySet() with object that has entrySet method', function() { @@ -750,48 +742,60 @@ describe('Compile', function() { function entrySet() { return { - k: "v", k2: "v2" - } + k: 'v', + k2: 'v2', + }; } - assert.equal(render(vm, {data: { entrySet: entrySet }}), expected) + assert.equal(render(vm, { data: { entrySet: entrySet } }), expected); }); it('nested object', function() { var vm = '$data'; var expected = '{k={k2=v2}, kk={k3=v3}}'; - assert.equal(render(vm, {data: {k: {k2: "v2"}, kk: {k3: "v3"}}}), expected) + assert.equal(render(vm, { data: { k: { k2: 'v2' }, kk: { k3: 'v3' } } }), expected); }); it('object that has toString as own property', function() { var vm = '$data'; var expected = 'custom'; - assert.equal(render(vm, {data: {toString: function() { return 'custom'; }, key: "value", key2: "value2", key3: {key4: "value4"}}}), expected) + assert.equal( + render(vm, { + data: { + toString: function() { + return 'custom'; + }, + key: 'value', + key2: 'value2', + key3: { key4: 'value4' }, + }, + }), + expected + ); }); it('simple array', function() { var vm = '$data'; var expected = '[a, b]'; - assert.equal(render(vm, {data: ["a", "b"]}), expected) + assert.equal(render(vm, { data: ['a', 'b'] }), expected); }); it('nested array', function() { var vm = '$data'; var expected = '[a, [b]]'; - assert.equal(render(vm, {data: ["a", ["b"]]}), expected) + assert.equal(render(vm, { data: ['a', ['b']] }), expected); }); it('object in array', function() { var vm = '$data'; var expected = '[a, {k=v}]'; - assert.equal(render(vm, {data: ["a", {k: "v"}]}), expected) + assert.equal(render(vm, { data: ['a', { k: 'v' }] }), expected); }); it('array in object', function() { var vm = '$data'; var expected = '{k=[a, b]}'; - assert.equal(render(vm, {data: {k: ["a", "b"]}}), expected) + assert.equal(render(vm, { data: { k: ['a', 'b'] } }), expected); }); }); -}) - +}); diff --git a/packages/amplify-velocity-template/tests/foreach.test.js b/packages/amplify-velocity-template/tests/foreach.test.js index 92fb8c3311..9f8ec72a67 100644 --- a/packages/amplify-velocity-template/tests/foreach.test.js +++ b/packages/amplify-velocity-template/tests/foreach.test.js @@ -1,124 +1,115 @@ 'use strict'; -var Velocity = require('../src/velocity') -var assert = require("assert") +var Velocity = require('../src/velocity'); +var assert = require('assert'); var render = Velocity.render; describe('Loops', function() { - it('#foreach', function() { - var vm = '#foreach( $product in $allProducts )
  • $product
  • #end' - var data = {allProducts: ["book", "phone"]} - assert.equal('
  • book
  • phone
  • ', render(vm, data)) - }) + var vm = '#foreach( $product in $allProducts )
  • $product
  • #end'; + var data = { allProducts: ['book', 'phone'] }; + assert.equal('
  • book
  • phone
  • ', render(vm, data)); + }); it('#foreach with map', function() { - var vm = '#foreach($key in $products) name => $products.name #end' - var data = {products: {name: "hanwen"}} - assert.equal(' name => hanwen ', render(vm, data)) - }) + var vm = '#foreach($key in $products) name => $products.name #end'; + var data = { products: { name: 'hanwen' } }; + assert.equal(' name => hanwen ', render(vm, data)); + }); it('#foreach with map hasNext', function() { - var vm = '#foreach($product in $products)$product.name#if($foreach.hasNext),#end#end' - var data = {products: {product1: {name: "hanwen1"}, product2: {name: "hanwen2"}, product3: {name: "hanwen3"}}}; - assert.equal('hanwen1,hanwen2,hanwen3', render(vm, data)) - }) + var vm = '#foreach($product in $products)$product.name#if($foreach.hasNext),#end#end'; + var data = { products: { product1: { name: 'hanwen1' }, product2: { name: 'hanwen2' }, product3: { name: 'hanwen3' } } }; + assert.equal('hanwen1,hanwen2,hanwen3', render(vm, data)); + }); it('#foreach with map hasNext as method', function() { - var vm = '#foreach($product in $products)$product.name#if($foreach.hasNext()),#end#end' - var data = {products: {product1: {name: "hanwen1"}, product2: {name: "hanwen2"}, product3: {name: "hanwen3"}}}; - assert.equal('hanwen1,hanwen2,hanwen3', render(vm, data)) - }) + var vm = '#foreach($product in $products)$product.name#if($foreach.hasNext()),#end#end'; + var data = { products: { product1: { name: 'hanwen1' }, product2: { name: 'hanwen2' }, product3: { name: 'hanwen3' } } }; + assert.equal('hanwen1,hanwen2,hanwen3', render(vm, data)); + }); it('#foreach with map keySet', function() { - var vm = '#foreach($key in $products.keySet())' + - ' $key => $products.get($key) #end' - var data = {products: {name: "hanwen"}} - assert.equal(' name => hanwen ', render(vm, data)) - }) + var vm = '#foreach($key in $products.keySet())' + ' $key => $products.get($key) #end'; + var data = { products: { name: 'hanwen' } }; + assert.equal(' name => hanwen ', render(vm, data)); + }); it('#foreach with nest foreach', function() { - var vm = '#foreach($i in [1..2])${velocityCount}' + - '#foreach($j in [2..3])${velocityCount}#end#end' - assert.equal('112212', render(vm)) - var vm = '#foreach($i in [5..2])$i#end' - assert.equal('5432', render(vm)) - }) + var vm = '#foreach($i in [1..2])${velocityCount}' + '#foreach($j in [2..3])${velocityCount}#end#end'; + assert.equal('112212', render(vm)); + var vm = '#foreach($i in [5..2])$i#end'; + assert.equal('5432', render(vm)); + }); it('#foreach with nest non-empty foreach', function() { - var vm = '#foreach($i in [1..2])' + - '[#foreach($j in [1..2])$j#if($foreach.hasNext),#end#end]' + - '#if($foreach.hasNext),#end#end' - assert.equal('[1,2],[1,2]', render(vm)) - }) + var vm = '#foreach($i in [1..2])' + '[#foreach($j in [1..2])$j#if($foreach.hasNext),#end#end]' + '#if($foreach.hasNext),#end#end'; + assert.equal('[1,2],[1,2]', render(vm)); + }); it('#foreach with nest empty foreach', function() { - var vm = '#foreach($i in [1..2])' + - '[#foreach($j in [])$j#if($foreach.hasNext),#end#end]' + - '#if($foreach.hasNext),#end#end' - assert.equal('[],[]', render(vm)) - }) + var vm = '#foreach($i in [1..2])' + '[#foreach($j in [])$j#if($foreach.hasNext),#end#end]' + '#if($foreach.hasNext),#end#end'; + assert.equal('[],[]', render(vm)); + }); it('#foreach with map entrySet', function() { - var vm = '' + - '#set($js_file = {\n' + - ' "js_arale":"build/js/arale.js?t=20110608",\n' + - ' "js_ma_template":"build/js/ma/template.js?t=20110608",\n' + - ' "js_pa_pa":"build/js/pa/pa.js?t=20110608",\n' + - ' "js_swiff":"build/js/app/swiff.js?t=20110608",\n' + - ' "js_alieditControl":"build/js/pa/alieditcontrol-update.js?"\n' + - '})\n' + - '#foreach($_item in $js_file.entrySet())' + - '$_item.key = $staticServer.getURI("/${_item.value}")\n' + - '#end' - - var ret = 'js_arale = /path/build/js/arale.js?t=20110608\n' + - 'js_ma_template = /path/build/js/ma/template.js?t=20110608\n' + - 'js_pa_pa = /path/build/js/pa/pa.js?t=20110608\n' + - 'js_swiff = /path/build/js/app/swiff.js?t=20110608\n' + - 'js_alieditControl = /path/build/js/pa/alieditcontrol-update.js?\n' + var vm = + '' + + '#set($js_file = {\n' + + ' "js_arale":"build/js/arale.js?t=20110608",\n' + + ' "js_ma_template":"build/js/ma/template.js?t=20110608",\n' + + ' "js_pa_pa":"build/js/pa/pa.js?t=20110608",\n' + + ' "js_swiff":"build/js/app/swiff.js?t=20110608",\n' + + ' "js_alieditControl":"build/js/pa/alieditcontrol-update.js?"\n' + + '})\n' + + '#foreach($_item in $js_file.entrySet())' + + '$_item.key = $staticServer.getURI("/${_item.value}")\n' + + '#end'; + + var ret = + 'js_arale = /path/build/js/arale.js?t=20110608\n' + + 'js_ma_template = /path/build/js/ma/template.js?t=20110608\n' + + 'js_pa_pa = /path/build/js/pa/pa.js?t=20110608\n' + + 'js_swiff = /path/build/js/app/swiff.js?t=20110608\n' + + 'js_alieditControl = /path/build/js/pa/alieditcontrol-update.js?\n'; var data = { staticServer: { getURI: function(url) { - return '/path' + url - } - } - } - - assert.equal(ret.trim(), render(vm, data).trim()) + return '/path' + url; + }, + }, + }; - }) + assert.equal(ret.trim(), render(vm, data).trim()); + }); it('#foreach with #macro, $velocityCount should work, #25', function() { - var vm = '#macro(local) #end ' + - '#foreach ($one in [1,2,4]) #local() $velocityCount #end' - var ret = render(vm).replace(/\s+/g, '') - assert.equal('123', ret) - }) + var vm = '#macro(local) #end ' + '#foreach ($one in [1,2,4]) #local() $velocityCount #end'; + var ret = render(vm).replace(/\s+/g, ''); + assert.equal('123', ret); + }); it('#break', function() { - var vm = '#foreach($num in [1..6])' + - ' #if($foreach.count > 3) #break #end $num #end' - assert.equal(' 1 2 3 4 ', render(vm)) - }) + var vm = '#foreach($num in [1..6])' + ' #if($foreach.count > 3) #break #end $num #end'; + assert.equal(' 1 2 3 4 ', render(vm)); + }); it('#break for map', function() { - var vm = '#foreach($item in $map)' + - ' #if($foreach.count > 2) #break #end $item #end' - var data = {map: {item1: '1', item2: '2', item3: '3', item4: '4'}} - assert.equal(' 1 2 3 ', render(vm, data)) - }) + var vm = '#foreach($item in $map)' + ' #if($foreach.count > 2) #break #end $item #end'; + var data = { map: { item1: '1', item2: '2', item3: '3', item4: '4' } }; + assert.equal(' 1 2 3 ', render(vm, data)); + }); it('foreach for null', function() { var vm = '#foreach($num in $bar) #end'; - assert.equal('', render(vm)) - }) + assert.equal('', render(vm)); + }); it('support #foreach(${itemData} in ${defaultData})', function() { const vm = `#set($allProducts = [1, 2, 3]) #foreach(\${product} in \${allProducts})
  • $product
  • #end`; - const html = render(vm) + const html = render(vm); html.should.containEql('
  • 1
  • '); html.should.containEql('
  • 2
  • '); }); @@ -140,9 +131,9 @@ describe('Loops', function() { #end `; const context = { - records: [[1], [2], [3]] + records: [[1], [2], [3]], }; const ret = render(vm, context); ret.replace(/\s+/g, '').should.equal('matched:"[2]"'); }); -}) +}); diff --git a/packages/amplify-velocity-template/tests/helper.js b/packages/amplify-velocity-template/tests/helper.js index 20ef5cd090..2c872e2440 100644 --- a/packages/amplify-velocity-template/tests/helper.js +++ b/packages/amplify-velocity-template/tests/helper.js @@ -1,59 +1,59 @@ 'use strict'; -var Velocity = require('../src/velocity') -var assert = require("assert") +var Velocity = require('../src/velocity'); +var assert = require('assert'); describe('Helper', function() { - var getRefText = Velocity.Helper.getRefText - var parse = Velocity.parse + var getRefText = Velocity.Helper.getRefText; + var parse = Velocity.parse; describe('getRefText', function() { it('simple reference', function() { - var foo = '$a.b' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + var foo = '$a.b'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference method', function() { - var foo = '$a.b()' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + var foo = '$a.b()'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference method with arguments', function() { - var foo = '$a.b("hello")' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) + var foo = '$a.b("hello")'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); - foo = '$a.b(\'hello\')' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) + foo = "$a.b('hello')"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); - foo = '$a.b(\'hello\',10)' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + foo = "$a.b('hello',10)"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference method with arguments array', function() { - var foo = '$a.b(["hello"])' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) + var foo = '$a.b(["hello"])'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); - foo = '$a.b([\'hello\', 2])' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + foo = "$a.b(['hello', 2])"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference index', function() { - var foo = '$a.b[1]' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - - foo = '$a.b["cc"]' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - - foo = '$a.b[\'cc\']' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) - }) -}) + var foo = '$a.b[1]'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + + foo = '$a.b["cc"]'; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + + foo = "$a.b['cc']"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); + }); +}); diff --git a/packages/amplify-velocity-template/tests/index.ts b/packages/amplify-velocity-template/tests/index.ts index 2bfb4fc487..8b526a0ab2 100644 --- a/packages/amplify-velocity-template/tests/index.ts +++ b/packages/amplify-velocity-template/tests/index.ts @@ -1,5 +1,5 @@ /// -import * as Velocity from 'velocityjs' +import * as Velocity from 'velocityjs'; Velocity.render('hello world'); @@ -9,5 +9,5 @@ const compiler = new Velocity.Compile(Velocity.parse('hello world'), { return value; }, }); -const str = compiler.render({}, {}, true) -console.log(compiler.cost) \ No newline at end of file +const str = compiler.render({}, {}, true); +console.log(compiler.cost); diff --git a/packages/amplify-velocity-template/tests/parse.js b/packages/amplify-velocity-template/tests/parse.js index 526da74f98..06d279222a 100644 --- a/packages/amplify-velocity-template/tests/parse.js +++ b/packages/amplify-velocity-template/tests/parse.js @@ -1,11 +1,9 @@ 'use strict'; -var parse = require('../src/velocity').parse -var assert = require("assert") +var parse = require('../src/velocity').parse; +var assert = require('assert'); describe('Parser', function() { - describe('simple references', function() { - it('self define block', function() { var vm = '#cms(1)
    #H(1,"第一个链接")
    #end'; var ast = parse(vm, { cms: true }); @@ -14,230 +12,223 @@ describe('Parser', function() { }); it('simple references', function() { - var vm = 'hello world: $foo' - var ret = parse(vm) + var vm = 'hello world: $foo'; + var ret = parse(vm); - assert.ok(ret instanceof Array) - assert.equal(2, ret.length) - assert.equal('hello world: ', ret[0]) - assert.equal('foo', ret[1].id) - }) + assert.ok(ret instanceof Array); + assert.equal(2, ret.length); + assert.equal('hello world: ', ret[0]); + assert.equal('foo', ret[1].id); + }); it('valid variable references', function() { - var vm = '$mud-Slinger_1' - assert.equal('mud-Slinger_1', parse(vm)[0].id) - }) + var vm = '$mud-Slinger_1'; + assert.equal('mud-Slinger_1', parse(vm)[0].id); + }); it('wraped references', function() { - var vm = '${mudSlinger}' - var ast = parse(vm)[0] - assert.equal(true, ast.isWraped) - assert.equal('mudSlinger', ast.id) - }) + var vm = '${mudSlinger}'; + var ast = parse(vm)[0]; + assert.equal(true, ast.isWraped); + assert.equal('mudSlinger', ast.id); + }); it('function call references', function() { - var ast = parse('$foo()')[0] - assert.equal(false, ast.args) - assert.equal('references', ast.type) - }) - }) + var ast = parse('$foo()')[0]; + assert.equal(false, ast.args); + assert.equal('references', ast.type); + }); + }); describe('Properties', function() { - it('simple property', function() { - var vm = '$customer.Address' - var asts = parse(vm) + var vm = '$customer.Address'; + var asts = parse(vm); assert.deepEqual(asts[0], { - id: "customer", + id: 'customer', prue: true, - type: "references", - path: [{type: 'property', id: 'Address'}], + type: 'references', + path: [{ type: 'property', id: 'Address' }], leader: '$', - pos: { first_line: 1, last_line: 1, first_column: 0, last_column: 17 } - }) - }) - - }) + pos: { first_line: 1, last_line: 1, first_column: 0, last_column: 17 }, + }); + }); + }); describe('Methods ', function() { - it('with no arguments', function() { - var vm = '$foo.bar()' - var ast = parse(vm)[0] - - assert.deepEqual(ast['path'], [{ - type: "method", - id: "bar", - args: false - }]) - }) + var vm = '$foo.bar()'; + var ast = parse(vm)[0]; + + assert.deepEqual(ast['path'], [ + { + type: 'method', + id: 'bar', + args: false, + }, + ]); + }); it('with arguments integer', function() { - var vm = '$foo.bar(10)' - var ast = parse(vm)[0] - - assert.deepEqual(ast['path'], [{ - type: "method", - id: "bar", - args: [{ - type: "integer", - value: "10" - }] - }]) - }) + var vm = '$foo.bar(10)'; + var ast = parse(vm)[0]; + + assert.deepEqual(ast['path'], [ + { + type: 'method', + id: 'bar', + args: [ + { + type: 'integer', + value: '10', + }, + ], + }, + ]); + }); it('with arguments references', function() { - var vm = '$foo.bar($bar)' - var ast = parse(vm)[0] - - assert.equal(ast.prue, true) - - assert.deepEqual(ast.path[0].args, [{ - type: "references", - leader: "$", - id: "bar" - }]) - }) - - }) + var vm = '$foo.bar($bar)'; + var ast = parse(vm)[0]; + + assert.equal(ast.prue, true); + + assert.deepEqual(ast.path[0].args, [ + { + type: 'references', + leader: '$', + id: 'bar', + }, + ]); + }); + }); describe('Index', function() { - it('all kind of indexs', function() { - var vm = '$foo[0] $foo[$i] $foo["bar"]' - var asts = parse(vm) + var vm = '$foo[0] $foo[$i] $foo["bar"]'; + var asts = parse(vm); - assert.equal(5, asts.length) + assert.equal(5, asts.length); // asts[0].path[0] => $foo[0] // {type: 'index', id: {type: 'integer', value: '0'}} - assert.equal('index', asts[0].path[0].type) - assert.equal('integer', asts[0].path[0].id.type) - assert.equal('0', asts[0].path[0].id.value) + assert.equal('index', asts[0].path[0].type); + assert.equal('integer', asts[0].path[0].id.type); + assert.equal('0', asts[0].path[0].id.value); // asts[2].path[0] => $foo[$i] // {type: 'references', id: {type:'references', id: 'i', leader: '$'}} - assert.equal('index', asts[2].path[0].type) - assert.equal('references', asts[2].path[0].id.type) - assert.equal('i', asts[2].path[0].id.id) + assert.equal('index', asts[2].path[0].type); + assert.equal('references', asts[2].path[0].id.type); + assert.equal('i', asts[2].path[0].id.id); // asts[4].path[0] => $foo["bar"] // {type: 'index', id: {type: 'string', value: 'bar', isEval: true} - assert.equal('index', asts[4].path[0].type) - assert.equal('string', asts[4].path[0].id.type) - assert.equal('bar', asts[4].path[0].id.value) - - }) - - }) + assert.equal('index', asts[4].path[0].type); + assert.equal('string', asts[4].path[0].id.type); + assert.equal('bar', asts[4].path[0].id.value); + }); + }); describe('complex references', function() { - it('property + index + property', function() { - var vm = '$foo.bar[1].junk' - var ast = parse(vm)[0] - - assert.equal('foo', ast.id) - assert.equal(3, ast.path.length) - - var paths = ast.path + var vm = '$foo.bar[1].junk'; + var ast = parse(vm)[0]; - assert.equal('property', paths[0].type) - assert.equal('index', paths[1].type) - assert.equal('property', paths[2].type) + assert.equal('foo', ast.id); + assert.equal(3, ast.path.length); - }) + var paths = ast.path; + assert.equal('property', paths[0].type); + assert.equal('index', paths[1].type); + assert.equal('property', paths[2].type); + }); it('method + index', function() { - var vm = '$foo.callMethod()[1]' - var ast = parse(vm)[0] - - assert.equal(2, ast.path.length) + var vm = '$foo.callMethod()[1]'; + var ast = parse(vm)[0]; - assert.equal('method', ast.path[0].type) - assert.equal('callMethod', ast.path[0].id) + assert.equal(2, ast.path.length); - assert.equal('index', ast.path[1].type) - assert.equal('1', ast.path[1].id.value) - assert.equal('integer', ast.path[1].id.type) + assert.equal('method', ast.path[0].type); + assert.equal('callMethod', ast.path[0].id); - }) + assert.equal('index', ast.path[1].type); + assert.equal('1', ast.path[1].id.value); + assert.equal('integer', ast.path[1].id.type); + }); it('property should not start with alphabet', function() { - var asts = parse('$foo.124') - var ast2 = parse('$foo.-24')[0] + var asts = parse('$foo.124'); + var ast2 = parse('$foo.-24')[0]; - assert.equal(3, asts.length) - assert.equal('foo', asts[0].id) - assert.equal(undefined, asts[0].path) + assert.equal(3, asts.length); + assert.equal('foo', asts[0].id); + assert.equal(undefined, asts[0].path); - assert.equal(undefined, ast2.path) - - }) + assert.equal(undefined, ast2.path); + }); it('index should end with close bracket', function() { assert.throws(function() { - parse("$foo.bar['a'12]") - }, /Parse error/) - }) - - }) + parse("$foo.bar['a'12]"); + }, /Parse error/); + }); + }); describe('Directives', function() { - it('#macro', function() { - var vm = '#macro( d $a $b)#if($b)$a#end#end #d($foo $bar)' - var asts = parse(vm) + var vm = '#macro( d $a $b)#if($b)$a#end#end #d($foo $bar)'; + var asts = parse(vm); - var ifAst = asts[0][1] + var ifAst = asts[0][1]; - assert.equal(ifAst[0].condition.type, 'references') - assert.equal(ifAst[0].condition.id, 'b') - assert.equal(ifAst[0].condition.prue, undefined) + assert.equal(ifAst[0].condition.type, 'references'); + assert.equal(ifAst[0].condition.id, 'b'); + assert.equal(ifAst[0].condition.prue, undefined); - assert.equal(ifAst[1].type, 'references') - assert.equal(ifAst[1].id, 'a') - assert.equal(ifAst[1].prue, true) + assert.equal(ifAst[1].type, 'references'); + assert.equal(ifAst[1].id, 'a'); + assert.equal(ifAst[1].prue, true); - assert.equal(asts.length, 3) - assert.equal(ifAst.length, 2) - }) + assert.equal(asts.length, 3); + assert.equal(ifAst.length, 2); + }); it('#setter will work fine', function() { var vm = ''; var asts = parse(vm); - asts.every(function(ast) { - return typeof ast === 'string'; - }).should.equal(true); + asts + .every(function(ast) { + return typeof ast === 'string'; + }) + .should.equal(true); var vm2 = ''; asts = parse(vm2); asts[1].type.should.equal('macro_call'); }); - - }) + }); describe('comment identify', function() { - it('one line comment', function() { - var asts = parse('#set( $monkey.Number = 123)##number literal') + var asts = parse('#set( $monkey.Number = 123)##number literal'); - assert.equal(2, asts.length) - assert.equal('comment', asts[1].type) - }) + assert.equal(2, asts.length); + assert.equal('comment', asts[1].type); + }); it('all comment', function() { - var asts = parse('##number literal') + var asts = parse('##number literal'); - asts.length.should.equal(1) + asts.length.should.equal(1); asts[0].type.should.equal('comment'); asts = parse('##'); - asts.length.should.equal(1) + asts.length.should.equal(1); asts[0].type.should.equal('comment'); }); - - }) + }); describe('raw identify', function() { it('raw content', function() { @@ -247,5 +238,4 @@ describe('Parser', function() { assert.equal('\nThis content is ignored.\n', asts[0].value); }); }); - -}) +}); diff --git a/packages/amplify-velocity-template/tests/return.test.js b/packages/amplify-velocity-template/tests/return.test.js index 2f855d54bb..1a3894e8e3 100644 --- a/packages/amplify-velocity-template/tests/return.test.js +++ b/packages/amplify-velocity-template/tests/return.test.js @@ -1,36 +1,35 @@ -var Velocity = require('../src/velocity') -var assert = require("assert") -var parse = Velocity.parse -var Compile = Velocity.Compile +var Velocity = require('../src/velocity'); +var assert = require('assert'); +var parse = Velocity.parse; +var Compile = Velocity.Compile; describe('Return', function() { var render = Velocity.render; function getContext(str, context, macros) { - var compile = new Compile(parse(str)) - compile.render(context, macros) - return compile.context + var compile = new Compile(parse(str)); + compile.render(context, macros); + return compile.context; } it('return value', function() { - var tpl = `#return ([1,2,3])` - const html = render(tpl) + var tpl = `#return ([1,2,3])`; + const html = render(tpl); console.log(tpl); - html.should.containEql('[1, 2, 3]') - }) + html.should.containEql('[1, 2, 3]'); + }); it('return empty value', function() { - var tpl = `#return` - const html = render(tpl) + var tpl = `#return`; + const html = render(tpl); console.log(tpl); - html.should.containEql('') - }) + html.should.containEql(''); + }); it('return empty value', function() { - var tpl = `#return(null)` - const html = render(tpl) + var tpl = `#return(null)`; + const html = render(tpl); console.log(tpl); - html.should.containEql('') - }) - -}) + html.should.containEql(''); + }); +}); diff --git a/packages/amplify-velocity-template/tests/runner/assert.js b/packages/amplify-velocity-template/tests/runner/assert.js index 931f2569f7..698ec8edab 100644 --- a/packages/amplify-velocity-template/tests/runner/assert.js +++ b/packages/amplify-velocity-template/tests/runner/assert.js @@ -25,345 +25,348 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. (function(global) { - -// Object.create compatible in IE -var create = Object.create || function(p) { - if (!p) throw Error('no type'); - function f() {}; - f.prototype = p; - return new f(); -}; - -// UTILITY -var util = { - inherits: function(ctor, superCtor) { - ctor.super_ = superCtor; - ctor.prototype = create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); + // Object.create compatible in IE + var create = + Object.create || + function(p) { + if (!p) throw Error('no type'); + function f() {} + f.prototype = p; + return new f(); + }; + + // UTILITY + var util = { + inherits: function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true, + }, + }); + }, + }; + + var pSlice = Array.prototype.slice; + + // 1. The assert module provides functions that throw + // AssertionError's when particular conditions are not met. The + // assert module must conform to the following interface. + + var assert = ok; + + global['assert'] = assert; + + if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = assert; } -}; -var pSlice = Array.prototype.slice; - -// 1. The assert module provides functions that throw -// AssertionError's when particular conditions are not met. The -// assert module must conform to the following interface. + // 2. The AssertionError is defined in assert. + // new assert.AssertionError({ message: message, + // actual: actual, + // expected: expected }) + + assert.AssertionError = function AssertionError(options) { + this.name = 'AssertionError'; + this.message = options.message; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + var stackStartFunction = options.stackStartFunction || fail; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, stackStartFunction); + } + }; + util.inherits(assert.AssertionError, Error); -var assert = ok; + function replacer(key, value) { + if (value === undefined) { + return '' + value; + } + if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) { + return value.toString(); + } + if (typeof value === 'function' || value instanceof RegExp) { + return value.toString(); + } + return value; + } -global['assert'] = assert; + function truncate(s, n) { + if (typeof s == 'string') { + return s.length < n ? s : s.slice(0, n); + } else { + return s; + } + } -if (typeof module === 'object' && typeof module.exports === 'object') { - module.exports = assert; -}; + assert.AssertionError.prototype.toString = function() { + if (this.message) { + return [this.name + ':', this.message].join(' '); + } else { + return [ + this.name + ':', + truncate(JSON.stringify(this.actual, replacer), 128), + this.operator, + truncate(JSON.stringify(this.expected, replacer), 128), + ].join(' '); + } + }; + + // assert.AssertionError instanceof Error + + assert.AssertionError.__proto__ = Error.prototype; + + // At present only the three keys mentioned above are used and + // understood by the spec. Implementations or sub modules can pass + // other keys to the AssertionError's constructor - they will be + // ignored. + + // 3. All of the following functions must throw an AssertionError + // when a corresponding condition is not met, with a message that + // may be undefined if not provided. All assertion methods provide + // both the actual and expected values to the assertion error for + // display purposes. + + function fail(actual, expected, message, operator, stackStartFunction) { + throw new assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator, + stackStartFunction: stackStartFunction, + }); + } -// 2. The AssertionError is defined in assert. -// new assert.AssertionError({ message: message, -// actual: actual, -// expected: expected }) + // EXTENSION! allows for well behaved errors defined elsewhere. + assert.fail = fail; -assert.AssertionError = function AssertionError(options) { - this.name = 'AssertionError'; - this.message = options.message; - this.actual = options.actual; - this.expected = options.expected; - this.operator = options.operator; - var stackStartFunction = options.stackStartFunction || fail; + // 4. Pure assertion tests whether a value is truthy, as determined + // by !!guard. + // assert.ok(guard, message_opt); + // This statement is equivalent to assert.equal(true, !!guard, + // message_opt);. To test strictly for the value true, use + // assert.strictEqual(true, guard, message_opt);. - if (Error.captureStackTrace) { - Error.captureStackTrace(this, stackStartFunction); + function ok(value, message) { + if (!!!value) fail(value, true, message, '==', assert.ok); } -}; -util.inherits(assert.AssertionError, Error); + assert.ok = ok; -function replacer(key, value) { - if (value === undefined) { - return '' + value; - } - if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) { - return value.toString(); - } - if (typeof value === 'function' || value instanceof RegExp) { - return value.toString(); - } - return value; -} - -function truncate(s, n) { - if (typeof s == 'string') { - return s.length < n ? s : s.slice(0, n); - } else { - return s; - } -} - -assert.AssertionError.prototype.toString = function() { - if (this.message) { - return [this.name + ':', this.message].join(' '); - } else { - return [ - this.name + ':', - truncate(JSON.stringify(this.actual, replacer), 128), - this.operator, - truncate(JSON.stringify(this.expected, replacer), 128) - ].join(' '); - } -}; - -// assert.AssertionError instanceof Error - -assert.AssertionError.__proto__ = Error.prototype; - -// At present only the three keys mentioned above are used and -// understood by the spec. Implementations or sub modules can pass -// other keys to the AssertionError's constructor - they will be -// ignored. - -// 3. All of the following functions must throw an AssertionError -// when a corresponding condition is not met, with a message that -// may be undefined if not provided. All assertion methods provide -// both the actual and expected values to the assertion error for -// display purposes. - -function fail(actual, expected, message, operator, stackStartFunction) { - throw new assert.AssertionError({ - message: message, - actual: actual, - expected: expected, - operator: operator, - stackStartFunction: stackStartFunction - }); -} - -// EXTENSION! allows for well behaved errors defined elsewhere. -assert.fail = fail; - -// 4. Pure assertion tests whether a value is truthy, as determined -// by !!guard. -// assert.ok(guard, message_opt); -// This statement is equivalent to assert.equal(true, !!guard, -// message_opt);. To test strictly for the value true, use -// assert.strictEqual(true, guard, message_opt);. - -function ok(value, message) { - if (!!!value) fail(value, true, message, '==', assert.ok); -} -assert.ok = ok; - -// 5. The equality assertion tests shallow, coercive equality with -// ==. -// assert.equal(actual, expected, message_opt); - -assert.equal = function equal(actual, expected, message) { - if (actual != expected) fail(actual, expected, message, '==', assert.equal); -}; - -// 6. The non-equality assertion tests for whether two objects are not equal -// with != assert.notEqual(actual, expected, message_opt); - -assert.notEqual = function notEqual(actual, expected, message) { - if (actual == expected) { - fail(actual, expected, message, '!=', assert.notEqual); - } -}; + // 5. The equality assertion tests shallow, coercive equality with + // ==. + // assert.equal(actual, expected, message_opt); -// 7. The equivalence assertion tests a deep equality relation. -// assert.deepEqual(actual, expected, message_opt); + assert.equal = function equal(actual, expected, message) { + if (actual != expected) fail(actual, expected, message, '==', assert.equal); + }; -assert.deepEqual = function deepEqual(actual, expected, message) { - if (!_deepEqual(actual, expected)) { - fail(actual, expected, message, 'deepEqual', assert.deepEqual); - } -}; + // 6. The non-equality assertion tests for whether two objects are not equal + // with != assert.notEqual(actual, expected, message_opt); -function _deepEqual(actual, expected) { - // 7.1. All identical values are equivalent, as determined by ===. - if (actual === expected) { - return true; + assert.notEqual = function notEqual(actual, expected, message) { + if (actual == expected) { + fail(actual, expected, message, '!=', assert.notEqual); + } + }; -// } else if (Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { -// if (actual.length != expected.length) return false; -// -// for (var i = 0; i < actual.length; i++) { -// if (actual[i] !== expected[i]) return false; -// } -// -// return true; -// - // 7.2. If the expected value is a Date object, the actual value is - // equivalent if it is also a Date object that refers to the same time. - } else if (actual instanceof Date && expected instanceof Date) { - return actual.getTime() === expected.getTime(); - - // 7.3 If the expected value is a RegExp object, the actual value is - // equivalent if it is also a RegExp object with the same source and - // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). - } else if (actual instanceof RegExp && expected instanceof RegExp) { - return actual.source === expected.source && - actual.global === expected.global && - actual.multiline === expected.multiline && - actual.lastIndex === expected.lastIndex && - actual.ignoreCase === expected.ignoreCase; - - // 7.4. Other pairs that do not both pass typeof value == 'object', - // equivalence is determined by ==. - } else if (typeof actual != 'object' && typeof expected != 'object') { - return actual == expected; - - // 7.5 For all other Object pairs, including Array objects, equivalence is - // determined by having the same number of owned properties (as verified - // with Object.prototype.hasOwnProperty.call), the same set of keys - // (although not necessarily the same order), equivalent values for every - // corresponding key, and an identical 'prototype' property. Note: this - // accounts for both named and indexed properties on Arrays. - } else { - return objEquiv(actual, expected); + // 7. The equivalence assertion tests a deep equality relation. + // assert.deepEqual(actual, expected, message_opt); + + assert.deepEqual = function deepEqual(actual, expected, message) { + if (!_deepEqual(actual, expected)) { + fail(actual, expected, message, 'deepEqual', assert.deepEqual); + } + }; + + function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + + // } else if (Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { + // if (actual.length != expected.length) return false; + // + // for (var i = 0; i < actual.length; i++) { + // if (actual[i] !== expected[i]) return false; + // } + // + // return true; + // + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (actual instanceof Date && expected instanceof Date) { + return actual.getTime() === expected.getTime(); + + // 7.3 If the expected value is a RegExp object, the actual value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } else if (actual instanceof RegExp && expected instanceof RegExp) { + return ( + actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase + ); + + // 7.4. Other pairs that do not both pass typeof value == 'object', + // equivalence is determined by ==. + } else if (typeof actual != 'object' && typeof expected != 'object') { + return actual == expected; + + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } } -} -function isUndefinedOrNull(value) { - return value === null || value === undefined; -} + function isUndefinedOrNull(value) { + return value === null || value === undefined; + } -function isArguments(object) { - return Object.prototype.toString.call(object) == '[object Arguments]'; -} + function isArguments(object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; + } -function objEquiv(a, b) { - if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) - return false; - // an identical 'prototype' property. - if (a.prototype !== b.prototype) return false; - //~~~I've managed to break Object.keys through screwy arguments passing. - // Converting to array solves the problem. - if (isArguments(a)) { - if (!isArguments(b)) { - return false; + function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) return false; + // an identical 'prototype' property. + if (a.prototype !== b.prototype) return false; + //~~~I've managed to break Object.keys through screwy arguments passing. + // Converting to array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); } - a = pSlice.call(a); - b = pSlice.call(b); - return _deepEqual(a, b); - } - try { - var ka = Object.keys(a), + try { + var ka = Object.keys(a), kb = Object.keys(b), - key, i; - } catch (e) {//happens when one is a string literal and the other isn't - return false; - } - // having the same number of owned properties (keys incorporates - // hasOwnProperty) - if (ka.length != kb.length) - return false; - //the same set of keys (although not necessarily the same order), - ka.sort(); - kb.sort(); - //~~~cheap key test - for (i = ka.length - 1; i >= 0; i--) { - if (ka[i] != kb[i]) + key, + i; + } catch (e) { + //happens when one is a string literal and the other isn't return false; + } + // having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) return false; + } + return true; } - //equivalent values for every corresponding key, and - //~~~possibly expensive deep test - for (i = ka.length - 1; i >= 0; i--) { - key = ka[i]; - if (!_deepEqual(a[key], b[key])) return false; - } - return true; -} -// 8. The non-equivalence assertion tests for any deep inequality. -// assert.notDeepEqual(actual, expected, message_opt); + // 8. The non-equivalence assertion tests for any deep inequality. + // assert.notDeepEqual(actual, expected, message_opt); -assert.notDeepEqual = function notDeepEqual(actual, expected, message) { - if (_deepEqual(actual, expected)) { - fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); - } -}; + assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (_deepEqual(actual, expected)) { + fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); + } + }; -// 9. The strict equality assertion tests strict equality, as determined by ===. -// assert.strictEqual(actual, expected, message_opt); + // 9. The strict equality assertion tests strict equality, as determined by ===. + // assert.strictEqual(actual, expected, message_opt); -assert.strictEqual = function strictEqual(actual, expected, message) { - if (actual !== expected) { - fail(actual, expected, message, '===', assert.strictEqual); - } -}; + assert.strictEqual = function strictEqual(actual, expected, message) { + if (actual !== expected) { + fail(actual, expected, message, '===', assert.strictEqual); + } + }; -// 10. The strict non-equality assertion tests for strict inequality, as -// determined by !==. assert.notStrictEqual(actual, expected, message_opt); + // 10. The strict non-equality assertion tests for strict inequality, as + // determined by !==. assert.notStrictEqual(actual, expected, message_opt); -assert.notStrictEqual = function notStrictEqual(actual, expected, message) { - if (actual === expected) { - fail(actual, expected, message, '!==', assert.notStrictEqual); - } -}; + assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (actual === expected) { + fail(actual, expected, message, '!==', assert.notStrictEqual); + } + }; -function expectedException(actual, expected) { - if (!actual || !expected) { - return false; - } + function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } - if (expected instanceof RegExp) { - return expected.test(actual); - } else if (actual instanceof expected) { - return true; - } else if (expected.call({}, actual) === true) { - return true; - } + if (expected instanceof RegExp) { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } - return false; -} + return false; + } -function _throws(shouldThrow, block, expected, message) { - var actual; + function _throws(shouldThrow, block, expected, message) { + var actual; - if (typeof expected === 'string') { - message = expected; - expected = null; - } + if (typeof expected === 'string') { + message = expected; + expected = null; + } - try { - block(); - } catch (e) { - actual = e; - } + try { + block(); + } catch (e) { + actual = e; + } - message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + - (message ? ' ' + message : '.'); + message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + (message ? ' ' + message : '.'); - if (shouldThrow && !actual) { - fail('Missing expected exception' + message); - } + if (shouldThrow && !actual) { + fail('Missing expected exception' + message); + } - if (!shouldThrow && expectedException(actual, expected)) { - fail('Got unwanted exception' + message); - } + if (!shouldThrow && expectedException(actual, expected)) { + fail('Got unwanted exception' + message); + } - if ((shouldThrow && actual && expected && - !expectedException(actual, expected)) || (!shouldThrow && actual)) { - throw actual; + if ((shouldThrow && actual && expected && !expectedException(actual, expected)) || (!shouldThrow && actual)) { + throw actual; + } } -} -// 11. Expected to throw an error: -// assert.throws(block, Error_opt, message_opt); + // 11. Expected to throw an error: + // assert.throws(block, Error_opt, message_opt); -assert.throws = function(block, /*optional*/error, /*optional*/message) { - _throws.apply(this, [true].concat(pSlice.call(arguments))); -}; + assert.throws = function(block, /*optional*/ error, /*optional*/ message) { + _throws.apply(this, [true].concat(pSlice.call(arguments))); + }; -// EXTENSION! This is annoying to write outside this module. -assert.doesNotThrow = function(block, /*optional*/error, /*optional*/message) { - _throws.apply(this, [false].concat(pSlice.call(arguments))); -}; - -assert.ifError = function(err) { if (err) {throw err;}}; + // EXTENSION! This is annoying to write outside this module. + assert.doesNotThrow = function(block, /*optional*/ error, /*optional*/ message) { + _throws.apply(this, [false].concat(pSlice.call(arguments))); + }; + assert.ifError = function(err) { + if (err) { + throw err; + } + }; })(this); diff --git a/packages/amplify-velocity-template/tests/runner/index-debug.js b/packages/amplify-velocity-template/tests/runner/index-debug.js index 4d4971cd90..01509de32c 100644 --- a/packages/amplify-velocity-template/tests/runner/index-debug.js +++ b/packages/amplify-velocity-template/tests/runner/index-debug.js @@ -1,55 +1,52 @@ -define("velocityjs/0.4.10/index-debug", [], function(require, exports, module){ -'use strict'; -module.exports = require("velocityjs/0.4.10/src/velocity-debug"); - +define('velocityjs/0.4.10/index-debug', [], function(require, exports, module) { + 'use strict'; + module.exports = require('velocityjs/0.4.10/src/velocity-debug'); }); -define("velocityjs/0.4.10/src/velocity-debug", [], function(require, exports, module){ -var Parser = require("velocityjs/0.4.10/src/parse/index-debug"); -var utils = require("velocityjs/0.4.10/src/utils-debug"); -var Compile = require("velocityjs/0.4.10/src/compile/index-debug"); -var Helper = require("velocityjs/0.4.10/src/helper/index-debug"); - -Compile.Parser = Parser; -Parser._parse = Parser.parse; - -Parser.parse = function (str) { - var asts = Parser._parse(str); - - /** - * remove all newline after all direction such as `#set, #each` - */ - utils.forEach(asts, function trim(ast, i){ - var TRIM_REG = /^[ \t]*\n/; - if (ast.type && ast.type !== 'references') { - var _ast = asts[i + 1]; - if (typeof _ast === 'string' && TRIM_REG.test(_ast)) { - asts[i + 1] = _ast.replace(TRIM_REG, ''); - } - } - }); +define('velocityjs/0.4.10/src/velocity-debug', [], function(require, exports, module) { + var Parser = require('velocityjs/0.4.10/src/parse/index-debug'); + var utils = require('velocityjs/0.4.10/src/utils-debug'); + var Compile = require('velocityjs/0.4.10/src/compile/index-debug'); + var Helper = require('velocityjs/0.4.10/src/helper/index-debug'); + + Compile.Parser = Parser; + Parser._parse = Parser.parse; - return utils.makeLevel(asts); -}; + Parser.parse = function(str) { + var asts = Parser._parse(str); -var Velocity = { - Parser : Parser, - Compile : Compile, - Helper: Helper -}; + /** + * remove all newline after all direction such as `#set, #each` + */ + utils.forEach(asts, function trim(ast, i) { + var TRIM_REG = /^[ \t]*\n/; + if (ast.type && ast.type !== 'references') { + var _ast = asts[i + 1]; + if (typeof _ast === 'string' && TRIM_REG.test(_ast)) { + asts[i + 1] = _ast.replace(TRIM_REG, ''); + } + } + }); -Velocity.render = function (template, context, macros) { + return utils.makeLevel(asts); + }; - var asts = Parser.parse(template); - var compile = new Compile(asts); - return compile.render(context, macros); -}; + var Velocity = { + Parser: Parser, + Compile: Compile, + Helper: Helper, + }; -module.exports = Velocity; + Velocity.render = function(template, context, macros) { + var asts = Parser.parse(template); + var compile = new Compile(asts); + return compile.render(context, macros); + }; + module.exports = Velocity; }); -define("velocityjs/0.4.10/src/parse/index-debug", [], function(require, exports, module){ -/* parser generated by jison 0.4.15 */ -/* +define('velocityjs/0.4.10/src/parse/index-debug', [], function(require, exports, module) { + /* parser generated by jison 0.4.15 */ + /* Returns a Parser object of the following structure: Parser: { @@ -121,2073 +118,3489 @@ define("velocityjs/0.4.10/src/parse/index-debug", [], function(require, exports, recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) } */ -var parser = (function(){ -var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,8],$V1=[1,18],$V2=[1,9],$V3=[1,22],$V4=[1,21],$V5=[4,10,19,33,34,79],$V6=[1,26],$V7=[1,29],$V8=[1,30],$V9=[4,10,19,22,33,34,44,45,46,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81,91],$Va=[1,46],$Vb=[1,51],$Vc=[1,52],$Vd=[1,66],$Ve=[1,67],$Vf=[1,78],$Vg=[1,89],$Vh=[1,81],$Vi=[1,79],$Vj=[1,84],$Vk=[1,88],$Vl=[1,85],$Vm=[1,86],$Vn=[4,10,19,22,33,34,44,45,46,49,50,51,52,53,54,55,56,57,58,59,60,61,71,72,77,79,80,81,91],$Vo=[1,115],$Vp=[1,111],$Vq=[1,112],$Vr=[1,123],$Vs=[22,45,81],$Vt=[2,89],$Vu=[22,44,45,72,81],$Vv=[22,44,45,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81],$Vw=[22,44,45,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81,93],$Vx=[2,102],$Vy=[22,44,45,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81,91],$Vz=[2,105],$VA=[1,132],$VB=[1,138],$VC=[22,44,45],$VD=[1,143],$VE=[1,144],$VF=[1,145],$VG=[1,146],$VH=[1,147],$VI=[1,148],$VJ=[1,149],$VK=[1,150],$VL=[1,151],$VM=[1,152],$VN=[1,153],$VO=[1,154],$VP=[1,155],$VQ=[22,49,50,51,52,53,54,55,56,57,58,59,60,61],$VR=[45,81],$VS=[2,106],$VT=[22,33],$VU=[1,202],$VV=[1,201],$VW=[45,72],$VX=[22,49,50],$VY=[22,49,50,51,52,56,57,58,59,60,61],$VZ=[22,49,50,56,57,58,59,60,61]; -var parser = {trace: function trace() { }, -yy: {}, -symbols_: {"error":2,"root":3,"EOF":4,"statements":5,"statement":6,"references":7,"directives":8,"content":9,"COMMENT":10,"set":11,"if":12,"elseif":13,"else":14,"end":15,"foreach":16,"break":17,"define":18,"HASH":19,"NOESCAPE":20,"PARENTHESIS":21,"CLOSE_PARENTHESIS":22,"macro":23,"macro_call":24,"SET":25,"equal":26,"IF":27,"expression":28,"ELSEIF":29,"ELSE":30,"END":31,"FOREACH":32,"DOLLAR":33,"ID":34,"IN":35,"array":36,"BREAK":37,"DEFINE":38,"MACRO":39,"macro_args":40,"macro_call_args_all":41,"macro_call_args":42,"literals":43,"SPACE":44,"COMMA":45,"EQUAL":46,"map":47,"math":48,"||":49,"&&":50,"+":51,"-":52,"*":53,"/":54,"%":55,">":56,"<":57,"==":58,">=":59,"<=":60,"!=":61,"parenthesis":62,"!":63,"literal":64,"brace_begin":65,"attributes":66,"brace_end":67,"methodbd":68,"VAR_BEGIN":69,"MAP_BEGIN":70,"VAR_END":71,"MAP_END":72,"attribute":73,"method":74,"index":75,"property":76,"DOT":77,"params":78,"CONTENT":79,"BRACKET":80,"CLOSE_BRACKET":81,"string":82,"number":83,"BOOL":84,"integer":85,"INTEGER":86,"DECIMAL_POINT":87,"STRING":88,"EVAL_STRING":89,"range":90,"RANGE":91,"map_item":92,"MAP_SPLIT":93,"$accept":0,"$end":1}, -terminals_: {2:"error",4:"EOF",10:"COMMENT",19:"HASH",20:"NOESCAPE",21:"PARENTHESIS",22:"CLOSE_PARENTHESIS",25:"SET",27:"IF",29:"ELSEIF",30:"ELSE",31:"END",32:"FOREACH",33:"DOLLAR",34:"ID",35:"IN",37:"BREAK",38:"DEFINE",39:"MACRO",44:"SPACE",45:"COMMA",46:"EQUAL",49:"||",50:"&&",51:"+",52:"-",53:"*",54:"/",55:"%",56:">",57:"<",58:"==",59:">=",60:"<=",61:"!=",63:"!",69:"VAR_BEGIN",70:"MAP_BEGIN",71:"VAR_END",72:"MAP_END",77:"DOT",79:"CONTENT",80:"BRACKET",81:"CLOSE_BRACKET",84:"BOOL",86:"INTEGER",87:"DECIMAL_POINT",88:"STRING",89:"EVAL_STRING",91:"RANGE",93:"MAP_SPLIT"}, -productions_: [0,[3,1],[3,2],[5,1],[5,2],[6,1],[6,1],[6,1],[6,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,4],[8,1],[8,1],[11,5],[12,5],[13,5],[14,2],[15,2],[16,8],[16,8],[17,2],[18,6],[23,6],[23,5],[40,1],[40,2],[24,5],[24,4],[42,1],[42,1],[42,3],[42,3],[42,3],[42,3],[41,1],[41,2],[41,3],[41,2],[26,3],[28,1],[28,1],[28,1],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,1],[48,2],[48,2],[48,1],[48,1],[62,3],[7,5],[7,3],[7,5],[7,3],[7,2],[7,4],[7,2],[7,4],[65,1],[65,1],[67,1],[67,1],[66,1],[66,2],[73,1],[73,1],[73,1],[74,2],[68,4],[68,3],[78,1],[78,1],[78,3],[78,3],[76,2],[76,2],[75,3],[75,3],[75,3],[75,2],[75,2],[64,1],[64,1],[64,1],[83,1],[83,3],[83,4],[85,1],[85,2],[82,1],[82,1],[43,1],[43,1],[43,1],[36,3],[36,1],[36,2],[90,5],[90,5],[90,5],[90,5],[47,3],[47,2],[92,3],[92,3],[92,2],[92,5],[92,5],[9,1],[9,1],[9,2],[9,3],[9,3],[9,2]], -performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { -/* this == yyval */ - -var $0 = $$.length - 1; -switch (yystate) { -case 1: - return []; -break; -case 2: - return $$[$0-1]; -break; -case 3: case 31: case 35: case 36: case 80: case 88: case 89: - this.$ = [$$[$0]]; -break; -case 4: case 32: case 81: - this.$ = [].concat($$[$0-1], $$[$0]); -break; -case 5: - $$[$0]['prue'] = true; $$[$0].pos = this._$; this.$ = $$[$0]; -break; -case 6: - $$[$0].pos = this._$; this.$ = $$[$0]; -break; -case 7: case 9: case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 18: case 19: case 41: case 42: case 46: case 47: case 48: case 62: case 65: case 66: case 76: case 77: case 78: case 79: case 85: case 92: case 99: case 100: case 105: case 111: case 113: case 126: case 127: - this.$ = $$[$0]; -break; -case 8: - this.$ = {type: 'comment', value: $$[$0] }; -break; -case 17: - this.$ = { type: 'noescape' }; -break; -case 20: - this.$ = {type: 'set', equal: $$[$0-1] }; -break; -case 21: - this.$ = {type: 'if', condition: $$[$0-1] }; -break; -case 22: - this.$ = {type: 'elseif', condition: $$[$0-1] }; -break; -case 23: - this.$ = {type: 'else' }; -break; -case 24: - this.$ = {type: 'end' }; -break; -case 25: case 26: - this.$ = {type: 'foreach', to: $$[$0-3], from: $$[$0-1] }; -break; -case 27: - this.$ = {type: $$[$0] }; -break; -case 28: - this.$ = {type: 'define', id: $$[$0-1] }; -break; -case 29: - this.$ = {type: 'macro', id: $$[$0-2], args: $$[$0-1] }; -break; -case 30: - this.$ = {type: 'macro', id: $$[$0-1] }; -break; -case 33: - this.$ = { type:"macro_call", id: $$[$0-3].replace(/^\s+|\s+$/g, ''), args: $$[$0-1] }; -break; -case 34: - this.$ = { type:"macro_call", id: $$[$0-2].replace(/^\s+|\s+$/g, '') }; -break; -case 37: case 38: case 39: case 40: case 90: case 91: - this.$ = [].concat($$[$0-2], $$[$0]); -break; -case 43: case 44: case 94: case 95: - this.$ = $$[$0-1]; -break; -case 45: - this.$ = [$$[$0-2], $$[$0]]; -break; -case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: case 58: case 59: case 60: case 61: - this.$ = {type: 'math', expression: [$$[$0-2], $$[$0]], operator: $$[$0-1] }; -break; -case 63: - this.$ = {type: 'math', expression: [$$[$0]], operator: 'minus' }; -break; -case 64: - this.$ = {type: 'math', expression: [$$[$0]], operator: 'not' }; -break; -case 67: - this.$ = {type: 'math', expression: [$$[$0-1]], operator: 'parenthesis' }; -break; -case 68: - this.$ = {type: "references", id: $$[$0-2], path: $$[$0-1], isWraped: true, leader: $$[$0-4] }; -break; -case 69: - this.$ = {type: "references", id: $$[$0-1], path: $$[$0], leader: $$[$0-2] }; -break; -case 70: - this.$ = {type: "references", id: $$[$0-2].id, path: $$[$0-1], isWraped: true, leader: $$[$0-4], args: $$[$0-2].args }; -break; -case 71: - this.$ = {type: "references", id: $$[$0-1].id, path: $$[$0], leader: $$[$0-2], args: $$[$0-1].args }; -break; -case 72: - this.$ = {type: "references", id: $$[$0], leader: $$[$0-1] }; -break; -case 73: - this.$ = {type: "references", id: $$[$0-1], isWraped: true, leader: $$[$0-3] }; -break; -case 74: - this.$ = {type: "references", id: $$[$0].id, leader: $$[$0-1], args: $$[$0].args }; -break; -case 75: - this.$ = {type: "references", id: $$[$0-1].id, isWraped: true, args: $$[$0-1].args, leader: $$[$0-3] }; -break; -case 82: - this.$ = {type:"method", id: $$[$0].id, args: $$[$0].args }; -break; -case 83: - this.$ = {type: "index", id: $$[$0] }; -break; -case 84: - this.$ = {type: "property", id: $$[$0] }; if ($$[$0].type === 'content') this.$ = $$[$0]; -break; -case 86: - this.$ = {id: $$[$0-3], args: $$[$0-1] }; -break; -case 87: - this.$ = {id: $$[$0-2], args: false }; -break; -case 93: - this.$ = {type: 'content', value: $$[$0-1] + $$[$0] }; -break; -case 96: - this.$ = {type: "content", value: $$[$0-2] + $$[$0-1].value + $$[$0] }; -break; -case 97: case 98: - this.$ = {type: "content", value: $$[$0-1] + $$[$0] }; -break; -case 101: - this.$ = {type: 'bool', value: $$[$0] }; -break; -case 102: - this.$ = {type: "integer", value: $$[$0]}; -break; -case 103: - this.$ = {type: "decimal", value: + ($$[$0-2] + '.' + $$[$0]) }; -break; -case 104: - this.$ = {type: "decimal", value: - ($$[$0-2] + '.' + $$[$0]) }; -break; -case 106: - this.$ = - parseInt($$[$0], 10); -break; -case 107: - this.$ = {type: 'string', value: $$[$0] }; -break; -case 108: - this.$ = {type: 'string', value: $$[$0], isEval: true }; -break; -case 109: case 110: - this.$ = $$[$0]; -break; -case 112: - this.$ = {type: 'array', value: $$[$0-1] }; -break; -case 114: - this.$ = {type: 'array', value: [] }; -break; -case 115: case 116: case 117: case 118: - this.$ = {type: 'array', isRange: true, value: [$$[$0-3], $$[$0-1]]}; -break; -case 119: - this.$ = {type: 'map', value: $$[$0-1] }; -break; -case 120: - this.$ = {type: 'map'}; -break; -case 121: case 122: - this.$ = {}; this.$[$$[$0-2].value] = $$[$0]; -break; -case 123: - this.$ = {}; this.$[$$[$0-1].value] = $$[$01]; -break; -case 124: case 125: - this.$ = $$[$0-4]; this.$[$$[$0-2].value] = $$[$0]; -break; -case 128: case 131: - this.$ = $$[$0-1] + $$[$0]; -break; -case 129: - this.$ = $$[$0-2] + $$[$0-1] + $$[$0]; -break; -case 130: - this.$ = $$[$0-2] + $$[$0-1]; -break; -} -}, -table: [{3:1,4:[1,2],5:3,6:4,7:5,8:6,9:7,10:$V0,11:10,12:11,13:12,14:13,15:14,16:15,17:16,18:17,19:$V1,23:19,24:20,33:$V2,34:$V3,79:$V4},{1:[3]},{1:[2,1]},{4:[1,23],6:24,7:5,8:6,9:7,10:$V0,11:10,12:11,13:12,14:13,15:14,16:15,17:16,18:17,19:$V1,23:19,24:20,33:$V2,34:$V3,79:$V4},o($V5,[2,3]),o($V5,[2,5]),o($V5,[2,6]),o($V5,[2,7]),o($V5,[2,8]),{34:$V6,65:25,68:27,69:$V7,70:$V8,79:[1,28]},o($V5,[2,9]),o($V5,[2,10]),o($V5,[2,11]),o($V5,[2,12]),o($V5,[2,13]),o($V5,[2,14]),o($V5,[2,15]),o($V5,[2,16]),{20:[1,31],25:[1,34],27:[1,35],29:[1,36],30:[1,37],31:[1,38],32:[1,39],34:[1,33],37:[1,40],38:[1,41],39:[1,42],79:[1,32]},o($V5,[2,18]),o($V5,[2,19]),o($V5,[2,126]),o($V5,[2,127]),{1:[2,2]},o($V5,[2,4]),{34:[1,43],68:44},o($V9,[2,72],{66:45,73:47,74:48,75:49,76:50,21:$Va,77:$Vb,80:$Vc}),o($V9,[2,74],{73:47,74:48,75:49,76:50,66:53,77:$Vb,80:$Vc}),o($V5,[2,131]),{34:[2,76]},{34:[2,77]},{21:[1,54]},o($V5,[2,128]),{4:[1,56],21:[1,57],79:[1,55]},{21:[1,58]},{21:[1,59]},{21:[1,60]},o($V5,[2,23]),o($V5,[2,24]),{21:[1,61]},o($V5,[2,27]),{21:[1,62]},{21:[1,63]},{21:$Va,66:64,67:65,71:$Vd,72:$Ve,73:47,74:48,75:49,76:50,77:$Vb,80:$Vc},{66:68,67:69,71:$Vd,72:$Ve,73:47,74:48,75:49,76:50,77:$Vb,80:$Vc},o($V9,[2,69],{74:48,75:49,76:50,73:70,77:$Vb,80:$Vc}),{7:74,22:[1,72],33:$Vf,36:75,43:73,47:76,52:$Vg,64:77,70:$Vh,78:71,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},o($Vn,[2,80]),o($Vn,[2,82]),o($Vn,[2,83]),o($Vn,[2,84]),{34:[1,91],68:90,79:[1,92]},{7:94,33:$Vf,52:$Vg,64:93,79:[1,95],81:[1,96],82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},o($V9,[2,71],{74:48,75:49,76:50,73:70,77:$Vb,80:$Vc}),{22:[1,97]},o($V5,[2,129]),o($V5,[2,130]),{7:103,22:[1,99],33:$Vf,36:75,41:98,42:100,43:102,44:[1,101],47:76,52:$Vg,64:77,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},{7:105,26:104,33:$Vf},{7:113,21:$Vo,28:106,33:$Vf,36:107,47:108,48:109,52:$Vp,62:110,63:$Vq,64:114,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},{7:113,21:$Vo,28:116,33:$Vf,36:107,47:108,48:109,52:$Vp,62:110,63:$Vq,64:114,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},{33:[1,117]},{33:[1,118]},{34:[1,119]},{67:120,71:$Vd,72:$Ve,73:70,74:48,75:49,76:50,77:$Vb,80:$Vc},o($V9,[2,73]),o($V9,[2,78]),o($V9,[2,79]),{67:121,71:$Vd,72:$Ve,73:70,74:48,75:49,76:50,77:$Vb,80:$Vc},o($V9,[2,75]),o($Vn,[2,81]),{22:[1,122],45:$Vr},o($Vn,[2,87]),o($Vs,[2,88]),o([22,45],$Vt),o($Vu,[2,109]),o($Vu,[2,110]),o($Vu,[2,111]),{34:$V6,65:25,68:27,69:$V7,70:$V8},{7:127,33:$Vf,36:75,43:73,47:76,52:$Vg,64:77,70:$Vh,78:124,80:$Vi,81:[1,125],82:82,83:83,84:$Vj,85:126,86:$Vk,88:$Vl,89:$Vm,90:80},o($Vu,[2,113]),{72:[1,129],82:130,88:$Vl,89:$Vm,92:128},o($Vv,[2,99]),o($Vv,[2,100]),o($Vv,[2,101]),o($Vw,[2,107]),o($Vw,[2,108]),o($Vv,$Vx),o($Vy,$Vz,{87:[1,131]}),{86:$VA},o($Vn,[2,85]),o($Vn,[2,92],{21:$Va}),o($Vn,[2,93]),{79:[1,134],81:[1,133]},{81:[1,135]},o($Vn,[2,97]),o($Vn,[2,98]),o($V5,[2,17]),{22:[1,136]},o($V5,[2,34]),{22:[2,41],44:[1,137],45:$VB},{7:103,33:$Vf,36:75,42:139,43:102,47:76,52:$Vg,64:77,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},o($VC,[2,35]),o($VC,[2,36]),{22:[1,140]},{46:[1,141]},{22:[1,142]},{22:[2,46]},{22:[2,47]},{22:[2,48],49:$VD,50:$VE,51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ,56:$VK,57:$VL,58:$VM,59:$VN,60:$VO,61:$VP},o($VQ,[2,62]),{21:$Vo,62:156,86:$VA},{7:113,21:$Vo,33:$Vf,48:157,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},o($VQ,[2,65]),o($VQ,[2,66]),{7:113,21:$Vo,33:$Vf,48:158,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{22:[1,159]},{34:[1,160]},{34:[1,161]},{7:164,22:[1,163],33:$Vf,40:162},o($V9,[2,68]),o($V9,[2,70]),o($Vn,[2,86]),{7:166,33:$Vf,36:75,43:165,47:76,52:$Vg,64:77,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},{45:$Vr,81:[1,167]},o($Vu,[2,114]),o($VR,$Vx,{91:[1,168]}),o($VR,$Vt,{91:[1,169]}),{45:[1,171],72:[1,170]},o($Vu,[2,120]),{93:[1,172]},{86:[1,173]},o($Vy,$VS,{87:[1,174]}),o($Vn,[2,94]),o($Vn,[2,96]),o($Vn,[2,95]),o($V5,[2,33]),{7:176,22:[2,44],33:$Vf,36:75,43:175,47:76,52:$Vg,64:77,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},{7:178,33:$Vf,36:75,43:177,47:76,52:$Vg,64:77,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},{22:[2,42],44:[1,179],45:$VB},o($V5,[2,20]),{7:113,21:$Vo,28:180,33:$Vf,36:107,47:108,48:109,52:$Vp,62:110,63:$Vq,64:114,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},o($V5,[2,21]),{7:113,21:$Vo,33:$Vf,48:181,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:182,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:183,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:184,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:185,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:186,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:187,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:188,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:189,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:190,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:191,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:192,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},{7:113,21:$Vo,33:$Vf,48:193,52:$Vp,62:110,63:$Vq,64:114,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm},o($VQ,[2,63]),o($VQ,[2,64]),{22:[1,194],49:$VD,50:$VE,51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ,56:$VK,57:$VL,58:$VM,59:$VN,60:$VO,61:$VP},o($V5,[2,22]),{35:[1,195]},{22:[1,196]},{7:198,22:[1,197],33:$Vf},o($V5,[2,30]),o($VT,[2,31]),o($Vs,[2,90]),o($Vs,[2,91]),o($Vu,[2,112]),{7:200,33:$Vf,52:$VU,85:199,86:$VV},{7:204,33:$Vf,52:$VU,85:203,86:$VV},o($Vu,[2,119]),{82:205,88:$Vl,89:$Vm},o($VW,[2,123],{36:75,47:76,64:77,90:80,82:82,83:83,85:87,43:206,7:207,33:$Vf,52:$Vg,70:$Vh,80:$Vi,84:$Vj,86:$Vk,88:$Vl,89:$Vm}),o($Vv,[2,103]),{86:[1,208]},o($VC,[2,37]),o($VC,[2,40]),o($VC,[2,38]),o($VC,[2,39]),{7:176,22:[2,43],33:$Vf,36:75,43:175,47:76,52:$Vg,64:77,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},{22:[2,45]},o($VX,[2,49],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ,56:$VK,57:$VL,58:$VM,59:$VN,60:$VO,61:$VP}),o($VX,[2,50],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ,56:$VK,57:$VL,58:$VM,59:$VN,60:$VO,61:$VP}),o($VY,[2,51],{53:$VH,54:$VI,55:$VJ}),o($VY,[2,52],{53:$VH,54:$VI,55:$VJ}),o($VQ,[2,53]),o($VQ,[2,54]),o($VQ,[2,55]),o($VZ,[2,56],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ}),o($VZ,[2,57],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ}),o($VZ,[2,58],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ}),o($VZ,[2,59],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ}),o($VZ,[2,60],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ}),o($VZ,[2,61],{51:$VF,52:$VG,53:$VH,54:$VI,55:$VJ}),o($VQ,[2,67]),{7:209,33:$Vf,36:210,80:$Vi,90:80},o($V5,[2,28]),o($V5,[2,29]),o($VT,[2,32]),{81:[1,211]},{81:[1,212]},{81:$Vz},{86:[1,213]},{81:[1,214]},{81:[1,215]},{93:[1,216]},o($VW,[2,121]),o($VW,[2,122]),o($Vv,[2,104]),{22:[1,217]},{22:[1,218]},o($Vu,[2,115]),o($Vu,[2,117]),{81:$VS},o($Vu,[2,116]),o($Vu,[2,118]),{7:219,33:$Vf,36:75,43:220,47:76,52:$Vg,64:77,70:$Vh,80:$Vi,82:82,83:83,84:$Vj,85:87,86:$Vk,88:$Vl,89:$Vm,90:80},o($V5,[2,25]),o($V5,[2,26]),o($VW,[2,124]),o($VW,[2,125])], -defaultActions: {2:[2,1],23:[2,2],29:[2,76],30:[2,77],107:[2,46],108:[2,47],180:[2,45],201:[2,105],213:[2,106]}, -parseError: function parseError(str, hash) { - if (hash.recoverable) { - this.trace(str); - } else { - throw new Error(str); - } -}, -parse: function parse(input) { - var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; - var args = lstack.slice.call(arguments, 1); - var lexer = Object.create(this.lexer); - var sharedState = { yy: {} }; - for (var k in this.yy) { - if (Object.prototype.hasOwnProperty.call(this.yy, k)) { + var parser = (function() { + var o = function(k, v, o, l) { + for (o = o || {}, l = k.length; l--; o[k[l]] = v); + return o; + }, + $V0 = [1, 8], + $V1 = [1, 18], + $V2 = [1, 9], + $V3 = [1, 22], + $V4 = [1, 21], + $V5 = [4, 10, 19, 33, 34, 79], + $V6 = [1, 26], + $V7 = [1, 29], + $V8 = [1, 30], + $V9 = [4, 10, 19, 22, 33, 34, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81, 91], + $Va = [1, 46], + $Vb = [1, 51], + $Vc = [1, 52], + $Vd = [1, 66], + $Ve = [1, 67], + $Vf = [1, 78], + $Vg = [1, 89], + $Vh = [1, 81], + $Vi = [1, 79], + $Vj = [1, 84], + $Vk = [1, 88], + $Vl = [1, 85], + $Vm = [1, 86], + $Vn = [4, 10, 19, 22, 33, 34, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 71, 72, 77, 79, 80, 81, 91], + $Vo = [1, 115], + $Vp = [1, 111], + $Vq = [1, 112], + $Vr = [1, 123], + $Vs = [22, 45, 81], + $Vt = [2, 89], + $Vu = [22, 44, 45, 72, 81], + $Vv = [22, 44, 45, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81], + $Vw = [22, 44, 45, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81, 93], + $Vx = [2, 102], + $Vy = [22, 44, 45, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81, 91], + $Vz = [2, 105], + $VA = [1, 132], + $VB = [1, 138], + $VC = [22, 44, 45], + $VD = [1, 143], + $VE = [1, 144], + $VF = [1, 145], + $VG = [1, 146], + $VH = [1, 147], + $VI = [1, 148], + $VJ = [1, 149], + $VK = [1, 150], + $VL = [1, 151], + $VM = [1, 152], + $VN = [1, 153], + $VO = [1, 154], + $VP = [1, 155], + $VQ = [22, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61], + $VR = [45, 81], + $VS = [2, 106], + $VT = [22, 33], + $VU = [1, 202], + $VV = [1, 201], + $VW = [45, 72], + $VX = [22, 49, 50], + $VY = [22, 49, 50, 51, 52, 56, 57, 58, 59, 60, 61], + $VZ = [22, 49, 50, 56, 57, 58, 59, 60, 61]; + var parser = { + trace: function trace() {}, + yy: {}, + symbols_: { + error: 2, + root: 3, + EOF: 4, + statements: 5, + statement: 6, + references: 7, + directives: 8, + content: 9, + COMMENT: 10, + set: 11, + if: 12, + elseif: 13, + else: 14, + end: 15, + foreach: 16, + break: 17, + define: 18, + HASH: 19, + NOESCAPE: 20, + PARENTHESIS: 21, + CLOSE_PARENTHESIS: 22, + macro: 23, + macro_call: 24, + SET: 25, + equal: 26, + IF: 27, + expression: 28, + ELSEIF: 29, + ELSE: 30, + END: 31, + FOREACH: 32, + DOLLAR: 33, + ID: 34, + IN: 35, + array: 36, + BREAK: 37, + DEFINE: 38, + MACRO: 39, + macro_args: 40, + macro_call_args_all: 41, + macro_call_args: 42, + literals: 43, + SPACE: 44, + COMMA: 45, + EQUAL: 46, + map: 47, + math: 48, + '||': 49, + '&&': 50, + '+': 51, + '-': 52, + '*': 53, + '/': 54, + '%': 55, + '>': 56, + '<': 57, + '==': 58, + '>=': 59, + '<=': 60, + '!=': 61, + parenthesis: 62, + '!': 63, + literal: 64, + brace_begin: 65, + attributes: 66, + brace_end: 67, + methodbd: 68, + VAR_BEGIN: 69, + MAP_BEGIN: 70, + VAR_END: 71, + MAP_END: 72, + attribute: 73, + method: 74, + index: 75, + property: 76, + DOT: 77, + params: 78, + CONTENT: 79, + BRACKET: 80, + CLOSE_BRACKET: 81, + string: 82, + number: 83, + BOOL: 84, + integer: 85, + INTEGER: 86, + DECIMAL_POINT: 87, + STRING: 88, + EVAL_STRING: 89, + range: 90, + RANGE: 91, + map_item: 92, + MAP_SPLIT: 93, + $accept: 0, + $end: 1, + }, + terminals_: { + 2: 'error', + 4: 'EOF', + 10: 'COMMENT', + 19: 'HASH', + 20: 'NOESCAPE', + 21: 'PARENTHESIS', + 22: 'CLOSE_PARENTHESIS', + 25: 'SET', + 27: 'IF', + 29: 'ELSEIF', + 30: 'ELSE', + 31: 'END', + 32: 'FOREACH', + 33: 'DOLLAR', + 34: 'ID', + 35: 'IN', + 37: 'BREAK', + 38: 'DEFINE', + 39: 'MACRO', + 44: 'SPACE', + 45: 'COMMA', + 46: 'EQUAL', + 49: '||', + 50: '&&', + 51: '+', + 52: '-', + 53: '*', + 54: '/', + 55: '%', + 56: '>', + 57: '<', + 58: '==', + 59: '>=', + 60: '<=', + 61: '!=', + 63: '!', + 69: 'VAR_BEGIN', + 70: 'MAP_BEGIN', + 71: 'VAR_END', + 72: 'MAP_END', + 77: 'DOT', + 79: 'CONTENT', + 80: 'BRACKET', + 81: 'CLOSE_BRACKET', + 84: 'BOOL', + 86: 'INTEGER', + 87: 'DECIMAL_POINT', + 88: 'STRING', + 89: 'EVAL_STRING', + 91: 'RANGE', + 93: 'MAP_SPLIT', + }, + productions_: [ + 0, + [3, 1], + [3, 2], + [5, 1], + [5, 2], + [6, 1], + [6, 1], + [6, 1], + [6, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 4], + [8, 1], + [8, 1], + [11, 5], + [12, 5], + [13, 5], + [14, 2], + [15, 2], + [16, 8], + [16, 8], + [17, 2], + [18, 6], + [23, 6], + [23, 5], + [40, 1], + [40, 2], + [24, 5], + [24, 4], + [42, 1], + [42, 1], + [42, 3], + [42, 3], + [42, 3], + [42, 3], + [41, 1], + [41, 2], + [41, 3], + [41, 2], + [26, 3], + [28, 1], + [28, 1], + [28, 1], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 1], + [48, 2], + [48, 2], + [48, 1], + [48, 1], + [62, 3], + [7, 5], + [7, 3], + [7, 5], + [7, 3], + [7, 2], + [7, 4], + [7, 2], + [7, 4], + [65, 1], + [65, 1], + [67, 1], + [67, 1], + [66, 1], + [66, 2], + [73, 1], + [73, 1], + [73, 1], + [74, 2], + [68, 4], + [68, 3], + [78, 1], + [78, 1], + [78, 3], + [78, 3], + [76, 2], + [76, 2], + [75, 3], + [75, 3], + [75, 3], + [75, 2], + [75, 2], + [64, 1], + [64, 1], + [64, 1], + [83, 1], + [83, 3], + [83, 4], + [85, 1], + [85, 2], + [82, 1], + [82, 1], + [43, 1], + [43, 1], + [43, 1], + [36, 3], + [36, 1], + [36, 2], + [90, 5], + [90, 5], + [90, 5], + [90, 5], + [47, 3], + [47, 2], + [92, 3], + [92, 3], + [92, 2], + [92, 5], + [92, 5], + [9, 1], + [9, 1], + [9, 2], + [9, 3], + [9, 3], + [9, 2], + ], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { + /* this == yyval */ + + var $0 = $$.length - 1; + switch (yystate) { + case 1: + return []; + break; + case 2: + return $$[$0 - 1]; + break; + case 3: + case 31: + case 35: + case 36: + case 80: + case 88: + case 89: + this.$ = [$$[$0]]; + break; + case 4: + case 32: + case 81: + this.$ = [].concat($$[$0 - 1], $$[$0]); + break; + case 5: + $$[$0]['prue'] = true; + $$[$0].pos = this._$; + this.$ = $$[$0]; + break; + case 6: + $$[$0].pos = this._$; + this.$ = $$[$0]; + break; + case 7: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 18: + case 19: + case 41: + case 42: + case 46: + case 47: + case 48: + case 62: + case 65: + case 66: + case 76: + case 77: + case 78: + case 79: + case 85: + case 92: + case 99: + case 100: + case 105: + case 111: + case 113: + case 126: + case 127: + this.$ = $$[$0]; + break; + case 8: + this.$ = { type: 'comment', value: $$[$0] }; + break; + case 17: + this.$ = { type: 'noescape' }; + break; + case 20: + this.$ = { type: 'set', equal: $$[$0 - 1] }; + break; + case 21: + this.$ = { type: 'if', condition: $$[$0 - 1] }; + break; + case 22: + this.$ = { type: 'elseif', condition: $$[$0 - 1] }; + break; + case 23: + this.$ = { type: 'else' }; + break; + case 24: + this.$ = { type: 'end' }; + break; + case 25: + case 26: + this.$ = { type: 'foreach', to: $$[$0 - 3], from: $$[$0 - 1] }; + break; + case 27: + this.$ = { type: $$[$0] }; + break; + case 28: + this.$ = { type: 'define', id: $$[$0 - 1] }; + break; + case 29: + this.$ = { type: 'macro', id: $$[$0 - 2], args: $$[$0 - 1] }; + break; + case 30: + this.$ = { type: 'macro', id: $$[$0 - 1] }; + break; + case 33: + this.$ = { type: 'macro_call', id: $$[$0 - 3].replace(/^\s+|\s+$/g, ''), args: $$[$0 - 1] }; + break; + case 34: + this.$ = { type: 'macro_call', id: $$[$0 - 2].replace(/^\s+|\s+$/g, '') }; + break; + case 37: + case 38: + case 39: + case 40: + case 90: + case 91: + this.$ = [].concat($$[$0 - 2], $$[$0]); + break; + case 43: + case 44: + case 94: + case 95: + this.$ = $$[$0 - 1]; + break; + case 45: + this.$ = [$$[$0 - 2], $$[$0]]; + break; + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + case 58: + case 59: + case 60: + case 61: + this.$ = { type: 'math', expression: [$$[$0 - 2], $$[$0]], operator: $$[$0 - 1] }; + break; + case 63: + this.$ = { type: 'math', expression: [$$[$0]], operator: 'minus' }; + break; + case 64: + this.$ = { type: 'math', expression: [$$[$0]], operator: 'not' }; + break; + case 67: + this.$ = { type: 'math', expression: [$$[$0 - 1]], operator: 'parenthesis' }; + break; + case 68: + this.$ = { type: 'references', id: $$[$0 - 2], path: $$[$0 - 1], isWraped: true, leader: $$[$0 - 4] }; + break; + case 69: + this.$ = { type: 'references', id: $$[$0 - 1], path: $$[$0], leader: $$[$0 - 2] }; + break; + case 70: + this.$ = { type: 'references', id: $$[$0 - 2].id, path: $$[$0 - 1], isWraped: true, leader: $$[$0 - 4], args: $$[$0 - 2].args }; + break; + case 71: + this.$ = { type: 'references', id: $$[$0 - 1].id, path: $$[$0], leader: $$[$0 - 2], args: $$[$0 - 1].args }; + break; + case 72: + this.$ = { type: 'references', id: $$[$0], leader: $$[$0 - 1] }; + break; + case 73: + this.$ = { type: 'references', id: $$[$0 - 1], isWraped: true, leader: $$[$0 - 3] }; + break; + case 74: + this.$ = { type: 'references', id: $$[$0].id, leader: $$[$0 - 1], args: $$[$0].args }; + break; + case 75: + this.$ = { type: 'references', id: $$[$0 - 1].id, isWraped: true, args: $$[$0 - 1].args, leader: $$[$0 - 3] }; + break; + case 82: + this.$ = { type: 'method', id: $$[$0].id, args: $$[$0].args }; + break; + case 83: + this.$ = { type: 'index', id: $$[$0] }; + break; + case 84: + this.$ = { type: 'property', id: $$[$0] }; + if ($$[$0].type === 'content') this.$ = $$[$0]; + break; + case 86: + this.$ = { id: $$[$0 - 3], args: $$[$0 - 1] }; + break; + case 87: + this.$ = { id: $$[$0 - 2], args: false }; + break; + case 93: + this.$ = { type: 'content', value: $$[$0 - 1] + $$[$0] }; + break; + case 96: + this.$ = { type: 'content', value: $$[$0 - 2] + $$[$0 - 1].value + $$[$0] }; + break; + case 97: + case 98: + this.$ = { type: 'content', value: $$[$0 - 1] + $$[$0] }; + break; + case 101: + this.$ = { type: 'bool', value: $$[$0] }; + break; + case 102: + this.$ = { type: 'integer', value: $$[$0] }; + break; + case 103: + this.$ = { type: 'decimal', value: +($$[$0 - 2] + '.' + $$[$0]) }; + break; + case 104: + this.$ = { type: 'decimal', value: -($$[$0 - 2] + '.' + $$[$0]) }; + break; + case 106: + this.$ = -parseInt($$[$0], 10); + break; + case 107: + this.$ = { type: 'string', value: $$[$0] }; + break; + case 108: + this.$ = { type: 'string', value: $$[$0], isEval: true }; + break; + case 109: + case 110: + this.$ = $$[$0]; + break; + case 112: + this.$ = { type: 'array', value: $$[$0 - 1] }; + break; + case 114: + this.$ = { type: 'array', value: [] }; + break; + case 115: + case 116: + case 117: + case 118: + this.$ = { type: 'array', isRange: true, value: [$$[$0 - 3], $$[$0 - 1]] }; + break; + case 119: + this.$ = { type: 'map', value: $$[$0 - 1] }; + break; + case 120: + this.$ = { type: 'map' }; + break; + case 121: + case 122: + this.$ = {}; + this.$[$$[$0 - 2].value] = $$[$0]; + break; + case 123: + this.$ = {}; + this.$[$$[$0 - 1].value] = $$[$01]; + break; + case 124: + case 125: + this.$ = $$[$0 - 4]; + this.$[$$[$0 - 2].value] = $$[$0]; + break; + case 128: + case 131: + this.$ = $$[$0 - 1] + $$[$0]; + break; + case 129: + this.$ = $$[$0 - 2] + $$[$0 - 1] + $$[$0]; + break; + case 130: + this.$ = $$[$0 - 2] + $$[$0 - 1]; + break; + } + }, + table: [ + { + 3: 1, + 4: [1, 2], + 5: 3, + 6: 4, + 7: 5, + 8: 6, + 9: 7, + 10: $V0, + 11: 10, + 12: 11, + 13: 12, + 14: 13, + 15: 14, + 16: 15, + 17: 16, + 18: 17, + 19: $V1, + 23: 19, + 24: 20, + 33: $V2, + 34: $V3, + 79: $V4, + }, + { 1: [3] }, + { 1: [2, 1] }, + { + 4: [1, 23], + 6: 24, + 7: 5, + 8: 6, + 9: 7, + 10: $V0, + 11: 10, + 12: 11, + 13: 12, + 14: 13, + 15: 14, + 16: 15, + 17: 16, + 18: 17, + 19: $V1, + 23: 19, + 24: 20, + 33: $V2, + 34: $V3, + 79: $V4, + }, + o($V5, [2, 3]), + o($V5, [2, 5]), + o($V5, [2, 6]), + o($V5, [2, 7]), + o($V5, [2, 8]), + { 34: $V6, 65: 25, 68: 27, 69: $V7, 70: $V8, 79: [1, 28] }, + o($V5, [2, 9]), + o($V5, [2, 10]), + o($V5, [2, 11]), + o($V5, [2, 12]), + o($V5, [2, 13]), + o($V5, [2, 14]), + o($V5, [2, 15]), + o($V5, [2, 16]), + { + 20: [1, 31], + 25: [1, 34], + 27: [1, 35], + 29: [1, 36], + 30: [1, 37], + 31: [1, 38], + 32: [1, 39], + 34: [1, 33], + 37: [1, 40], + 38: [1, 41], + 39: [1, 42], + 79: [1, 32], + }, + o($V5, [2, 18]), + o($V5, [2, 19]), + o($V5, [2, 126]), + o($V5, [2, 127]), + { 1: [2, 2] }, + o($V5, [2, 4]), + { 34: [1, 43], 68: 44 }, + o($V9, [2, 72], { 66: 45, 73: 47, 74: 48, 75: 49, 76: 50, 21: $Va, 77: $Vb, 80: $Vc }), + o($V9, [2, 74], { 73: 47, 74: 48, 75: 49, 76: 50, 66: 53, 77: $Vb, 80: $Vc }), + o($V5, [2, 131]), + { 34: [2, 76] }, + { 34: [2, 77] }, + { 21: [1, 54] }, + o($V5, [2, 128]), + { 4: [1, 56], 21: [1, 57], 79: [1, 55] }, + { 21: [1, 58] }, + { 21: [1, 59] }, + { 21: [1, 60] }, + o($V5, [2, 23]), + o($V5, [2, 24]), + { 21: [1, 61] }, + o($V5, [2, 27]), + { 21: [1, 62] }, + { 21: [1, 63] }, + { 21: $Va, 66: 64, 67: 65, 71: $Vd, 72: $Ve, 73: 47, 74: 48, 75: 49, 76: 50, 77: $Vb, 80: $Vc }, + { 66: 68, 67: 69, 71: $Vd, 72: $Ve, 73: 47, 74: 48, 75: 49, 76: 50, 77: $Vb, 80: $Vc }, + o($V9, [2, 69], { 74: 48, 75: 49, 76: 50, 73: 70, 77: $Vb, 80: $Vc }), + { + 7: 74, + 22: [1, 72], + 33: $Vf, + 36: 75, + 43: 73, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 78: 71, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + o($Vn, [2, 80]), + o($Vn, [2, 82]), + o($Vn, [2, 83]), + o($Vn, [2, 84]), + { 34: [1, 91], 68: 90, 79: [1, 92] }, + { 7: 94, 33: $Vf, 52: $Vg, 64: 93, 79: [1, 95], 81: [1, 96], 82: 82, 83: 83, 84: $Vj, 85: 87, 86: $Vk, 88: $Vl, 89: $Vm }, + o($V9, [2, 71], { 74: 48, 75: 49, 76: 50, 73: 70, 77: $Vb, 80: $Vc }), + { 22: [1, 97] }, + o($V5, [2, 129]), + o($V5, [2, 130]), + { + 7: 103, + 22: [1, 99], + 33: $Vf, + 36: 75, + 41: 98, + 42: 100, + 43: 102, + 44: [1, 101], + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + { 7: 105, 26: 104, 33: $Vf }, + { + 7: 113, + 21: $Vo, + 28: 106, + 33: $Vf, + 36: 107, + 47: 108, + 48: 109, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + { + 7: 113, + 21: $Vo, + 28: 116, + 33: $Vf, + 36: 107, + 47: 108, + 48: 109, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + { 33: [1, 117] }, + { 33: [1, 118] }, + { 34: [1, 119] }, + { 67: 120, 71: $Vd, 72: $Ve, 73: 70, 74: 48, 75: 49, 76: 50, 77: $Vb, 80: $Vc }, + o($V9, [2, 73]), + o($V9, [2, 78]), + o($V9, [2, 79]), + { 67: 121, 71: $Vd, 72: $Ve, 73: 70, 74: 48, 75: 49, 76: 50, 77: $Vb, 80: $Vc }, + o($V9, [2, 75]), + o($Vn, [2, 81]), + { 22: [1, 122], 45: $Vr }, + o($Vn, [2, 87]), + o($Vs, [2, 88]), + o([22, 45], $Vt), + o($Vu, [2, 109]), + o($Vu, [2, 110]), + o($Vu, [2, 111]), + { 34: $V6, 65: 25, 68: 27, 69: $V7, 70: $V8 }, + { + 7: 127, + 33: $Vf, + 36: 75, + 43: 73, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 78: 124, + 80: $Vi, + 81: [1, 125], + 82: 82, + 83: 83, + 84: $Vj, + 85: 126, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + o($Vu, [2, 113]), + { 72: [1, 129], 82: 130, 88: $Vl, 89: $Vm, 92: 128 }, + o($Vv, [2, 99]), + o($Vv, [2, 100]), + o($Vv, [2, 101]), + o($Vw, [2, 107]), + o($Vw, [2, 108]), + o($Vv, $Vx), + o($Vy, $Vz, { 87: [1, 131] }), + { 86: $VA }, + o($Vn, [2, 85]), + o($Vn, [2, 92], { 21: $Va }), + o($Vn, [2, 93]), + { 79: [1, 134], 81: [1, 133] }, + { 81: [1, 135] }, + o($Vn, [2, 97]), + o($Vn, [2, 98]), + o($V5, [2, 17]), + { 22: [1, 136] }, + o($V5, [2, 34]), + { 22: [2, 41], 44: [1, 137], 45: $VB }, + { + 7: 103, + 33: $Vf, + 36: 75, + 42: 139, + 43: 102, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + o($VC, [2, 35]), + o($VC, [2, 36]), + { 22: [1, 140] }, + { 46: [1, 141] }, + { 22: [1, 142] }, + { 22: [2, 46] }, + { 22: [2, 47] }, + { + 22: [2, 48], + 49: $VD, + 50: $VE, + 51: $VF, + 52: $VG, + 53: $VH, + 54: $VI, + 55: $VJ, + 56: $VK, + 57: $VL, + 58: $VM, + 59: $VN, + 60: $VO, + 61: $VP, + }, + o($VQ, [2, 62]), + { 21: $Vo, 62: 156, 86: $VA }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 157, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + o($VQ, [2, 65]), + o($VQ, [2, 66]), + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 158, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { 22: [1, 159] }, + { 34: [1, 160] }, + { 34: [1, 161] }, + { 7: 164, 22: [1, 163], 33: $Vf, 40: 162 }, + o($V9, [2, 68]), + o($V9, [2, 70]), + o($Vn, [2, 86]), + { + 7: 166, + 33: $Vf, + 36: 75, + 43: 165, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + { 45: $Vr, 81: [1, 167] }, + o($Vu, [2, 114]), + o($VR, $Vx, { 91: [1, 168] }), + o($VR, $Vt, { 91: [1, 169] }), + { 45: [1, 171], 72: [1, 170] }, + o($Vu, [2, 120]), + { 93: [1, 172] }, + { 86: [1, 173] }, + o($Vy, $VS, { 87: [1, 174] }), + o($Vn, [2, 94]), + o($Vn, [2, 96]), + o($Vn, [2, 95]), + o($V5, [2, 33]), + { + 7: 176, + 22: [2, 44], + 33: $Vf, + 36: 75, + 43: 175, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + { + 7: 178, + 33: $Vf, + 36: 75, + 43: 177, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + { 22: [2, 42], 44: [1, 179], 45: $VB }, + o($V5, [2, 20]), + { + 7: 113, + 21: $Vo, + 28: 180, + 33: $Vf, + 36: 107, + 47: 108, + 48: 109, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + o($V5, [2, 21]), + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 181, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 182, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 183, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 184, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 185, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 186, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 187, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 188, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 189, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 190, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 191, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 192, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + { + 7: 113, + 21: $Vo, + 33: $Vf, + 48: 193, + 52: $Vp, + 62: 110, + 63: $Vq, + 64: 114, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }, + o($VQ, [2, 63]), + o($VQ, [2, 64]), + { + 22: [1, 194], + 49: $VD, + 50: $VE, + 51: $VF, + 52: $VG, + 53: $VH, + 54: $VI, + 55: $VJ, + 56: $VK, + 57: $VL, + 58: $VM, + 59: $VN, + 60: $VO, + 61: $VP, + }, + o($V5, [2, 22]), + { 35: [1, 195] }, + { 22: [1, 196] }, + { 7: 198, 22: [1, 197], 33: $Vf }, + o($V5, [2, 30]), + o($VT, [2, 31]), + o($Vs, [2, 90]), + o($Vs, [2, 91]), + o($Vu, [2, 112]), + { 7: 200, 33: $Vf, 52: $VU, 85: 199, 86: $VV }, + { 7: 204, 33: $Vf, 52: $VU, 85: 203, 86: $VV }, + o($Vu, [2, 119]), + { 82: 205, 88: $Vl, 89: $Vm }, + o($VW, [2, 123], { + 36: 75, + 47: 76, + 64: 77, + 90: 80, + 82: 82, + 83: 83, + 85: 87, + 43: 206, + 7: 207, + 33: $Vf, + 52: $Vg, + 70: $Vh, + 80: $Vi, + 84: $Vj, + 86: $Vk, + 88: $Vl, + 89: $Vm, + }), + o($Vv, [2, 103]), + { 86: [1, 208] }, + o($VC, [2, 37]), + o($VC, [2, 40]), + o($VC, [2, 38]), + o($VC, [2, 39]), + { + 7: 176, + 22: [2, 43], + 33: $Vf, + 36: 75, + 43: 175, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + { 22: [2, 45] }, + o($VX, [2, 49], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ, 56: $VK, 57: $VL, 58: $VM, 59: $VN, 60: $VO, 61: $VP }), + o($VX, [2, 50], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ, 56: $VK, 57: $VL, 58: $VM, 59: $VN, 60: $VO, 61: $VP }), + o($VY, [2, 51], { 53: $VH, 54: $VI, 55: $VJ }), + o($VY, [2, 52], { 53: $VH, 54: $VI, 55: $VJ }), + o($VQ, [2, 53]), + o($VQ, [2, 54]), + o($VQ, [2, 55]), + o($VZ, [2, 56], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ }), + o($VZ, [2, 57], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ }), + o($VZ, [2, 58], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ }), + o($VZ, [2, 59], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ }), + o($VZ, [2, 60], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ }), + o($VZ, [2, 61], { 51: $VF, 52: $VG, 53: $VH, 54: $VI, 55: $VJ }), + o($VQ, [2, 67]), + { 7: 209, 33: $Vf, 36: 210, 80: $Vi, 90: 80 }, + o($V5, [2, 28]), + o($V5, [2, 29]), + o($VT, [2, 32]), + { 81: [1, 211] }, + { 81: [1, 212] }, + { 81: $Vz }, + { 86: [1, 213] }, + { 81: [1, 214] }, + { 81: [1, 215] }, + { 93: [1, 216] }, + o($VW, [2, 121]), + o($VW, [2, 122]), + o($Vv, [2, 104]), + { 22: [1, 217] }, + { 22: [1, 218] }, + o($Vu, [2, 115]), + o($Vu, [2, 117]), + { 81: $VS }, + o($Vu, [2, 116]), + o($Vu, [2, 118]), + { + 7: 219, + 33: $Vf, + 36: 75, + 43: 220, + 47: 76, + 52: $Vg, + 64: 77, + 70: $Vh, + 80: $Vi, + 82: 82, + 83: 83, + 84: $Vj, + 85: 87, + 86: $Vk, + 88: $Vl, + 89: $Vm, + 90: 80, + }, + o($V5, [2, 25]), + o($V5, [2, 26]), + o($VW, [2, 124]), + o($VW, [2, 125]), + ], + defaultActions: { + 2: [2, 1], + 23: [2, 2], + 29: [2, 76], + 30: [2, 77], + 107: [2, 46], + 108: [2, 47], + 180: [2, 45], + 201: [2, 105], + 213: [2, 106], + }, + parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + throw new Error(str); + } + }, + parse: function parse(input) { + var self = this, + stack = [0], + tstack = [], + vstack = [null], + lstack = [], + table = this.table, + yytext = '', + yylineno = 0, + yyleng = 0, + recovering = 0, + TERROR = 2, + EOF = 1; + var args = lstack.slice.call(arguments, 1); + var lexer = Object.create(this.lexer); + var sharedState = { yy: {} }; + for (var k in this.yy) { + if (Object.prototype.hasOwnProperty.call(this.yy, k)) { sharedState.yy[k] = this.yy[k]; + } } - } - lexer.setInput(input, sharedState.yy); - sharedState.yy.lexer = lexer; - sharedState.yy.parser = this; - if (typeof lexer.yylloc == 'undefined') { - lexer.yylloc = {}; - } - var yyloc = lexer.yylloc; - lstack.push(yyloc); - var ranges = lexer.options && lexer.options.ranges; - if (typeof sharedState.yy.parseError === 'function') { - this.parseError = sharedState.yy.parseError; - } else { - this.parseError = Object.getPrototypeOf(this).parseError; - } - function popStack(n) { - stack.length = stack.length - 2 * n; - vstack.length = vstack.length - n; - lstack.length = lstack.length - n; - } - _token_stack: - function lex() { - var token; - token = lexer.lex() || EOF; - if (typeof token !== 'number') { - token = self.symbols_[token] || token; - } - return token; + lexer.setInput(input, sharedState.yy); + sharedState.yy.lexer = lexer; + sharedState.yy.parser = this; + if (typeof lexer.yylloc == 'undefined') { + lexer.yylloc = {}; } - var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; - while (true) { - state = stack[stack.length - 1]; - if (this.defaultActions[state]) { - action = this.defaultActions[state]; + var yyloc = lexer.yylloc; + lstack.push(yyloc); + var ranges = lexer.options && lexer.options.ranges; + if (typeof sharedState.yy.parseError === 'function') { + this.parseError = sharedState.yy.parseError; } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + _token_stack: function lex() { + var token; + token = lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + } + var symbol, + preErrorSymbol, + state, + action, + a, + r, + yyval = {}, + p, + len, + newState, + expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { if (symbol === null || typeof symbol == 'undefined') { - symbol = lex(); + symbol = lex(); } action = table[state] && table[state][symbol]; - } - if (typeof action === 'undefined' || !action.length || !action[0]) { - var errStr = ''; - expected = []; - for (p in table[state]) { - if (this.terminals_[p] && p > TERROR) { - expected.push('\'' + this.terminals_[p] + '\''); - } - } - if (lexer.showPosition) { - errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; - } else { - errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); - } - this.parseError(errStr, { - text: lexer.match, - token: this.terminals_[symbol] || symbol, - line: lexer.yylineno, - loc: yyloc, - expected: expected - }); + } + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push("'" + this.terminals_[p] + "'"); + } } - if (action[0] instanceof Array && action.length > 1) { + if (lexer.showPosition) { + errStr = + 'Parse error on line ' + + (yylineno + 1) + + ':\n' + + lexer.showPosition() + + '\nExpecting ' + + expected.join(', ') + + ", got '" + + (this.terminals_[symbol] || symbol) + + "'"; + } else { + errStr = + 'Parse error on line ' + + (yylineno + 1) + + ': Unexpected ' + + (symbol == EOF ? 'end of input' : "'" + (this.terminals_[symbol] || symbol) + "'"); + } + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected, + }); + } + if (action[0] instanceof Array && action.length > 1) { throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(lexer.yytext); - lstack.push(lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(lexer.yytext); + lstack.push(lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { yyleng = lexer.yyleng; yytext = lexer.yytext; yylineno = lexer.yylineno; yyloc = lexer.yylloc; if (recovering > 0) { - recovering--; + recovering--; } - } else { + } else { symbol = preErrorSymbol; preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = { + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = { first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, - last_column: lstack[lstack.length - 1].last_column - }; - if (ranges) { - yyval._$.range = [ - lstack[lstack.length - (len || 1)].range[0], - lstack[lstack.length - 1].range[1] - ]; - } - r = this.performAction.apply(yyval, [ - yytext, - yyleng, - yylineno, - sharedState.yy, - action[1], - vstack, - lstack - ].concat(args)); - if (typeof r !== 'undefined') { + last_column: lstack[lstack.length - 1].last_column, + }; + if (ranges) { + yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; + } + r = this.performAction.apply(yyval, [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args)); + if (typeof r !== 'undefined') { return r; - } - if (len) { + } + if (len) { stack = stack.slice(0, -1 * len * 2); vstack = vstack.slice(0, -1 * len); lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } } - } - return true; -}}; -/* generated by jison-lex 0.3.4 */ -var lexer = (function(){ -var lexer = ({ - -EOF:1, + return true; + }, + }; + /* generated by jison-lex 0.3.4 */ + var lexer = (function() { + var lexer = { + EOF: 1, -parseError:function parseError(str, hash) { - if (this.yy.parser) { + parseError: function parseError(str, hash) { + if (this.yy.parser) { this.yy.parser.parseError(str, hash); - } else { + } else { throw new Error(str); - } - }, - -// resets the lexer, sets new input -setInput:function (input, yy) { - this.yy = yy || this.yy || {}; - this._input = input; - this._more = this._backtrack = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = { + } + }, + + // resets the lexer, sets new input + setInput: function(input, yy) { + this.yy = yy || this.yy || {}; + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { first_line: 1, first_column: 0, last_line: 1, - last_column: 0 - }; - if (this.options.ranges) { - this.yylloc.range = [0,0]; - } - this.offset = 0; - return this; - }, - -// consumes and returns one char from the input -input:function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { + last_column: 0, + }; + if (this.options.ranges) { + this.yylloc.range = [0, 0]; + } + this.offset = 0; + return this; + }, + + // consumes and returns one char from the input + input: function() { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { this.yylineno++; this.yylloc.last_line++; - } else { + } else { this.yylloc.last_column++; - } - if (this.options.ranges) { + } + if (this.options.ranges) { this.yylloc.range[1]++; - } + } - this._input = this._input.slice(1); - return ch; - }, + this._input = this._input.slice(1); + return ch; + }, -// unshifts one char (or a string) into the input -unput:function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); + // unshifts one char (or a string) into the input + unput: function(ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length - len); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length - 1); - this.matched = this.matched.substr(0, this.matched.length - 1); + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); - if (lines.length - 1) { + if (lines.length - 1) { this.yylineno -= lines.length - 1; - } - var r = this.yylloc.range; + } + var r = this.yylloc.range; - this.yylloc = { + this.yylloc = { first_line: this.yylloc.first_line, last_line: this.yylineno + 1, first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) - + oldLines[oldLines.length - lines.length].length - lines[0].length : - this.yylloc.first_column - len - }; + last_column: lines + ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - + lines[0].length + : this.yylloc.first_column - len, + }; - if (this.options.ranges) { + if (this.options.ranges) { this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - this.yyleng = this.yytext.length; - return this; - }, - -// When called from action, caches matched text and appends it on next action -more:function () { - this._more = true; - return this; - }, - -// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. -reject:function () { - if (this.options.backtrack_lexer) { + } + this.yyleng = this.yytext.length; + return this; + }, + + // When called from action, caches matched text and appends it on next action + more: function() { + this._more = true; + return this; + }, + + // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. + reject: function() { + if (this.options.backtrack_lexer) { this._backtrack = true; - } else { - return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { - text: "", + } else { + return this.parseError( + 'Lexical error on line ' + + (this.yylineno + 1) + + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + + this.showPosition(), + { + text: '', token: null, - line: this.yylineno - }); + line: this.yylineno, + } + ); + } + return this; + }, - } - return this; - }, - -// retain first n characters of the match -less:function (n) { - this.unput(this.match.slice(n)); - }, - -// displays already matched input, i.e. for error messages -pastInput:function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, - -// displays upcoming input, i.e. for error messages -upcomingInput:function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20-next.length); - } - return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); - }, - -// displays the character position where the lexing error occurred, i.e. for error messages -showPosition:function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c + "^"; - }, - -// test the lexed token: return FALSE when not a match, otherwise return token -test_match:function (match, indexed_rule) { - var token, - lines, - backup; - - if (this.options.backtrack_lexer) { + // retain first n characters of the match + less: function(n) { + this.unput(this.match.slice(n)); + }, + + // displays already matched input, i.e. for error messages + pastInput: function() { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, ''); + }, + + // displays upcoming input, i.e. for error messages + upcomingInput: function() { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20 - next.length); + } + return (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ''); + }, + + // displays the character position where the lexing error occurred, i.e. for error messages + showPosition: function() { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join('-'); + return pre + this.upcomingInput() + '\n' + c + '^'; + }, + + // test the lexed token: return FALSE when not a match, otherwise return token + test_match: function(match, indexed_rule) { + var token, lines, backup; + + if (this.options.backtrack_lexer) { // save context backup = { - yylineno: this.yylineno, - yylloc: { - first_line: this.yylloc.first_line, - last_line: this.last_line, - first_column: this.yylloc.first_column, - last_column: this.yylloc.last_column - }, - yytext: this.yytext, - match: this.match, - matches: this.matches, - matched: this.matched, - yyleng: this.yyleng, - offset: this.offset, - _more: this._more, - _input: this._input, - yy: this.yy, - conditionStack: this.conditionStack.slice(0), - done: this.done + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column, + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done, }; if (this.options.ranges) { - backup.yylloc.range = this.yylloc.range.slice(0); + backup.yylloc.range = this.yylloc.range.slice(0); } - } + } - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) { + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { this.yylineno += lines.length; - } - this.yylloc = { + } + this.yylloc = { first_line: this.yylloc.last_line, last_line: this.yylineno + 1, first_column: this.yylloc.last_column, - last_column: lines ? - lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : - this.yylloc.last_column + match[0].length - }; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._backtrack = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); - if (this.done && this._input) { + last_column: lines + ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length + : this.yylloc.last_column + match[0].length, + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, (this.offset += this.yyleng)]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { this.done = false; - } - if (token) { + } + if (token) { return token; - } else if (this._backtrack) { + } else if (this._backtrack) { // recover context for (var k in backup) { - this[k] = backup[k]; + this[k] = backup[k]; } return false; // rule action called reject() implying the next rule should be tested instead. - } - return false; - }, + } + return false; + }, -// return next match in input -next:function () { - if (this.done) { + // return next match in input + next: function() { + if (this.done) { return this.EOF; - } - if (!this._input) { + } + if (!this._input) { this.done = true; - } + } - var token, - match, - tempMatch, - index; - if (!this._more) { + var token, match, tempMatch, index; + if (!this._more) { this.yytext = ''; this.match = ''; - } - var rules = this._currentRules(); - for (var i = 0; i < rules.length; i++) { + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { tempMatch = this._input.match(this.rules[rules[i]]); if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (this.options.backtrack_lexer) { - token = this.test_match(tempMatch, rules[i]); - if (token !== false) { - return token; - } else if (this._backtrack) { - match = false; - continue; // rule action called reject() implying a rule MISmatch. - } else { - // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) - return false; - } - } else if (!this.options.flex) { - break; + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; } + } else if (!this.options.flex) { + break; + } } - } - if (match) { + } + if (match) { token = this.test_match(match, rules[index]); if (token !== false) { - return token; + return token; } // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; - } - if (this._input === "") { + } + if (this._input === '') { return this.EOF; - } else { + } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { - text: "", - token: null, - line: this.yylineno + text: '', + token: null, + line: this.yylineno, }); - } - }, + } + }, -// return next match that has a token -lex:function lex() { - var r = this.next(); - if (r) { + // return next match that has a token + lex: function lex() { + var r = this.next(); + if (r) { return r; - } else { + } else { return this.lex(); - } - }, + } + }, -// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) -begin:function begin(condition) { - this.conditionStack.push(condition); - }, + // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) + begin: function begin(condition) { + this.conditionStack.push(condition); + }, -// pop the previously active lexer condition state off the condition stack -popState:function popState() { - var n = this.conditionStack.length - 1; - if (n > 0) { + // pop the previously active lexer condition state off the condition stack + popState: function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { return this.conditionStack.pop(); - } else { + } else { return this.conditionStack[0]; - } - }, + } + }, -// produce the lexer rule set which is active for the currently active lexer condition state -_currentRules:function _currentRules() { - if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + // produce the lexer rule set which is active for the currently active lexer condition state + _currentRules: function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; - } else { - return this.conditions["INITIAL"].rules; - } - }, + } else { + return this.conditions['INITIAL'].rules; + } + }, -// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available -topState:function topState(n) { - n = this.conditionStack.length - 1 - Math.abs(n || 0); - if (n >= 0) { + // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available + topState: function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { return this.conditionStack[n]; - } else { - return "INITIAL"; - } - }, - -// alias for begin(condition) -pushState:function pushState(condition) { - this.begin(condition); - }, - -// return the number of states currently on the stack -stateStackSize:function stateStackSize() { - return this.conditionStack.length; - }, -options: {}, -performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { -var YYSTATE=YY_START; -switch($avoiding_name_collisions) { -case 0: - var _reg = /\\+$/; - var _esc = yy_.yytext.match(_reg); - var _num = _esc ? _esc[0].length: null; - /*转义实现,非常恶心,暂时没有好的解决方案*/ - if (!_num || !(_num % 2)) { - this.begin("mu"); - } else { - yy_.yytext = yy_.yytext.replace(/\\$/, ''); - this.begin('esc'); - } - if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); - if(yy_.yytext) return 79; - -break; -case 1: - var _reg = /\\+$/; - var _esc = yy_.yytext.match(_reg); - var _num = _esc ? _esc[0].length: null; - if (!_num || !(_num % 2)) { - this.begin("h"); - } else { - yy_.yytext = yy_.yytext.replace(/\\$/, ''); - this.begin('esc'); - } - if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); - if(yy_.yytext) return 79; - -break; -case 2: return 79; -break; -case 3: this.popState(); return 10; -break; -case 4: this.popState(); yy_.yytext = yy_.yytext.replace(/^#\[\[|\]\]#$/g, ''); return 79 -break; -case 5: this.popState(); return 10; -break; -case 6: return 19; -break; -case 7: return 25; -break; -case 8: return 27; -break; -case 9: return 29; -break; -case 10: this.popState(); return 30; -break; -case 11: this.popState(); return 30; -break; -case 12: this.popState(); return 31; -break; -case 13: this.popState(); return 37; -break; -case 14: return 32; -break; -case 15: return 20; -break; -case 16: return 38; -break; -case 17: return 39; -break; -case 18: return 35; -break; -case 19: return yy_.yytext; -break; -case 20: return yy_.yytext; -break; -case 21: return yy_.yytext; -break; -case 22: return yy_.yytext; -break; -case 23: return yy_.yytext; -break; -case 24: return yy_.yytext; -break; -case 25: return yy_.yytext; -break; -case 26: return yy_.yytext; -break; -case 27: return 33; -break; -case 28: return 33; -break; -case 29: return yy_.yytext; -break; -case 30: return 46; -break; -case 31: - var len = this.stateStackSize(); - if (len >= 2 && this.topState() === 'c' && this.topState(1) === 'run') { - return 44; - } - -break; -case 32: /*ignore whitespace*/ -break; -case 33: return 70; -break; -case 34: return 72; -break; -case 35: return 93; -break; -case 36: yy.begin = true; return 69; -break; -case 37: this.popState(); if (yy.begin === true) { yy.begin = false; return 71;} else { return 79; } -break; -case 38: this.begin("c"); return 21; -break; -case 39: - if (this.popState() === "c") { - var len = this.stateStackSize(); - - if (this.topState() === 'run') { - this.popState(); - len = len - 1; - } - - var tailStack = this.topState(len - 2); - /** 遇到#set(a = b)括号结束后结束状态h*/ - if (len === 2 && tailStack === "h"){ - this.popState(); - } else if (len === 3 && tailStack === "mu" && this.topState(len - 3) === "h") { - // issue#7 $foo#if($a)...#end - this.popState(); - this.popState(); - } - - return 22; - } else { - return 79; - } - -break; -case 40: this.begin("i"); return 80; -break; -case 41: - if (this.popState() === "i") { - return 81; - } else { - return 79; - } - -break; -case 42: return 91; -break; -case 43: return 77; -break; -case 44: return 87; -break; -case 45: return 45; -break; -case 46: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2).replace(/\\"/g,'"'); return 89; -break; -case 47: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2).replace(/\\'/g,"'"); return 88; -break; -case 48: return 84; -break; -case 49: return 84; -break; -case 50: return 84; -break; -case 51: return 86; -break; -case 52: return 34; -break; -case 53: this.begin("run"); return 34; -break; -case 54: this.begin('h'); return 19; -break; -case 55: this.popState(); return 79; -break; -case 56: this.popState(); return 79; -break; -case 57: this.popState(); return 79; -break; -case 58: this.popState(); return 4; -break; -case 59: return 4; -break; -} -}, -rules: [/^(?:[^#]*?(?=\$))/,/^(?:[^\$]*?(?=#))/,/^(?:[^\x00]+)/,/^(?:#\*[\s\S]+?\*#)/,/^(?:#\[\[[\s\S]+?\]\]#)/,/^(?:##[^\n]+)/,/^(?:#(?=[a-zA-Z{]))/,/^(?:set[ ]*)/,/^(?:if[ ]*)/,/^(?:elseif[ ]*)/,/^(?:else\b)/,/^(?:\{else\})/,/^(?:end\b)/,/^(?:break\b)/,/^(?:foreach[ ]*)/,/^(?:noescape\b)/,/^(?:define[ ]*)/,/^(?:macro[ ]*)/,/^(?:in\b)/,/^(?:[%\+\-\*/])/,/^(?:<=)/,/^(?:>=)/,/^(?:[><])/,/^(?:==)/,/^(?:\|\|)/,/^(?:&&)/,/^(?:!=)/,/^(?:\$!(?=[{a-zA-Z_]))/,/^(?:\$(?=[{a-zA-Z_]))/,/^(?:!)/,/^(?:=)/,/^(?:[ ]+(?=[^,]))/,/^(?:\s+)/,/^(?:\{)/,/^(?:\})/,/^(?::[\s]*)/,/^(?:\{)/,/^(?:\})/,/^(?:\([\s]*(?=[$'"\[\{\-0-9\w()!]))/,/^(?:\))/,/^(?:\[[\s]*(?=[\-$"'0-9{\[\]]+))/,/^(?:\])/,/^(?:\.\.)/,/^(?:\.(?=[a-zA-Z_]))/,/^(?:\.(?=[\d]))/,/^(?:,[ ]*)/,/^(?:"(\\"|[^\"])*")/,/^(?:'(\\'|[^\'])*')/,/^(?:null\b)/,/^(?:false\b)/,/^(?:true\b)/,/^(?:[0-9]+)/,/^(?:[_a-zA-Z][a-zA-Z0-9_\-]*)/,/^(?:[_a-zA-Z][a-zA-Z0-9_\-]*[ ]*(?=\())/,/^(?:#)/,/^(?:.)/,/^(?:\s+)/,/^(?:[\$#])/,/^(?:$)/,/^(?:$)/], -conditions: {"mu":{"rules":[5,27,28,36,37,38,39,40,41,43,52,54,55,56,58],"inclusive":false},"c":{"rules":[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,38,39,40,41,43,44,45,46,47,48,49,50,51,52],"inclusive":false},"i":{"rules":[18,19,20,21,22,23,24,25,26,27,28,29,30,32,33,33,34,34,35,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52],"inclusive":false},"h":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,27,28,29,30,35,38,39,40,41,43,51,53,55,56,58],"inclusive":false},"esc":{"rules":[57],"inclusive":false},"run":{"rules":[27,28,29,31,32,33,34,35,38,39,40,41,43,44,45,46,47,48,49,50,51,52,55,56,58],"inclusive":false},"INITIAL":{"rules":[0,1,2,59],"inclusive":true}} -}); -return lexer; -})(); -parser.lexer = lexer; -function Parser () { - this.yy = {}; -} -Parser.prototype = parser;parser.Parser = Parser; -return new Parser; -})(); - - -if (typeof require !== 'undefined' && typeof exports !== 'undefined') { -exports.parser = parser; -exports.Parser = parser.Parser; -exports.parse = function () { return parser.parse.apply(parser, arguments); }; -exports.main = function commonjsMain(args) { - if (!args[1]) { - console.log('Usage: '+args[0]+' FILE'); - process.exit(1); - } - var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); - return exports.parser.parse(source); -}; -if (typeof module !== 'undefined' && require.main === module) { - exports.main(process.argv.slice(1)); -} -} -}); -define("velocityjs/0.4.10/src/utils-debug", [], function(require, exports, module){ -"use strict"; -var utils = {}; - -['forEach', 'some', 'every', 'filter', 'map'].forEach(function(fnName){ - utils[fnName] = function(arr, fn, context){ - if (!arr || typeof arr == 'string') return arr; - context = context || this; - if (arr[fnName]){ - return arr[fnName](fn, context); - } else { - var keys = Object.keys(arr); - return keys[fnName](function(key){ - return fn.call(context, arr[key], key, arr); - }, context); - } - }; -}); + } else { + return 'INITIAL'; + } + }, -var number = 0; -utils.guid = function(){ - return number++; -}; - -utils.mixin = function (to, from){ - utils.forEach(from, function(val, key){ - var toString = {}.toString.call(val); - if (utils.isArray(val) || utils.isObject(val)) { - to[key] = utils.mixin(val, to[key] || {}); - } else { - to[key] = val; - } - }); - return to; -}; + // alias for begin(condition) + pushState: function pushState(condition) { + this.begin(condition); + }, -utils.isArray = function(obj){ - return {}.toString.call(obj) === '[object Array]'; -}; + // return the number of states currently on the stack + stateStackSize: function stateStackSize() { + return this.conditionStack.length; + }, + options: {}, + performAction: function anonymous(yy, yy_, $avoiding_name_collisions, YY_START) { + var YYSTATE = YY_START; + switch ($avoiding_name_collisions) { + case 0: + var _reg = /\\+$/; + var _esc = yy_.yytext.match(_reg); + var _num = _esc ? _esc[0].length : null; + /*转义实现,非常恶心,暂时没有好的解决方案*/ + if (!_num || !(_num % 2)) { + this.begin('mu'); + } else { + yy_.yytext = yy_.yytext.replace(/\\$/, ''); + this.begin('esc'); + } + if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); + if (yy_.yytext) return 79; + + break; + case 1: + var _reg = /\\+$/; + var _esc = yy_.yytext.match(_reg); + var _num = _esc ? _esc[0].length : null; + if (!_num || !(_num % 2)) { + this.begin('h'); + } else { + yy_.yytext = yy_.yytext.replace(/\\$/, ''); + this.begin('esc'); + } + if (_num > 1) yy_.yytext = yy_.yytext.replace(/(\\\\)+$/, '\\'); + if (yy_.yytext) return 79; + + break; + case 2: + return 79; + break; + case 3: + this.popState(); + return 10; + break; + case 4: + this.popState(); + yy_.yytext = yy_.yytext.replace(/^#\[\[|\]\]#$/g, ''); + return 79; + break; + case 5: + this.popState(); + return 10; + break; + case 6: + return 19; + break; + case 7: + return 25; + break; + case 8: + return 27; + break; + case 9: + return 29; + break; + case 10: + this.popState(); + return 30; + break; + case 11: + this.popState(); + return 30; + break; + case 12: + this.popState(); + return 31; + break; + case 13: + this.popState(); + return 37; + break; + case 14: + return 32; + break; + case 15: + return 20; + break; + case 16: + return 38; + break; + case 17: + return 39; + break; + case 18: + return 35; + break; + case 19: + return yy_.yytext; + break; + case 20: + return yy_.yytext; + break; + case 21: + return yy_.yytext; + break; + case 22: + return yy_.yytext; + break; + case 23: + return yy_.yytext; + break; + case 24: + return yy_.yytext; + break; + case 25: + return yy_.yytext; + break; + case 26: + return yy_.yytext; + break; + case 27: + return 33; + break; + case 28: + return 33; + break; + case 29: + return yy_.yytext; + break; + case 30: + return 46; + break; + case 31: + var len = this.stateStackSize(); + if (len >= 2 && this.topState() === 'c' && this.topState(1) === 'run') { + return 44; + } + + break; + case 32 /*ignore whitespace*/: + break; + case 33: + return 70; + break; + case 34: + return 72; + break; + case 35: + return 93; + break; + case 36: + yy.begin = true; + return 69; + break; + case 37: + this.popState(); + if (yy.begin === true) { + yy.begin = false; + return 71; + } else { + return 79; + } + break; + case 38: + this.begin('c'); + return 21; + break; + case 39: + if (this.popState() === 'c') { + var len = this.stateStackSize(); + + if (this.topState() === 'run') { + this.popState(); + len = len - 1; + } -utils.isObject = function(obj){ - return {}.toString.call(obj) === '[object Object]'; -}; + var tailStack = this.topState(len - 2); + /** 遇到#set(a = b)括号结束后结束状态h*/ + if (len === 2 && tailStack === 'h') { + this.popState(); + } else if (len === 3 && tailStack === 'mu' && this.topState(len - 3) === 'h') { + // issue#7 $foo#if($a)...#end + this.popState(); + this.popState(); + } -utils.indexOf = function(elem, arr){ - if (utils.isArray(arr)) { - return arr.indexOf(elem); + return 22; + } else { + return 79; + } + + break; + case 40: + this.begin('i'); + return 80; + break; + case 41: + if (this.popState() === 'i') { + return 81; + } else { + return 79; + } + + break; + case 42: + return 91; + break; + case 43: + return 77; + break; + case 44: + return 87; + break; + case 45: + return 45; + break; + case 46: + yy_.yytext = yy_.yytext.substr(1, yy_.yyleng - 2).replace(/\\"/g, '"'); + return 89; + break; + case 47: + yy_.yytext = yy_.yytext.substr(1, yy_.yyleng - 2).replace(/\\'/g, "'"); + return 88; + break; + case 48: + return 84; + break; + case 49: + return 84; + break; + case 50: + return 84; + break; + case 51: + return 86; + break; + case 52: + return 34; + break; + case 53: + this.begin('run'); + return 34; + break; + case 54: + this.begin('h'); + return 19; + break; + case 55: + this.popState(); + return 79; + break; + case 56: + this.popState(); + return 79; + break; + case 57: + this.popState(); + return 79; + break; + case 58: + this.popState(); + return 4; + break; + case 59: + return 4; + break; + } + }, + rules: [ + /^(?:[^#]*?(?=\$))/, + /^(?:[^\$]*?(?=#))/, + /^(?:[^\x00]+)/, + /^(?:#\*[\s\S]+?\*#)/, + /^(?:#\[\[[\s\S]+?\]\]#)/, + /^(?:##[^\n]+)/, + /^(?:#(?=[a-zA-Z{]))/, + /^(?:set[ ]*)/, + /^(?:if[ ]*)/, + /^(?:elseif[ ]*)/, + /^(?:else\b)/, + /^(?:\{else\})/, + /^(?:end\b)/, + /^(?:break\b)/, + /^(?:foreach[ ]*)/, + /^(?:noescape\b)/, + /^(?:define[ ]*)/, + /^(?:macro[ ]*)/, + /^(?:in\b)/, + /^(?:[%\+\-\*/])/, + /^(?:<=)/, + /^(?:>=)/, + /^(?:[><])/, + /^(?:==)/, + /^(?:\|\|)/, + /^(?:&&)/, + /^(?:!=)/, + /^(?:\$!(?=[{a-zA-Z_]))/, + /^(?:\$(?=[{a-zA-Z_]))/, + /^(?:!)/, + /^(?:=)/, + /^(?:[ ]+(?=[^,]))/, + /^(?:\s+)/, + /^(?:\{)/, + /^(?:\})/, + /^(?::[\s]*)/, + /^(?:\{)/, + /^(?:\})/, + /^(?:\([\s]*(?=[$'"\[\{\-0-9\w()!]))/, + /^(?:\))/, + /^(?:\[[\s]*(?=[\-$"'0-9{\[\]]+))/, + /^(?:\])/, + /^(?:\.\.)/, + /^(?:\.(?=[a-zA-Z_]))/, + /^(?:\.(?=[\d]))/, + /^(?:,[ ]*)/, + /^(?:"(\\"|[^\"])*")/, + /^(?:'(\\'|[^\'])*')/, + /^(?:null\b)/, + /^(?:false\b)/, + /^(?:true\b)/, + /^(?:[0-9]+)/, + /^(?:[_a-zA-Z][a-zA-Z0-9_\-]*)/, + /^(?:[_a-zA-Z][a-zA-Z0-9_\-]*[ ]*(?=\())/, + /^(?:#)/, + /^(?:.)/, + /^(?:\s+)/, + /^(?:[\$#])/, + /^(?:$)/, + /^(?:$)/, + ], + conditions: { + mu: { rules: [5, 27, 28, 36, 37, 38, 39, 40, 41, 43, 52, 54, 55, 56, 58], inclusive: false }, + c: { + rules: [ + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 38, + 39, + 40, + 41, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + ], + inclusive: false, + }, + i: { + rules: [ + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 32, + 33, + 33, + 34, + 34, + 35, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + ], + inclusive: false, + }, + h: { + rules: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 27, 28, 29, 30, 35, 38, 39, 40, 41, 43, 51, 53, 55, 56, 58], + inclusive: false, + }, + esc: { rules: [57], inclusive: false }, + run: { + rules: [27, 28, 29, 31, 32, 33, 34, 35, 38, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 55, 56, 58], + inclusive: false, + }, + INITIAL: { rules: [0, 1, 2, 59], inclusive: true }, + }, + }; + return lexer; + })(); + parser.lexer = lexer; + function Parser() { + this.yy = {}; + } + Parser.prototype = parser; + parser.Parser = Parser; + return new Parser(); + })(); + + if (typeof require !== 'undefined' && typeof exports !== 'undefined') { + exports.parser = parser; + exports.Parser = parser.Parser; + exports.parse = function() { + return parser.parse.apply(parser, arguments); + }; + exports.main = function commonjsMain(args) { + if (!args[1]) { + console.log('Usage: ' + args[0] + ' FILE'); + process.exit(1); + } + var source = require('fs').readFileSync(require('path').normalize(args[1]), 'utf8'); + return exports.parser.parse(source); + }; + if (typeof module !== 'undefined' && require.main === module) { + exports.main(process.argv.slice(1)); + } } -}; - -utils.keys = Object.keys; -utils.now = Date.now; - -function makeLevel(block, index){ +}); +define('velocityjs/0.4.10/src/utils-debug', [], function(require, exports, module) { + 'use strict'; + var utils = {}; + + ['forEach', 'some', 'every', 'filter', 'map'].forEach(function(fnName) { + utils[fnName] = function(arr, fn, context) { + if (!arr || typeof arr == 'string') return arr; + context = context || this; + if (arr[fnName]) { + return arr[fnName](fn, context); + } else { + var keys = Object.keys(arr); + return keys[fnName](function(key) { + return fn.call(context, arr[key], key, arr); + }, context); + } + }; + }); - var blockTypes = { - 'if': 1, - 'foreach': 1, - 'macro': 1, - 'noescape': 1, - 'define': 1 + var number = 0; + utils.guid = function() { + return number++; }; - var len = block.length; - index = index || 0; - var ret = []; - var ignore = index - 1; - - for (var i = index; i < len; i++) { - - if (i <= ignore) continue; - - var ast = block[i]; - var type = ast.type; - - if (!blockTypes[type] && type !== 'end') { - - ret.push(ast); - - } else if (type === 'end') { + utils.mixin = function(to, from) { + utils.forEach(from, function(val, key) { + var toString = {}.toString.call(val); + if (utils.isArray(val) || utils.isObject(val)) { + to[key] = utils.mixin(val, to[key] || {}); + } else { + to[key] = val; + } + }); + return to; + }; - return {arr: ret, step: i}; + utils.isArray = function(obj) { + return {}.toString.call(obj) === '[object Array]'; + }; - } else { + utils.isObject = function(obj) { + return {}.toString.call(obj) === '[object Object]'; + }; - var _ret = makeLevel(block, i + 1); - ignore = _ret.step; - _ret.arr.unshift(block[i]); - ret.push(_ret.arr); + utils.indexOf = function(elem, arr) { + if (utils.isArray(arr)) { + return arr.indexOf(elem); + } + }; + utils.keys = Object.keys; + utils.now = Date.now; + + function makeLevel(block, index) { + var blockTypes = { + if: 1, + foreach: 1, + macro: 1, + noescape: 1, + define: 1, + }; + + var len = block.length; + index = index || 0; + var ret = []; + var ignore = index - 1; + + for (var i = index; i < len; i++) { + if (i <= ignore) continue; + + var ast = block[i]; + var type = ast.type; + + if (!blockTypes[type] && type !== 'end') { + ret.push(ast); + } else if (type === 'end') { + return { arr: ret, step: i }; + } else { + var _ret = makeLevel(block, i + 1); + ignore = _ret.step; + _ret.arr.unshift(block[i]); + ret.push(_ret.arr); + } } + return ret; } - return ret; -} - -utils.makeLevel = makeLevel; - -module.exports = utils; + utils.makeLevel = makeLevel; + module.exports = utils; }); -define("velocityjs/0.4.10/src/compile/index-debug", [], function(require, exports, module){ -var utils = require("velocityjs/0.4.10/src/utils-debug"); -var Helper = require("velocityjs/0.4.10/src/helper/index-debug"); -function Velocity(asts, config) { - this.asts = asts; - this.config = { - // 自动输出为经过html encode输出 - escape: true, - // 不需要转义的白名单 - unescape: {} +define('velocityjs/0.4.10/src/compile/index-debug', [], function(require, exports, module) { + var utils = require('velocityjs/0.4.10/src/utils-debug'); + var Helper = require('velocityjs/0.4.10/src/helper/index-debug'); + function Velocity(asts, config) { + this.asts = asts; + this.config = { + // 自动输出为经过html encode输出 + escape: true, + // 不需要转义的白名单 + unescape: {}, + }; + utils.mixin(this.config, config); + this.init(); + } + + Velocity.Helper = Helper; + Velocity.prototype = { + constructor: Velocity, }; - utils.mixin(this.config, config); - this.init(); -} - -Velocity.Helper = Helper; -Velocity.prototype = { - constructor: Velocity -}; - -require("velocityjs/0.4.10/src/compile/blocks-debug")(Velocity, utils); -require("velocityjs/0.4.10/src/compile/literal-debug")(Velocity, utils); -require("velocityjs/0.4.10/src/compile/references-debug")(Velocity, utils); -require("velocityjs/0.4.10/src/compile/set-debug")(Velocity, utils); -require("velocityjs/0.4.10/src/compile/expression-debug")(Velocity, utils); -require("velocityjs/0.4.10/src/compile/compile-debug")(Velocity, utils); -module.exports = Velocity; + require('velocityjs/0.4.10/src/compile/blocks-debug')(Velocity, utils); + require('velocityjs/0.4.10/src/compile/literal-debug')(Velocity, utils); + require('velocityjs/0.4.10/src/compile/references-debug')(Velocity, utils); + require('velocityjs/0.4.10/src/compile/set-debug')(Velocity, utils); + require('velocityjs/0.4.10/src/compile/expression-debug')(Velocity, utils); + require('velocityjs/0.4.10/src/compile/compile-debug')(Velocity, utils); + module.exports = Velocity; }); -define("velocityjs/0.4.10/src/helper/index-debug", [], function(require, exports, module){ -var Helper = {}; -var utils = require("velocityjs/0.4.10/src/utils-debug"); -require("velocityjs/0.4.10/src/helper/text-debug")(Helper, utils); -module.exports = Helper; - +define('velocityjs/0.4.10/src/helper/index-debug', [], function(require, exports, module) { + var Helper = {}; + var utils = require('velocityjs/0.4.10/src/utils-debug'); + require('velocityjs/0.4.10/src/helper/text-debug')(Helper, utils); + module.exports = Helper; }); -define("velocityjs/0.4.10/src/helper/text-debug", [], function(require, exports, module){ -module.exports = function(Helper, utils){ - /** - * 获取引用文本,当引用自身不存在的情况下,需要返回原来的模板字符串 - */ - function getRefText(ast){ - - var ret = ast.leader; - var isFn = ast.args !== undefined; - - if (ast.type === 'macro_call') { - ret = '#'; - } - - if (ast.isWraped) ret += '{'; - - if (isFn) { - ret += getMethodText(ast); - } else { - ret += ast.id; - } - - utils.forEach(ast.path, function(ref){ - //不支持method并且传递参数 - if (ref.type == 'method') { - ret += '.' + getMethodText(ref); - } else if (ref.type == 'index') { - - var text = ''; - var id = ref.id; - - if (id.type === 'integer') { - - text = id.value; - - } else if (id.type === 'string') { - - var sign = id.isEval? '"': "'"; - text = sign + id.value + sign; - - } else { - - text = getRefText(id); - - } - - ret += '[' + text + ']'; - - } else if (ref.type == 'property') { - - ret += '.' + ref.id; +define('velocityjs/0.4.10/src/helper/text-debug', [], function(require, exports, module) { + module.exports = function(Helper, utils) { + /** + * 获取引用文本,当引用自身不存在的情况下,需要返回原来的模板字符串 + */ + function getRefText(ast) { + var ret = ast.leader; + var isFn = ast.args !== undefined; + if (ast.type === 'macro_call') { + ret = '#'; } - }, this); + if (ast.isWraped) ret += '{'; - if (ast.isWraped) ret += '}'; - - return ret; - } - - function getMethodText(ref) { - - var args = []; - var ret = ''; - - utils.forEach(ref.args, function(arg){ - args.push(getLiteral(arg)); - }); - - ret += ref.id + '(' + args.join(',') + ')'; - - return ret; - - } - - function getLiteral(ast){ - - var ret = ''; - - switch(ast.type) { - - case 'string': { - var sign = ast.isEval? '"': "'"; - ret = sign + ast.value + sign; - break; + if (isFn) { + ret += getMethodText(ast); + } else { + ret += ast.id; } - case 'integer': - case 'bool' : { - ret = ast.value; - break; - } + utils.forEach( + ast.path, + function(ref) { + //不支持method并且传递参数 + if (ref.type == 'method') { + ret += '.' + getMethodText(ref); + } else if (ref.type == 'index') { + var text = ''; + var id = ref.id; + + if (id.type === 'integer') { + text = id.value; + } else if (id.type === 'string') { + var sign = id.isEval ? '"' : "'"; + text = sign + id.value + sign; + } else { + text = getRefText(id); + } - case 'array': { - ret = '['; - var len = ast.value.length - 1; - utils.forEach(ast.value, function(arg, i){ - ret += getLiteral(arg); - if (i !== len) ret += ', '; - }); - ret += ']'; - break; - } + ret += '[' + text + ']'; + } else if (ref.type == 'property') { + ret += '.' + ref.id; + } + }, + this + ); - default: - ret = getRefText(ast) + if (ast.isWraped) ret += '}'; + + return ret; } - return ret; - } + function getMethodText(ref) { + var args = []; + var ret = ''; - Helper.getRefText = getRefText; -}; + utils.forEach(ref.args, function(arg) { + args.push(getLiteral(arg)); + }); -}); -define("velocityjs/0.4.10/src/compile/blocks-debug", [], function(require, exports, module){ -module.exports = function(Velocity, utils){ + ret += ref.id + '(' + args.join(',') + ')'; - /** - * blocks语法处理 - */ - utils.mixin(Velocity.prototype, { - /** - * 处理代码库: if foreach macro - */ - getBlock: function(block) { + return ret; + } - var ast = block[0]; + function getLiteral(ast) { var ret = ''; switch (ast.type) { - case 'if': - ret = this.getBlockIf(block); - break; - case 'foreach': - ret = this.getBlockEach(block); + case 'string': { + var sign = ast.isEval ? '"' : "'"; + ret = sign + ast.value + sign; break; - case 'macro': - this.setBlockMacro(block); - break; - case 'noescape': - ret = this._render(block.slice(1)); + } + + case 'integer': + case 'bool': { + ret = ast.value; break; - case 'define': - this.setBlockDefine(block); + } + + case 'array': { + ret = '['; + var len = ast.value.length - 1; + utils.forEach(ast.value, function(arg, i) { + ret += getLiteral(arg); + if (i !== len) ret += ', '; + }); + ret += ']'; break; + } + default: - ret = this._render(block); + ret = getRefText(ast); } - return ret || ''; - }, - - /** - * define - */ - setBlockDefine: function(block){ - var ast = block[0]; - var _block = block.slice(1); - var defines = this.defines; - - defines[ast.id] = _block; - }, + return ret; + } + Helper.getRefText = getRefText; + }; +}); +define('velocityjs/0.4.10/src/compile/blocks-debug', [], function(require, exports, module) { + module.exports = function(Velocity, utils) { /** - * define macro + * blocks语法处理 */ - setBlockMacro: function(block){ - var ast = block[0]; - var _block = block.slice(1); - var macros = this.macros; - - macros[ast.id] = { - asts: _block, - args: ast.args - }; - }, + utils.mixin(Velocity.prototype, { + /** + * 处理代码库: if foreach macro + */ + getBlock: function(block) { + var ast = block[0]; + var ret = ''; - /** - * parse macro call - */ - getMacro: function(ast){ - var macro = this.macros[ast.id]; - var ret = ''; + switch (ast.type) { + case 'if': + ret = this.getBlockIf(block); + break; + case 'foreach': + ret = this.getBlockEach(block); + break; + case 'macro': + this.setBlockMacro(block); + break; + case 'noescape': + ret = this._render(block.slice(1)); + break; + case 'define': + this.setBlockDefine(block); + break; + default: + ret = this._render(block); + } - if (!macro) { + return ret || ''; + }, - var jsmacros = this.jsmacros; - macro = jsmacros[ast.id]; - var jsArgs = []; + /** + * define + */ + setBlockDefine: function(block) { + var ast = block[0]; + var _block = block.slice(1); + var defines = this.defines; - if (macro && macro.apply) { + defines[ast.id] = _block; + }, - utils.forEach(ast.args, function(a){ - jsArgs.push(this.getLiteral(a)); - }, this); + /** + * define macro + */ + setBlockMacro: function(block) { + var ast = block[0]; + var _block = block.slice(1); + var macros = this.macros; + + macros[ast.id] = { + asts: _block, + args: ast.args, + }; + }, - try { - ret = macro.apply(this, jsArgs); - } catch(e){ - var pos = ast.pos; - var text = Velocity.Helper.getRefText(ast); - // throws error tree - var err = '\n at ' + text + ' L/N ' + pos.first_line + ':' + pos.first_column; - e.name = ''; - e.message += err; - throw new Error(e); + /** + * parse macro call + */ + getMacro: function(ast) { + var macro = this.macros[ast.id]; + var ret = ''; + + if (!macro) { + var jsmacros = this.jsmacros; + macro = jsmacros[ast.id]; + var jsArgs = []; + + if (macro && macro.apply) { + utils.forEach( + ast.args, + function(a) { + jsArgs.push(this.getLiteral(a)); + }, + this + ); + + try { + ret = macro.apply(this, jsArgs); + } catch (e) { + var pos = ast.pos; + var text = Velocity.Helper.getRefText(ast); + // throws error tree + var err = '\n at ' + text + ' L/N ' + pos.first_line + ':' + pos.first_column; + e.name = ''; + e.message += err; + throw new Error(e); + } } - + } else { + var asts = macro.asts; + var args = macro.args; + var _call_args = ast.args; + var local = {}; + var localKey = []; + var guid = utils.guid(); + var contextId = 'macro:' + ast.id + ':' + guid; + + utils.forEach( + args, + function(ref, i) { + if (_call_args[i]) { + local[ref.id] = this.getLiteral(_call_args[i]); + } else { + local[ref.id] = undefined; + } + }, + this + ); + + ret = this.eval(asts, local, contextId); } - } else { - var asts = macro.asts; - var args = macro.args; - var _call_args = ast.args; - var local = {}; - var localKey = []; - var guid = utils.guid(); - var contextId = 'macro:' + ast.id + ':' + guid; + return ret; + }, - utils.forEach(args, function(ref, i){ - if (_call_args[i]) { - local[ref.id] = this.getLiteral(_call_args[i]); + /** + * eval + * @param str {array|string} 需要解析的字符串 + * @param local {object} 局部变量 + * @param contextId {string} + * @return {string} + */ + eval: function(str, local, contextId) { + if (!local) { + if (utils.isArray(str)) { + return this._render(str); } else { - local[ref.id] = undefined; + return this.evalStr(str); } - }, this); - - ret = this.eval(asts, local, contextId); - } - - return ret; - }, - - /** - * eval - * @param str {array|string} 需要解析的字符串 - * @param local {object} 局部变量 - * @param contextId {string} - * @return {string} - */ - eval: function(str, local, contextId){ - - if (!local) { - - if (utils.isArray(str)) { - return this._render(str); } else { - return this.evalStr(str); - } - - } else { - - var asts = []; - var Parser = Velocity.Parser; - contextId = contextId || ('eval:' + utils.guid()); - - if (utils.isArray(str)) { - - asts = str; - - } else if (Parser) { + var asts = []; + var Parser = Velocity.Parser; + contextId = contextId || 'eval:' + utils.guid(); + + if (utils.isArray(str)) { + asts = str; + } else if (Parser) { + asts = Parser.parse(str); + } - asts = Parser.parse(str); + if (asts.length) { + this.local[contextId] = local; + var ret = this._render(asts, contextId); + this.local[contextId] = {}; + this.conditions.shift(); + this.condition = this.conditions[0] || ''; + return ret; + } } + }, - if (asts.length) { + /** + * parse #foreach + */ + getBlockEach: function(block) { + var ast = block[0]; + var _from = this.getLiteral(ast.from); + var _block = block.slice(1); + var _to = ast.to; + var local = { + foreach: { + count: 0, + }, + }; + var ret = ''; + var guid = utils.guid(); + var contextId = 'foreach:' + guid; + + var type = {}.toString.call(_from); + if (!_from || (type !== '[object Array]' && type !== '[object Object]')) return; + + var len = utils.isArray(_from) ? _from.length : utils.keys(_from).length; + + utils.forEach( + _from, + function(val, i) { + if (this.setBreak) return; + //构造临时变量 + local[_to] = val; + //TODO: here, the foreach variable give to local, when _from is not an + //array, count and hasNext would be undefined, also i is not the + //index. + local['foreach']['count'] = i + 1; + local['foreach']['index'] = i; + local['foreach']['hasNext'] = i + 1 < len; + local['velocityCount'] = i + 1; + this.local[contextId] = local; + ret += this._render(_block, contextId); + }, + this + ); + + this.setBreak = false; + //删除临时变量 + this.local[contextId] = {}; + this.conditions.shift(); + this.condition = this.conditions[0] || ''; - this.local[contextId] = local; - var ret = this._render(asts, contextId); - this.local[contextId] = {}; - this.conditions.shift(); - this.condition = this.conditions[0] || ''; + return ret; + }, - return ret; - } + /** + * parse #if + */ + getBlockIf: function(block) { + var str = ''; + var received = false; + var asts = []; - } + utils.some( + block, + function(ast) { + if (ast.condition) { + if (received) return true; + received = this.getExpression(ast.condition); + } else if (ast.type === 'else') { + if (received) return true; + received = true; + } else if (received) { + asts.push(ast); + } - }, + return false; + }, + this + ); + return this._render(asts); + }, + }); + }; +}); +define('velocityjs/0.4.10/src/compile/literal-debug', [], function(require, exports, module) { + module.exports = function(Velocity, utils) { /** - * parse #foreach + * literal解释模块 + * @require {method} getReferences */ - getBlockEach: function(block){ - - var ast = block[0]; - var _from = this.getLiteral(ast.from); - var _block = block.slice(1); - var _to = ast.to; - var local = { - foreach: { - count: 0 + utils.mixin(Velocity.prototype, { + /** + * 字面量求值,主要包括string, integer, array, map四种数据结构 + * @param literal {object} 定义于velocity.yy文件,type描述数据类型,value属性 + * 是literal值描述 + * @return {object|string|number|array}返回对应的js变量 + */ + getLiteral: function(literal) { + var type = literal.type; + var ret = ''; + + if (type == 'string') { + ret = this.getString(literal); + } else if (type == 'integer') { + ret = parseInt(literal.value, 10); + } else if (type == 'decimal') { + ret = parseFloat(literal.value, 10); + } else if (type == 'array') { + ret = this.getArray(literal); + } else if (type == 'map') { + ret = {}; + var map = literal.value; + + utils.forEach( + map, + function(exp, key) { + ret[key] = this.getLiteral(exp); + }, + this + ); + } else if (type == 'bool') { + if (literal.value === 'null') { + ret = null; + } else if (literal.value === 'false') { + ret = false; + } else if (literal.value === 'true') { + ret = true; + } + } else { + return this.getReferences(literal); } - }; - var ret = ''; - var guid = utils.guid(); - var contextId = 'foreach:' + guid; - - var type = ({}).toString.call(_from); - if (!_from || (type !== '[object Array]' && type !== '[object Object]')) return; - - var len = utils.isArray(_from)? _from.length: utils.keys(_from).length; - utils.forEach(_from, function(val, i){ - - if (this.setBreak) return; - //构造临时变量 - local[_to] = val; - //TODO: here, the foreach variable give to local, when _from is not an - //array, count and hasNext would be undefined, also i is not the - //index. - local['foreach']['count'] = i + 1; - local['foreach']['index'] = i; - local['foreach']['hasNext'] = i + 1 < len; - local['velocityCount'] = i + 1; - this.local[contextId] = local; - ret += this._render(_block, contextId); - - }, this); - - this.setBreak = false; - //删除临时变量 - this.local[contextId] = {}; - this.conditions.shift(); - this.condition = this.conditions[0] || ''; - - return ret; - - }, - - /** - * parse #if - */ - getBlockIf: function(block) { - - var str = ''; - var received = false; - var asts = []; - - utils.some(block, function(ast){ - - if (ast.condition) { + return ret; + }, - if (received) return true; - received = this.getExpression(ast.condition); + /** + * 对字符串求值,对已双引号字符串,需要做变量替换 + */ + getString: function(literal) { + var val = literal.value; + var ret = val; - } else if (ast.type === 'else') { - if (received) return true; - received = true; - } else if (received) { - asts.push(ast); + if (literal.isEval && (val.indexOf('#') !== -1 || val.indexOf('$') !== -1)) { + ret = this.evalStr(val); } - return false; - - }, this); - - return this._render(asts); - } - }); -}; - -}); -define("velocityjs/0.4.10/src/compile/literal-debug", [], function(require, exports, module){ -module.exports = function(Velocity, utils){ - /** - * literal解释模块 - * @require {method} getReferences - */ - utils.mixin(Velocity.prototype, { - /** - * 字面量求值,主要包括string, integer, array, map四种数据结构 - * @param literal {object} 定义于velocity.yy文件,type描述数据类型,value属性 - * 是literal值描述 - * @return {object|string|number|array}返回对应的js变量 - */ - getLiteral: function(literal){ - - var type = literal.type; - var ret = ''; - - if (type == 'string') { - - ret = this.getString(literal); - - } else if (type == 'integer') { - - ret = parseInt(literal.value, 10); - - } else if (type == 'decimal') { - - ret = parseFloat(literal.value, 10); + return ret; + }, - } else if (type == 'array') { + /** + * 对array字面量求值,比如[1, 2]=> [1,2],[1..5] => [1,2,3,4,5] + * @param literal {object} array字面量的描述对象,分为普通数组和range数组两种 + * ,和js基本一致 + * @return {array} 求值得到的数组 + */ + getArray: function(literal) { + var ret = []; - ret = this.getArray(literal); + if (literal.isRange) { + var begin = literal.value[0]; + if (begin.type === 'references') { + begin = this.getReferences(begin); + } - } else if(type == 'map') { + var end = literal.value[1]; + if (end.type === 'references') { + end = this.getReferences(end); + } - ret = {}; - var map = literal.value; + end = parseInt(end, 10); + begin = parseInt(begin, 10); - utils.forEach(map, function(exp, key){ - ret[key] = this.getLiteral(exp); - }, this); - } else if(type == 'bool') { + var i; - if (literal.value === "null") { - ret = null; - } else if (literal.value === 'false') { - ret = false; - } else if (literal.value === 'true') { - ret = true; + if (!isNaN(begin) && !isNaN(end)) { + if (begin < end) { + for (i = begin; i <= end; i++) ret.push(i); + } else { + for (i = begin; i >= end; i--) ret.push(i); + } + } + } else { + utils.forEach( + literal.value, + function(exp) { + ret.push(this.getLiteral(exp)); + }, + this + ); } - } else { - - return this.getReferences(literal); - - } - - return ret; - }, - - /** - * 对字符串求值,对已双引号字符串,需要做变量替换 - */ - getString: function(literal){ - var val = literal.value; - var ret = val; + return ret; + }, - if (literal.isEval && (val.indexOf('#') !== -1 || val.indexOf("$") !== -1)) { - ret = this.evalStr(val); + /** + * 对双引号字符串进行eval求值,替换其中的变量,只支持最基本的变量类型替换 + */ + evalStr: function(str) { + var asts = Velocity.Parser.parse(str); + return this._render(asts); + }, + }); + }; +}); +define('velocityjs/0.4.10/src/compile/references-debug', [], function(require, exports, module) { + module.exports = function(Velocity, utils) { + 'use strict'; + + function getSize(obj) { + if (utils.isArray(obj)) { + return obj.length; + } else if (utils.isObject(obj)) { + return utils.keys(obj).length; } - return ret; - }, + return undefined; + } /** - * 对array字面量求值,比如[1, 2]=> [1,2],[1..5] => [1,2,3,4,5] - * @param literal {object} array字面量的描述对象,分为普通数组和range数组两种 - * ,和js基本一致 - * @return {array} 求值得到的数组 + * unicode转码 */ - getArray: function(literal){ - - var ret = []; - - if (literal.isRange) { - - var begin = literal.value[0]; - if (begin.type === 'references') { - begin = this.getReferences(begin); - } - - var end = literal.value[1]; - if (end.type === 'references') { - end = this.getReferences(end); - } - - end = parseInt(end, 10); - begin = parseInt(begin, 10); - - var i; - - if (!isNaN(begin) && !isNaN(end)) { - - if (begin < end) { - for (i = begin; i <= end; i++) ret.push(i); + function convert(str) { + if (typeof str !== 'string') return str; + + var result = ''; + var escape = false; + var i, c, cstr; + + for (i = 0; i < str.length; i++) { + c = str.charAt(i); + if ((' ' <= c && c <= '~') || c == '\r' || c == '\n') { + if (c == '&') { + cstr = '&'; + escape = true; + } else if (c == '<') { + cstr = '<'; + escape = true; + } else if (c == '>') { + cstr = '>'; + escape = true; } else { - for (i = begin; i >= end; i--) ret.push(i); + cstr = c.toString(); } + } else { + cstr = '&#' + c.charCodeAt().toString() + ';'; } - } else { - utils.forEach(literal.value, function(exp){ - ret.push(this.getLiteral(exp)); - }, this); + result = result + cstr; } - return ret; - }, - - /** - * 对双引号字符串进行eval求值,替换其中的变量,只支持最基本的变量类型替换 - */ - evalStr: function(str){ - var asts = Velocity.Parser.parse(str); - return this._render(asts); + return escape ? result : str; } - }); -}; -}); -define("velocityjs/0.4.10/src/compile/references-debug", [], function(require, exports, module){ -module.exports = function(Velocity, utils){ - - 'use strict'; - - function getSize(obj){ + utils.mixin(Velocity.prototype, { + //增加某些函数,不需要执行html转义 + addIgnoreEscpape: function(key) { + if (!utils.isArray(key)) key = [key]; - if (utils.isArray(obj)) { - return obj.length; - } else if (utils.isObject(obj)) { - return utils.keys(obj).length; - } - - return undefined; - } + utils.forEach( + key, + function(key) { + this.config.unescape[key] = true; + }, + this + ); + }, - /** - * unicode转码 - */ - function convert(str){ - - if (typeof str !== 'string') return str; - - var result = "" - var escape = false - var i, c, cstr; - - for(i = 0 ; i < str.length ; i++) { - c = str.charAt(i); - if((' ' <= c && c <= '~') || (c == '\r') || (c == '\n')) { - if(c == '&') { - cstr = "&" - escape = true - } else if(c == '<') { - cstr = "<" - escape = true - } else if(c == '>') { - cstr = ">" - escape = true - } else { - cstr = c.toString() + /** + * 引用求值 + * @param {object} ast 结构来自velocity.yy + * @param {bool} isVal 取值还是获取字符串,两者的区别在于,求值返回结果,求 + * 字符串,如果没有返回变量自身,比如$foo + */ + getReferences: function(ast, isVal) { + if (ast.prue) { + var define = this.defines[ast.id]; + if (utils.isArray(define)) { + return this._render(define); + } + if (ast.id in this.config.unescape) ast.prue = false; } - } else { - cstr = "&#" + c.charCodeAt().toString() + ";" - } - - result = result + cstr - } + var escape = this.config.escape; - return escape ? result : str + var isSilent = this.silence || ast.leader === '$!'; + var isfn = ast.args !== undefined; + var context = this.context; + var ret = context[ast.id]; + var local = this.getLocal(ast); - } - - - utils.mixin(Velocity.prototype, { - - //增加某些函数,不需要执行html转义 - addIgnoreEscpape: function(key){ - - if (!utils.isArray(key)) key = [key] - - utils.forEach(key, function(key){ - this.config.unescape[key] = true - }, this) - - }, - - /** - * 引用求值 - * @param {object} ast 结构来自velocity.yy - * @param {bool} isVal 取值还是获取字符串,两者的区别在于,求值返回结果,求 - * 字符串,如果没有返回变量自身,比如$foo - */ - getReferences: function(ast, isVal) { + var text = Velocity.Helper.getRefText(ast); - if (ast.prue) { - var define = this.defines[ast.id]; - if (utils.isArray(define)) { - return this._render(define); + if (text in context) { + return ast.prue && escape ? convert(context[text]) : context[text]; } - if (ast.id in this.config.unescape) ast.prue = false; - } - var escape = this.config.escape; - - var isSilent = this.silence || ast.leader === "$!"; - var isfn = ast.args !== undefined; - var context = this.context; - var ret = context[ast.id]; - var local = this.getLocal(ast); - - var text = Velocity.Helper.getRefText(ast); - - if (text in context) { - return (ast.prue && escape) ? convert(context[text]) : context[text]; - } - - - if (ret !== undefined && isfn) { - ret = this.getPropMethod(ast, context, ast); - } - - if (local.isLocaled) ret = local['value']; - - if (ast.path && ret !== undefined) { - - utils.some(ast.path, function(property){ - //第三个参数,返回后面的参数ast - ret = this.getAttributes(property, ret, ast); - - }, this); - } - - if (isVal && ret === undefined) { - ret = isSilent? '' : Velocity.Helper.getRefText(ast); - } - - ret = (ast.prue && escape) ? convert(ret) : ret; - - return ret; - }, - - /** - * 获取局部变量,在macro和foreach循环中使用 - */ - getLocal: function(ast){ + if (ret !== undefined && isfn) { + ret = this.getPropMethod(ast, context, ast); + } - var id = ast.id; - var local = this.local; - var ret = false; + if (local.isLocaled) ret = local['value']; + + if (ast.path && ret !== undefined) { + utils.some( + ast.path, + function(property) { + //第三个参数,返回后面的参数ast + ret = this.getAttributes(property, ret, ast); + }, + this + ); + } - var isLocaled = utils.some(this.conditions, function(contextId){ - var _local = local[contextId]; - if (id in _local) { - ret = _local[id]; - return true; + if (isVal && ret === undefined) { + ret = isSilent ? '' : Velocity.Helper.getRefText(ast); } - return false; - }, this); + ret = ast.prue && escape ? convert(ret) : ret; + + return ret; + }, - return { - value: ret, - isLocaled: isLocaled - }; - }, - /** - * $foo.bar 属性求值,最后面两个参数在用户传递的函数中用到 - * @param {object} property 属性描述,一个对象,主要包括id,type等定义 - * @param {object} baseRef 当前执行链结果,比如$a.b.c,第一次baseRef是$a, - * 第二次是$a.b返回值 - * @private - */ - getAttributes: function(property, baseRef, ast){ /** - * type对应着velocity.yy中的attribute,三种类型: method, index, property + * 获取局部变量,在macro和foreach循环中使用 */ - var type = property.type; - var ret; - var id = property.id; - if (type === 'method'){ - ret = this.getPropMethod(property, baseRef, ast); - } else if (type === 'property') { - ret = baseRef[id]; - } else { - ret = this.getPropIndex(property, baseRef); - } - return ret; - }, - - /** - * $foo.bar[1] index求值 - * @private - */ - getPropIndex: function(property, baseRef){ - var ast = property.id; - var key; - if (ast.type === 'references'){ - key = this.getReferences(ast); - } else if(ast.type === 'integer'){ - key = ast.value; - } else { - key = ast.value; - } - - return baseRef[key]; - }, - - /** - * $foo.bar()求值 - */ - getPropMethod: function(property, baseRef, ast){ - - var id = property.id; - var ret = ''; - var _id = id.slice(3); + getLocal: function(ast) { + var id = ast.id; + var local = this.local; + var ret = false; + + var isLocaled = utils.some( + this.conditions, + function(contextId) { + var _local = local[contextId]; + if (id in _local) { + ret = _local[id]; + return true; + } - // getter 处理 - if (id.indexOf('get') === 0 && !(id in baseRef)) { + return false; + }, + this + ); - if (_id) { - ret = baseRef[_id]; + return { + value: ret, + isLocaled: isLocaled, + }; + }, + /** + * $foo.bar 属性求值,最后面两个参数在用户传递的函数中用到 + * @param {object} property 属性描述,一个对象,主要包括id,type等定义 + * @param {object} baseRef 当前执行链结果,比如$a.b.c,第一次baseRef是$a, + * 第二次是$a.b返回值 + * @private + */ + getAttributes: function(property, baseRef, ast) { + /** + * type对应着velocity.yy中的attribute,三种类型: method, index, property + */ + var type = property.type; + var ret; + var id = property.id; + if (type === 'method') { + ret = this.getPropMethod(property, baseRef, ast); + } else if (type === 'property') { + ret = baseRef[id]; } else { - //map 对应的get方法 - _id = this.getLiteral(property.args[0]); - ret = baseRef[_id]; + ret = this.getPropIndex(property, baseRef); } - return ret; + }, - // setter 处理 - } else if (id.indexOf('set') === 0 && !baseRef[id]) { - - baseRef[_id] = this.getLiteral(property.args[0]); - // $page.setName(123) - baseRef.toString = function() { return ''; }; - return baseRef; - - } else if (id.indexOf('is') === 0 && !(id in baseRef)) { - - _id = id.slice(2); - ret = baseRef[_id]; - return ret; - - } else if (id === 'keySet') { - - return utils.keys(baseRef); - - } else if (id === 'entrySet') { - - ret = []; - utils.forEach(baseRef, function(value, key){ - ret.push({key: key, value: value}); - }); - - return ret; - - } else if (id === 'size') { - - return getSize(baseRef); - - } else { - - ret = baseRef[id]; - var args = []; + /** + * $foo.bar[1] index求值 + * @private + */ + getPropIndex: function(property, baseRef) { + var ast = property.id; + var key; + if (ast.type === 'references') { + key = this.getReferences(ast); + } else if (ast.type === 'integer') { + key = ast.value; + } else { + key = ast.value; + } - utils.forEach(property.args, function(exp){ - args.push(this.getLiteral(exp)); - }, this); + return baseRef[key]; + }, - if (ret && ret.call) { + /** + * $foo.bar()求值 + */ + getPropMethod: function(property, baseRef, ast) { + var id = property.id; + var ret = ''; + var _id = id.slice(3); + + // getter 处理 + if (id.indexOf('get') === 0 && !(id in baseRef)) { + if (_id) { + ret = baseRef[_id]; + } else { + //map 对应的get方法 + _id = this.getLiteral(property.args[0]); + ret = baseRef[_id]; + } - var that = this; + return ret; - baseRef.eval = function() { - return that.eval.apply(that, arguments); + // setter 处理 + } else if (id.indexOf('set') === 0 && !baseRef[id]) { + baseRef[_id] = this.getLiteral(property.args[0]); + // $page.setName(123) + baseRef.toString = function() { + return ''; }; + return baseRef; + } else if (id.indexOf('is') === 0 && !(id in baseRef)) { + _id = id.slice(2); + ret = baseRef[_id]; + return ret; + } else if (id === 'keySet') { + return utils.keys(baseRef); + } else if (id === 'entrySet') { + ret = []; + utils.forEach(baseRef, function(value, key) { + ret.push({ key: key, value: value }); + }); - try { - ret = ret.apply(baseRef, args); - } catch(e) { - var pos = ast.pos; - var text = Velocity.Helper.getRefText(ast); - var err = ' on ' + text + ' at L/N ' + pos.first_line + ':' + pos.first_column; - e.name = ''; - e.message += err; - throw new Error(e); - } - + return ret; + } else if (id === 'size') { + return getSize(baseRef); } else { - ret = undefined; - } - } - - return ret; - } - }) + ret = baseRef[id]; + var args = []; + + utils.forEach( + property.args, + function(exp) { + args.push(this.getLiteral(exp)); + }, + this + ); + + if (ret && ret.call) { + var that = this; + + baseRef.eval = function() { + return that.eval.apply(that, arguments); + }; -} + try { + ret = ret.apply(baseRef, args); + } catch (e) { + var pos = ast.pos; + var text = Velocity.Helper.getRefText(ast); + var err = ' on ' + text + ' at L/N ' + pos.first_line + ':' + pos.first_column; + e.name = ''; + e.message += err; + throw new Error(e); + } + } else { + ret = undefined; + } + } + return ret; + }, + }); + }; }); -define("velocityjs/0.4.10/src/compile/set-debug", [], function(require, exports, module){ -module.exports = function(Velocity, utils){ - /** - * 变量设置 - */ - utils.mixin(Velocity.prototype, { +define('velocityjs/0.4.10/src/compile/set-debug', [], function(require, exports, module) { + module.exports = function(Velocity, utils) { /** - * 获取执行环境,对于macro中定义的变量,为局部变量,不贮存在全局中,执行后销毁 + * 变量设置 */ - getContext: function(){ - var condition = this.condition; - var local = this.local; - if (condition) { - return local[condition]; - } else { - return this.context; - } - }, - /** - * parse #set - */ - setValue: function(ast){ - var ref = ast.equal[0]; - var context = this.getContext(); - - //see https://github.com/shepherdwind/velocity.js/issues/25 - if (this.condition && this.condition.indexOf('macro:') === 0) { - context = this.context; - } else if (this.context[ref.id] != null) { - context = this.context; - } - - var valAst = ast.equal[1]; - var val; - - if (valAst.type === 'math') { - val = this.getExpression(valAst); - } else { - val = this.getLiteral(ast.equal[1]); - } - - if (!ref.path) { - - context[ref.id] = val; - - } else { - - var baseRef = context[ref.id]; - if (typeof baseRef != 'object') { - baseRef = {}; + utils.mixin(Velocity.prototype, { + /** + * 获取执行环境,对于macro中定义的变量,为局部变量,不贮存在全局中,执行后销毁 + */ + getContext: function() { + var condition = this.condition; + var local = this.local; + if (condition) { + return local[condition]; + } else { + return this.context; + } + }, + /** + * parse #set + */ + setValue: function(ast) { + var ref = ast.equal[0]; + var context = this.getContext(); + + //see https://github.com/shepherdwind/velocity.js/issues/25 + if (this.condition && this.condition.indexOf('macro:') === 0) { + context = this.context; + } else if (this.context[ref.id] != null) { + context = this.context; } - context[ref.id] = baseRef; - var len = ref.path ? ref.path.length: 0; - - //console.log(val); - utils.forEach(ref.path, function(exp, i){ - - var isEnd = len === i + 1; - var key = exp.id; - if (exp.type === 'index') key = key.value; - baseRef[key] = isEnd? val: {}; - baseRef = baseRef[key]; + var valAst = ast.equal[1]; + var val; - }); + if (valAst.type === 'math') { + val = this.getExpression(valAst); + } else { + val = this.getLiteral(ast.equal[1]); + } - } - } - }); -}; + if (!ref.path) { + context[ref.id] = val; + } else { + var baseRef = context[ref.id]; + if (typeof baseRef != 'object') { + baseRef = {}; + } + context[ref.id] = baseRef; + var len = ref.path ? ref.path.length : 0; + + //console.log(val); + utils.forEach(ref.path, function(exp, i) { + var isEnd = len === i + 1; + var key = exp.id; + if (exp.type === 'index') key = key.value; + baseRef[key] = isEnd ? val : {}; + baseRef = baseRef[key]; + }); + } + }, + }); + }; }); -define("velocityjs/0.4.10/src/compile/expression-debug", [], function(require, exports, module){ -module.exports = function(Velocity, utils){ - /** - * expression运算 - */ - utils.mixin(Velocity.prototype, { +define('velocityjs/0.4.10/src/compile/expression-debug', [], function(require, exports, module) { + module.exports = function(Velocity, utils) { /** - * 表达式求值,表达式主要是数学表达式,逻辑运算和比较运算,到最底层数据结构, - * 基本数据类型,使用 getLiteral求值,getLiteral遇到是引用的时候,使用 - * getReferences求值 + * expression运算 */ - getExpression: function(ast){ - - var exp = ast.expression; - var ret; - if (ast.type === 'math') { - - switch(ast.operator) { - case '+': - ret = this.getExpression(exp[0]) + this.getExpression(exp[1]); - break; - - case '-': - ret = this.getExpression(exp[0]) - this.getExpression(exp[1]); - break; - - case '/': - ret = this.getExpression(exp[0]) / this.getExpression(exp[1]); - break; - - case '%': - ret = this.getExpression(exp[0]) % this.getExpression(exp[1]); - break; - - case '*': - ret = this.getExpression(exp[0]) * this.getExpression(exp[1]); - break; - - case '||': - ret = this.getExpression(exp[0]) || this.getExpression(exp[1]); - break; - - case '&&': - ret = this.getExpression(exp[0]) && this.getExpression(exp[1]); - break; - - case '>': - ret = this.getExpression(exp[0]) > this.getExpression(exp[1]); - break; - - case '<': - ret = this.getExpression(exp[0]) < this.getExpression(exp[1]); - break; - - case '==': - ret = this.getExpression(exp[0]) == this.getExpression(exp[1]); - break; - - case '>=': - ret = this.getExpression(exp[0]) >= this.getExpression(exp[1]); - break; - - case '<=': - ret = this.getExpression(exp[0]) <= this.getExpression(exp[1]); - break; - - case '!=': - ret = this.getExpression(exp[0]) != this.getExpression(exp[1]); - break; - - case 'minus': - ret = - this.getExpression(exp[0]); - break; - - case 'not': - ret = !this.getExpression(exp[0]); - break; - - case 'parenthesis': - ret = this.getExpression(exp[0]); - break; + utils.mixin(Velocity.prototype, { + /** + * 表达式求值,表达式主要是数学表达式,逻辑运算和比较运算,到最底层数据结构, + * 基本数据类型,使用 getLiteral求值,getLiteral遇到是引用的时候,使用 + * getReferences求值 + */ + getExpression: function(ast) { + var exp = ast.expression; + var ret; + if (ast.type === 'math') { + switch (ast.operator) { + case '+': + ret = this.getExpression(exp[0]) + this.getExpression(exp[1]); + break; + + case '-': + ret = this.getExpression(exp[0]) - this.getExpression(exp[1]); + break; + + case '/': + ret = this.getExpression(exp[0]) / this.getExpression(exp[1]); + break; + + case '%': + ret = this.getExpression(exp[0]) % this.getExpression(exp[1]); + break; + + case '*': + ret = this.getExpression(exp[0]) * this.getExpression(exp[1]); + break; + + case '||': + ret = this.getExpression(exp[0]) || this.getExpression(exp[1]); + break; + + case '&&': + ret = this.getExpression(exp[0]) && this.getExpression(exp[1]); + break; + + case '>': + ret = this.getExpression(exp[0]) > this.getExpression(exp[1]); + break; + + case '<': + ret = this.getExpression(exp[0]) < this.getExpression(exp[1]); + break; + + case '==': + ret = this.getExpression(exp[0]) == this.getExpression(exp[1]); + break; + + case '>=': + ret = this.getExpression(exp[0]) >= this.getExpression(exp[1]); + break; + + case '<=': + ret = this.getExpression(exp[0]) <= this.getExpression(exp[1]); + break; + + case '!=': + ret = this.getExpression(exp[0]) != this.getExpression(exp[1]); + break; + + case 'minus': + ret = -this.getExpression(exp[0]); + break; + + case 'not': + ret = !this.getExpression(exp[0]); + break; + + case 'parenthesis': + ret = this.getExpression(exp[0]); + break; + + default: + return; + // code + } - default: - return; - // code + return ret; + } else { + return this.getLiteral(ast); } - - return ret; - } else { - return this.getLiteral(ast); - } - } - }); -}; - + }, + }); + }; }); -define("velocityjs/0.4.10/src/compile/compile-debug", [], function(require, exports, module){ -module.exports = function(Velocity, utils){ - - /** - * compile - */ - utils.mixin(Velocity.prototype, { - init: function(){ - this.context = {}; - this.macros = {}; - this.defines = {}; - this.conditions = []; - this.local = {}; - this.silence = false; - this.unescape = {}; - }, - +define('velocityjs/0.4.10/src/compile/compile-debug', [], function(require, exports, module) { + module.exports = function(Velocity, utils) { /** - * @param context {object} 上下文环境,数据对象 - * @param macro {object} self defined #macro - * @param silent {bool} 如果是true,$foo变量将原样输出 - * @return str + * compile */ - render: function(context, macros, silence){ - - this.silence = !!silence; - this.context = context || {}; - this.jsmacros = macros || {}; - var t1 = utils.now(); - var str = this._render(); - var t2 = utils.now(); - var cost = t2 - t1; + utils.mixin(Velocity.prototype, { + init: function() { + this.context = {}; + this.macros = {}; + this.defines = {}; + this.conditions = []; + this.local = {}; + this.silence = false; + this.unescape = {}; + }, - this.cost = cost; + /** + * @param context {object} 上下文环境,数据对象 + * @param macro {object} self defined #macro + * @param silent {bool} 如果是true,$foo变量将原样输出 + * @return str + */ + render: function(context, macros, silence) { + this.silence = !!silence; + this.context = context || {}; + this.jsmacros = macros || {}; + var t1 = utils.now(); + var str = this._render(); + var t2 = utils.now(); + var cost = t2 - t1; - return str; - }, + this.cost = cost; - /** - * 解析入口函数 - * @param ast {array} 模板结构数组 - * @param contextId {number} 执行环境id,对于macro有局部作用域,变量的设置和 - * 取值,都放在一个this.local下,通过contextId查找 - * @return {string}解析后的字符串 - */ - _render: function(asts, contextId){ + return str; + }, - var str = ''; - asts = asts || this.asts; + /** + * 解析入口函数 + * @param ast {array} 模板结构数组 + * @param contextId {number} 执行环境id,对于macro有局部作用域,变量的设置和 + * 取值,都放在一个this.local下,通过contextId查找 + * @return {string}解析后的字符串 + */ + _render: function(asts, contextId) { + var str = ''; + asts = asts || this.asts; - if (contextId) { + if (contextId) { + if (contextId !== this.condition && utils.indexOf(contextId, this.conditions) === -1) { + this.conditions.unshift(contextId); + } - if (contextId !== this.condition && - utils.indexOf(contextId, this.conditions) === -1) { - this.conditions.unshift(contextId); + this.condition = contextId; + } else { + this.condition = null; } - this.condition = contextId; + utils.forEach( + asts, + function(ast) { + switch (ast.type) { + case 'references': + str += this.getReferences(ast, true); + break; - } else { - this.condition = null; - } + case 'set': + this.setValue(ast); + break; - utils.forEach(asts, function(ast){ + case 'break': + this.setBreak = true; + break; - switch(ast.type) { - case 'references': - str += this.getReferences(ast, true); - break; + case 'macro_call': + str += this.getMacro(ast); + break; - case 'set': - this.setValue(ast); - break; + case 'comment': + break; - case 'break': - this.setBreak = true; - break; - - case 'macro_call': - str += this.getMacro(ast); - break; - - case 'comment': - break; - - default: - str += typeof ast == 'string' ? ast : this.getBlock(ast); - break; - } - }, this); - - return str; - } - }); -}; + default: + str += typeof ast == 'string' ? ast : this.getBlock(ast); + break; + } + }, + this + ); + return str; + }, + }); + }; }); diff --git a/packages/amplify-velocity-template/tests/runner/index.js b/packages/amplify-velocity-template/tests/runner/index.js index bbae247e2d..39f32f7d36 100644 --- a/packages/amplify-velocity-template/tests/runner/index.js +++ b/packages/amplify-velocity-template/tests/runner/index.js @@ -1,2 +1,2462 @@ -define("velocityjs/0.4.10/index",[],function(e,t,s){"use strict";s.exports=e("velocityjs/0.4.10/src/velocity")}),define("velocityjs/0.4.10/src/velocity",[],function(e,t,s){var i=e("velocityjs/0.4.10/src/parse/index"),r=e("velocityjs/0.4.10/src/utils"),n=e("velocityjs/0.4.10/src/compile/index"),a=e("velocityjs/0.4.10/src/helper/index");n.Parser=i,i._parse=i.parse,i.parse=function(e){var t=i._parse(e);return r.forEach(t,function(e,s){var i=/^[ \t]*\n/;if(e.type&&"references"!==e.type){var r=t[s+1];"string"==typeof r&&i.test(r)&&(t[s+1]=r.replace(i,""))}}),r.makeLevel(t)};var c={Parser:i,Compile:n,Helper:a};c.render=function(e,t,s){var r=i.parse(e),a=new n(r);return a.render(t,s)},s.exports=c}),define("velocityjs/0.4.10/src/parse/index",[],function(e,t,s){var i=function(){function e(){this.yy={}}var t=function(e,t,s,i){for(s=s||{},i=e.length;i--;s[e[i]]=t);return s},s=[1,8],i=[1,18],r=[1,9],n=[1,22],a=[1,21],c=[4,10,19,33,34,79],o=[1,26],h=[1,29],l=[1,30],u=[4,10,19,22,33,34,44,45,46,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81,91],p=[1,46],y=[1,51],f=[1,52],g=[1,66],d=[1,67],v=[1,78],m=[1,89],b=[1,81],k=[1,79],x=[1,84],E=[1,88],_=[1,85],S=[1,86],$=[4,10,19,22,33,34,44,45,46,49,50,51,52,53,54,55,56,57,58,59,60,61,71,72,77,79,80,81,91],A=[1,115],I=[1,111],O=[1,112],j=[1,123],L=[22,45,81],N=[2,89],R=[22,44,45,72,81],P=[22,44,45,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81],T=[22,44,45,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81,93],w=[2,102],C=[22,44,45,49,50,51,52,53,54,55,56,57,58,59,60,61,72,79,81,91],M=[2,105],B=[1,132],D=[1,138],F=[22,44,45],H=[1,143],z=[1,144],G=[1,145],V=[1,146],Z=[1,147],K=[1,148],W=[1,149],q=[1,150],U=[1,151],Q=[1,152],Y=[1,153],J=[1,154],X=[1,155],et=[22,49,50,51,52,53,54,55,56,57,58,59,60,61],tt=[45,81],st=[2,106],it=[22,33],rt=[1,202],nt=[1,201],at=[45,72],ct=[22,49,50],ot=[22,49,50,51,52,56,57,58,59,60,61],ht=[22,49,50,56,57,58,59,60,61],lt={trace:function(){},yy:{},symbols_:{error:2,root:3,EOF:4,statements:5,statement:6,references:7,directives:8,content:9,COMMENT:10,set:11,"if":12,elseif:13,"else":14,end:15,foreach:16,"break":17,define:18,HASH:19,NOESCAPE:20,PARENTHESIS:21,CLOSE_PARENTHESIS:22,macro:23,macro_call:24,SET:25,equal:26,IF:27,expression:28,ELSEIF:29,ELSE:30,END:31,FOREACH:32,DOLLAR:33,ID:34,IN:35,array:36,BREAK:37,DEFINE:38,MACRO:39,macro_args:40,macro_call_args_all:41,macro_call_args:42,literals:43,SPACE:44,COMMA:45,EQUAL:46,map:47,math:48,"||":49,"&&":50,"+":51,"-":52,"*":53,"/":54,"%":55,">":56,"<":57,"==":58,">=":59,"<=":60,"!=":61,parenthesis:62,"!":63,literal:64,brace_begin:65,attributes:66,brace_end:67,methodbd:68,VAR_BEGIN:69,MAP_BEGIN:70,VAR_END:71,MAP_END:72,attribute:73,method:74,index:75,property:76,DOT:77,params:78,CONTENT:79,BRACKET:80,CLOSE_BRACKET:81,string:82,number:83,BOOL:84,integer:85,INTEGER:86,DECIMAL_POINT:87,STRING:88,EVAL_STRING:89,range:90,RANGE:91,map_item:92,MAP_SPLIT:93,$accept:0,$end:1},terminals_:{2:"error",4:"EOF",10:"COMMENT",19:"HASH",20:"NOESCAPE",21:"PARENTHESIS",22:"CLOSE_PARENTHESIS",25:"SET",27:"IF",29:"ELSEIF",30:"ELSE",31:"END",32:"FOREACH",33:"DOLLAR",34:"ID",35:"IN",37:"BREAK",38:"DEFINE",39:"MACRO",44:"SPACE",45:"COMMA",46:"EQUAL",49:"||",50:"&&",51:"+",52:"-",53:"*",54:"/",55:"%",56:">",57:"<",58:"==",59:">=",60:"<=",61:"!=",63:"!",69:"VAR_BEGIN",70:"MAP_BEGIN",71:"VAR_END",72:"MAP_END",77:"DOT",79:"CONTENT",80:"BRACKET",81:"CLOSE_BRACKET",84:"BOOL",86:"INTEGER",87:"DECIMAL_POINT",88:"STRING",89:"EVAL_STRING",91:"RANGE",93:"MAP_SPLIT"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[6,1],[6,1],[6,1],[6,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,4],[8,1],[8,1],[11,5],[12,5],[13,5],[14,2],[15,2],[16,8],[16,8],[17,2],[18,6],[23,6],[23,5],[40,1],[40,2],[24,5],[24,4],[42,1],[42,1],[42,3],[42,3],[42,3],[42,3],[41,1],[41,2],[41,3],[41,2],[26,3],[28,1],[28,1],[28,1],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,3],[48,1],[48,2],[48,2],[48,1],[48,1],[62,3],[7,5],[7,3],[7,5],[7,3],[7,2],[7,4],[7,2],[7,4],[65,1],[65,1],[67,1],[67,1],[66,1],[66,2],[73,1],[73,1],[73,1],[74,2],[68,4],[68,3],[78,1],[78,1],[78,3],[78,3],[76,2],[76,2],[75,3],[75,3],[75,3],[75,2],[75,2],[64,1],[64,1],[64,1],[83,1],[83,3],[83,4],[85,1],[85,2],[82,1],[82,1],[43,1],[43,1],[43,1],[36,3],[36,1],[36,2],[90,5],[90,5],[90,5],[90,5],[47,3],[47,2],[92,3],[92,3],[92,2],[92,5],[92,5],[9,1],[9,1],[9,2],[9,3],[9,3],[9,2]],performAction:function(e,t,s,i,r,n){var a=n.length-1;switch(r){case 1:return[];case 2:return n[a-1];case 3:case 31:case 35:case 36:case 80:case 88:case 89:this.$=[n[a]];break;case 4:case 32:case 81:this.$=[].concat(n[a-1],n[a]);break;case 5:n[a].prue=!0,n[a].pos=this._$,this.$=n[a];break;case 6:n[a].pos=this._$,this.$=n[a];break;case 7:case 9:case 10:case 11:case 12:case 13:case 14:case 15:case 16:case 18:case 19:case 41:case 42:case 46:case 47:case 48:case 62:case 65:case 66:case 76:case 77:case 78:case 79:case 85:case 92:case 99:case 100:case 105:case 111:case 113:case 126:case 127:this.$=n[a];break;case 8:this.$={type:"comment",value:n[a]};break;case 17:this.$={type:"noescape"};break;case 20:this.$={type:"set",equal:n[a-1]};break;case 21:this.$={type:"if",condition:n[a-1]};break;case 22:this.$={type:"elseif",condition:n[a-1]};break;case 23:this.$={type:"else"};break;case 24:this.$={type:"end"};break;case 25:case 26:this.$={type:"foreach",to:n[a-3],from:n[a-1]};break;case 27:this.$={type:n[a]};break;case 28:this.$={type:"define",id:n[a-1]};break;case 29:this.$={type:"macro",id:n[a-2],args:n[a-1]};break;case 30:this.$={type:"macro",id:n[a-1]};break;case 33:this.$={type:"macro_call",id:n[a-3].replace(/^\s+|\s+$/g,""),args:n[a-1]};break;case 34:this.$={type:"macro_call",id:n[a-2].replace(/^\s+|\s+$/g,"")};break;case 37:case 38:case 39:case 40:case 90:case 91:this.$=[].concat(n[a-2],n[a]);break;case 43:case 44:case 94:case 95:this.$=n[a-1];break;case 45:this.$=[n[a-2],n[a]];break;case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 58:case 59:case 60:case 61:this.$={type:"math",expression:[n[a-2],n[a]],operator:n[a-1]};break;case 63:this.$={type:"math",expression:[n[a]],operator:"minus"};break;case 64:this.$={type:"math",expression:[n[a]],operator:"not"};break;case 67:this.$={type:"math",expression:[n[a-1]],operator:"parenthesis"};break;case 68:this.$={type:"references",id:n[a-2],path:n[a-1],isWraped:!0,leader:n[a-4]};break;case 69:this.$={type:"references",id:n[a-1],path:n[a],leader:n[a-2]};break;case 70:this.$={type:"references",id:n[a-2].id,path:n[a-1],isWraped:!0,leader:n[a-4],args:n[a-2].args};break;case 71:this.$={type:"references",id:n[a-1].id,path:n[a],leader:n[a-2],args:n[a-1].args};break;case 72:this.$={type:"references",id:n[a],leader:n[a-1]};break;case 73:this.$={type:"references",id:n[a-1],isWraped:!0,leader:n[a-3]};break;case 74:this.$={type:"references",id:n[a].id,leader:n[a-1],args:n[a].args};break;case 75:this.$={type:"references",id:n[a-1].id,isWraped:!0,args:n[a-1].args,leader:n[a-3]};break;case 82:this.$={type:"method",id:n[a].id,args:n[a].args};break;case 83:this.$={type:"index",id:n[a]};break;case 84:this.$={type:"property",id:n[a]},"content"===n[a].type&&(this.$=n[a]);break;case 86:this.$={id:n[a-3],args:n[a-1]};break;case 87:this.$={id:n[a-2],args:!1};break;case 93:this.$={type:"content",value:n[a-1]+n[a]};break;case 96:this.$={type:"content",value:n[a-2]+n[a-1].value+n[a]};break;case 97:case 98:this.$={type:"content",value:n[a-1]+n[a]};break;case 101:this.$={type:"bool",value:n[a]};break;case 102:this.$={type:"integer",value:n[a]};break;case 103:this.$={type:"decimal",value:+(n[a-2]+"."+n[a])};break;case 104:this.$={type:"decimal",value:-(n[a-2]+"."+n[a])};break;case 106:this.$=-parseInt(n[a],10);break;case 107:this.$={type:"string",value:n[a]};break;case 108:this.$={type:"string",value:n[a],isEval:!0};break;case 109:case 110:this.$=n[a];break;case 112:this.$={type:"array",value:n[a-1]};break;case 114:this.$={type:"array",value:[]};break;case 115:case 116:case 117:case 118:this.$={type:"array",isRange:!0,value:[n[a-3],n[a-1]]};break;case 119:this.$={type:"map",value:n[a-1]};break;case 120:this.$={type:"map"};break;case 121:case 122:this.$={},this.$[n[a-2].value]=n[a];break;case 123:this.$={},this.$[n[a-1].value]=n[$01];break;case 124:case 125:this.$=n[a-4],this.$[n[a-2].value]=n[a];break;case 128:case 131:this.$=n[a-1]+n[a];break;case 129:this.$=n[a-2]+n[a-1]+n[a];break;case 130:this.$=n[a-2]+n[a-1]}},table:[{3:1,4:[1,2],5:3,6:4,7:5,8:6,9:7,10:s,11:10,12:11,13:12,14:13,15:14,16:15,17:16,18:17,19:i,23:19,24:20,33:r,34:n,79:a},{1:[3]},{1:[2,1]},{4:[1,23],6:24,7:5,8:6,9:7,10:s,11:10,12:11,13:12,14:13,15:14,16:15,17:16,18:17,19:i,23:19,24:20,33:r,34:n,79:a},t(c,[2,3]),t(c,[2,5]),t(c,[2,6]),t(c,[2,7]),t(c,[2,8]),{34:o,65:25,68:27,69:h,70:l,79:[1,28]},t(c,[2,9]),t(c,[2,10]),t(c,[2,11]),t(c,[2,12]),t(c,[2,13]),t(c,[2,14]),t(c,[2,15]),t(c,[2,16]),{20:[1,31],25:[1,34],27:[1,35],29:[1,36],30:[1,37],31:[1,38],32:[1,39],34:[1,33],37:[1,40],38:[1,41],39:[1,42],79:[1,32]},t(c,[2,18]),t(c,[2,19]),t(c,[2,126]),t(c,[2,127]),{1:[2,2]},t(c,[2,4]),{34:[1,43],68:44},t(u,[2,72],{66:45,73:47,74:48,75:49,76:50,21:p,77:y,80:f}),t(u,[2,74],{73:47,74:48,75:49,76:50,66:53,77:y,80:f}),t(c,[2,131]),{34:[2,76]},{34:[2,77]},{21:[1,54]},t(c,[2,128]),{4:[1,56],21:[1,57],79:[1,55]},{21:[1,58]},{21:[1,59]},{21:[1,60]},t(c,[2,23]),t(c,[2,24]),{21:[1,61]},t(c,[2,27]),{21:[1,62]},{21:[1,63]},{21:p,66:64,67:65,71:g,72:d,73:47,74:48,75:49,76:50,77:y,80:f},{66:68,67:69,71:g,72:d,73:47,74:48,75:49,76:50,77:y,80:f},t(u,[2,69],{74:48,75:49,76:50,73:70,77:y,80:f}),{7:74,22:[1,72],33:v,36:75,43:73,47:76,52:m,64:77,70:b,78:71,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},t($,[2,80]),t($,[2,82]),t($,[2,83]),t($,[2,84]),{34:[1,91],68:90,79:[1,92]},{7:94,33:v,52:m,64:93,79:[1,95],81:[1,96],82:82,83:83,84:x,85:87,86:E,88:_,89:S},t(u,[2,71],{74:48,75:49,76:50,73:70,77:y,80:f}),{22:[1,97]},t(c,[2,129]),t(c,[2,130]),{7:103,22:[1,99],33:v,36:75,41:98,42:100,43:102,44:[1,101],47:76,52:m,64:77,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},{7:105,26:104,33:v},{7:113,21:A,28:106,33:v,36:107,47:108,48:109,52:I,62:110,63:O,64:114,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},{7:113,21:A,28:116,33:v,36:107,47:108,48:109,52:I,62:110,63:O,64:114,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},{33:[1,117]},{33:[1,118]},{34:[1,119]},{67:120,71:g,72:d,73:70,74:48,75:49,76:50,77:y,80:f},t(u,[2,73]),t(u,[2,78]),t(u,[2,79]),{67:121,71:g,72:d,73:70,74:48,75:49,76:50,77:y,80:f},t(u,[2,75]),t($,[2,81]),{22:[1,122],45:j},t($,[2,87]),t(L,[2,88]),t([22,45],N),t(R,[2,109]),t(R,[2,110]),t(R,[2,111]),{34:o,65:25,68:27,69:h,70:l},{7:127,33:v,36:75,43:73,47:76,52:m,64:77,70:b,78:124,80:k,81:[1,125],82:82,83:83,84:x,85:126,86:E,88:_,89:S,90:80},t(R,[2,113]),{72:[1,129],82:130,88:_,89:S,92:128},t(P,[2,99]),t(P,[2,100]),t(P,[2,101]),t(T,[2,107]),t(T,[2,108]),t(P,w),t(C,M,{87:[1,131]}),{86:B},t($,[2,85]),t($,[2,92],{21:p}),t($,[2,93]),{79:[1,134],81:[1,133]},{81:[1,135]},t($,[2,97]),t($,[2,98]),t(c,[2,17]),{22:[1,136]},t(c,[2,34]),{22:[2,41],44:[1,137],45:D},{7:103,33:v,36:75,42:139,43:102,47:76,52:m,64:77,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},t(F,[2,35]),t(F,[2,36]),{22:[1,140]},{46:[1,141]},{22:[1,142]},{22:[2,46]},{22:[2,47]},{22:[2,48],49:H,50:z,51:G,52:V,53:Z,54:K,55:W,56:q,57:U,58:Q,59:Y,60:J,61:X},t(et,[2,62]),{21:A,62:156,86:B},{7:113,21:A,33:v,48:157,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},t(et,[2,65]),t(et,[2,66]),{7:113,21:A,33:v,48:158,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{22:[1,159]},{34:[1,160]},{34:[1,161]},{7:164,22:[1,163],33:v,40:162},t(u,[2,68]),t(u,[2,70]),t($,[2,86]),{7:166,33:v,36:75,43:165,47:76,52:m,64:77,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},{45:j,81:[1,167]},t(R,[2,114]),t(tt,w,{91:[1,168]}),t(tt,N,{91:[1,169]}),{45:[1,171],72:[1,170]},t(R,[2,120]),{93:[1,172]},{86:[1,173]},t(C,st,{87:[1,174]}),t($,[2,94]),t($,[2,96]),t($,[2,95]),t(c,[2,33]),{7:176,22:[2,44],33:v,36:75,43:175,47:76,52:m,64:77,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},{7:178,33:v,36:75,43:177,47:76,52:m,64:77,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},{22:[2,42],44:[1,179],45:D},t(c,[2,20]),{7:113,21:A,28:180,33:v,36:107,47:108,48:109,52:I,62:110,63:O,64:114,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},t(c,[2,21]),{7:113,21:A,33:v,48:181,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:182,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:183,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:184,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:185,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:186,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:187,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:188,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:189,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:190,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:191,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:192,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},{7:113,21:A,33:v,48:193,52:I,62:110,63:O,64:114,82:82,83:83,84:x,85:87,86:E,88:_,89:S},t(et,[2,63]),t(et,[2,64]),{22:[1,194],49:H,50:z,51:G,52:V,53:Z,54:K,55:W,56:q,57:U,58:Q,59:Y,60:J,61:X},t(c,[2,22]),{35:[1,195]},{22:[1,196]},{7:198,22:[1,197],33:v},t(c,[2,30]),t(it,[2,31]),t(L,[2,90]),t(L,[2,91]),t(R,[2,112]),{7:200,33:v,52:rt,85:199,86:nt},{7:204,33:v,52:rt,85:203,86:nt},t(R,[2,119]),{82:205,88:_,89:S},t(at,[2,123],{36:75,47:76,64:77,90:80,82:82,83:83,85:87,43:206,7:207,33:v,52:m,70:b,80:k,84:x,86:E,88:_,89:S}),t(P,[2,103]),{86:[1,208]},t(F,[2,37]),t(F,[2,40]),t(F,[2,38]),t(F,[2,39]),{7:176,22:[2,43],33:v,36:75,43:175,47:76,52:m,64:77,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},{22:[2,45]},t(ct,[2,49],{51:G,52:V,53:Z,54:K,55:W,56:q,57:U,58:Q,59:Y,60:J,61:X}),t(ct,[2,50],{51:G,52:V,53:Z,54:K,55:W,56:q,57:U,58:Q,59:Y,60:J,61:X}),t(ot,[2,51],{53:Z,54:K,55:W}),t(ot,[2,52],{53:Z,54:K,55:W}),t(et,[2,53]),t(et,[2,54]),t(et,[2,55]),t(ht,[2,56],{51:G,52:V,53:Z,54:K,55:W}),t(ht,[2,57],{51:G,52:V,53:Z,54:K,55:W}),t(ht,[2,58],{51:G,52:V,53:Z,54:K,55:W}),t(ht,[2,59],{51:G,52:V,53:Z,54:K,55:W}),t(ht,[2,60],{51:G,52:V,53:Z,54:K,55:W}),t(ht,[2,61],{51:G,52:V,53:Z,54:K,55:W}),t(et,[2,67]),{7:209,33:v,36:210,80:k,90:80},t(c,[2,28]),t(c,[2,29]),t(it,[2,32]),{81:[1,211]},{81:[1,212]},{81:M},{86:[1,213]},{81:[1,214]},{81:[1,215]},{93:[1,216]},t(at,[2,121]),t(at,[2,122]),t(P,[2,104]),{22:[1,217]},{22:[1,218]},t(R,[2,115]),t(R,[2,117]),{81:st},t(R,[2,116]),t(R,[2,118]),{7:219,33:v,36:75,43:220,47:76,52:m,64:77,70:b,80:k,82:82,83:83,84:x,85:87,86:E,88:_,89:S,90:80},t(c,[2,25]),t(c,[2,26]),t(at,[2,124]),t(at,[2,125])],defaultActions:{2:[2,1],23:[2,2],29:[2,76],30:[2,77],107:[2,46],108:[2,47],180:[2,45],201:[2,105],213:[2,106]},parseError:function(e,t){if(!t.recoverable)throw new Error(e);this.trace(e)},parse:function(e){function t(){var e;return e=f.lex()||p,"number"!=typeof e&&(e=s.symbols_[e]||e),e}var s=this,i=[0],r=[null],n=[],a=this.table,c="",o=0,h=0,l=0,u=2,p=1,y=n.slice.call(arguments,1),f=Object.create(this.lexer),g={yy:{}};for(var d in this.yy)Object.prototype.hasOwnProperty.call(this.yy,d)&&(g.yy[d]=this.yy[d]);f.setInput(e,g.yy),g.yy.lexer=f,g.yy.parser=this,"undefined"==typeof f.yylloc&&(f.yylloc={});var v=f.yylloc;n.push(v);var m=f.options&&f.options.ranges;this.parseError="function"==typeof g.yy.parseError?g.yy.parseError:Object.getPrototypeOf(this).parseError;for(var b,k,x,E,_,S,$,A,I,O={};;){if(x=i[i.length-1],this.defaultActions[x]?E=this.defaultActions[x]:((null===b||"undefined"==typeof b)&&(b=t()),E=a[x]&&a[x][b]),"undefined"==typeof E||!E.length||!E[0]){var j="";I=[];for(S in a[x])this.terminals_[S]&&S>u&&I.push("'"+this.terminals_[S]+"'");j=f.showPosition?"Parse error on line "+(o+1)+":\n"+f.showPosition()+"\nExpecting "+I.join(", ")+", got '"+(this.terminals_[b]||b)+"'":"Parse error on line "+(o+1)+": Unexpected "+(b==p?"end of input":"'"+(this.terminals_[b]||b)+"'"),this.parseError(j,{text:f.match,token:this.terminals_[b]||b,line:f.yylineno,loc:v,expected:I})}if(E[0]instanceof Array&&E.length>1)throw new Error("Parse Error: multiple actions possible at state: "+x+", token: "+b);switch(E[0]){case 1:i.push(b),r.push(f.yytext),n.push(f.yylloc),i.push(E[1]),b=null,k?(b=k,k=null):(h=f.yyleng,c=f.yytext,o=f.yylineno,v=f.yylloc,l>0&&l--);break;case 2:if($=this.productions_[E[1]][1],O.$=r[r.length-$],O._$={first_line:n[n.length-($||1)].first_line,last_line:n[n.length-1].last_line,first_column:n[n.length-($||1)].first_column,last_column:n[n.length-1].last_column},m&&(O._$.range=[n[n.length-($||1)].range[0],n[n.length-1].range[1]]),_=this.performAction.apply(O,[c,h,o,g.yy,E[1],r,n].concat(y)),"undefined"!=typeof _)return _;$&&(i=i.slice(0,-1*$*2),r=r.slice(0,-1*$),n=n.slice(0,-1*$)),i.push(this.productions_[E[1]][0]),r.push(O.$),n.push(O._$),A=a[i[i.length-2]][i[i.length-1]],i.push(A);break;case 3:return!0}}return!0}},ut=function(){var e={EOF:1,parseError:function(e,t){if(!this.yy.parser)throw new Error(e);this.yy.parser.parseError(e,t)},setInput:function(e,t){return this.yy=t||this.yy||{},this._input=e,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var e=this._input[0];this.yytext+=e,this.yyleng++,this.offset++,this.match+=e,this.matched+=e;var t=e.match(/(?:\r\n?|\n).*/g);return t?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),e},unput:function(e){var t=e.length,s=e.split(/(?:\r\n?|\n)/g);this._input=e+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-t),this.offset-=t;var i=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),s.length-1&&(this.yylineno-=s.length-1);var r=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:s?(s.length===i.length?this.yylloc.first_column:0)+i[i.length-s.length].length-s[0].length:this.yylloc.first_column-t},this.options.ranges&&(this.yylloc.range=[r[0],r[0]+this.yyleng-t]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(e){this.unput(this.match.slice(e))},pastInput:function(){var e=this.matched.substr(0,this.matched.length-this.match.length);return(e.length>20?"...":"")+e.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var e=this.match;return e.length<20&&(e+=this._input.substr(0,20-e.length)),(e.substr(0,20)+(e.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var e=this.pastInput(),t=new Array(e.length+1).join("-");return e+this.upcomingInput()+"\n"+t+"^"},test_match:function(e,t){var s,i,r;if(this.options.backtrack_lexer&&(r={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(r.yylloc.range=this.yylloc.range.slice(0))),i=e[0].match(/(?:\r\n?|\n).*/g),i&&(this.yylineno+=i.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:i?i[i.length-1].length-i[i.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+e[0].length},this.yytext+=e[0],this.match+=e[0],this.matches=e,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(e[0].length),this.matched+=e[0],s=this.performAction.call(this,this.yy,this,t,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),s)return s;if(this._backtrack){for(var n in r)this[n]=r[n];return!1}return!1},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var e,t,s,i;this._more||(this.yytext="",this.match="");for(var r=this._currentRules(),n=0;nt[0].length)){if(t=s,i=n,this.options.backtrack_lexer){if(e=this.test_match(s,r[n]),e!==!1)return e;if(this._backtrack){t=!1;continue}return!1}if(!this.options.flex)break}return t?(e=this.test_match(t,r[i]),e!==!1?e:!1):""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var e=this.next();return e?e:this.lex()},begin:function(e){this.conditionStack.push(e)},popState:function(){var e=this.conditionStack.length-1;return e>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(e){return e=this.conditionStack.length-1-Math.abs(e||0),e>=0?this.conditionStack[e]:"INITIAL"},pushState:function(e){this.begin(e)},stateStackSize:function(){return this.conditionStack.length},options:{},performAction:function(e,t,s,i){switch(s){case 0:var r=/\\+$/,n=t.yytext.match(r),a=n?n[0].length:null;if(a&&a%2?(t.yytext=t.yytext.replace(/\\$/,""),this.begin("esc")):this.begin("mu"),a>1&&(t.yytext=t.yytext.replace(/(\\\\)+$/,"\\")),t.yytext)return 79;break;case 1:var r=/\\+$/,n=t.yytext.match(r),a=n?n[0].length:null;if(a&&a%2?(t.yytext=t.yytext.replace(/\\$/,""),this.begin("esc")):this.begin("h"),a>1&&(t.yytext=t.yytext.replace(/(\\\\)+$/,"\\")),t.yytext)return 79;break;case 2:return 79;case 3:return this.popState(),10;case 4:return this.popState(),t.yytext=t.yytext.replace(/^#\[\[|\]\]#$/g,""),79;case 5:return this.popState(),10;case 6:return 19;case 7:return 25;case 8:return 27;case 9:return 29;case 10:return this.popState(),30;case 11:return this.popState(),30;case 12:return this.popState(),31;case 13:return this.popState(),37;case 14:return 32;case 15:return 20;case 16:return 38;case 17:return 39;case 18:return 35;case 19:return t.yytext;case 20:return t.yytext;case 21:return t.yytext;case 22:return t.yytext;case 23:return t.yytext;case 24:return t.yytext;case 25:return t.yytext;case 26:return t.yytext;case 27:return 33;case 28:return 33;case 29:return t.yytext;case 30:return 46;case 31:var c=this.stateStackSize();if(c>=2&&"c"===this.topState()&&"run"===this.topState(1))return 44;break;case 32:break;case 33:return 70;case 34:return 72;case 35:return 93;case 36:return e.begin=!0,69;case 37:return this.popState(),e.begin===!0?(e.begin=!1,71):79;case 38:return this.begin("c"),21;case 39:if("c"===this.popState()){var c=this.stateStackSize();"run"===this.topState()&&(this.popState(),c-=1);var o=this.topState(c-2);return 2===c&&"h"===o?this.popState():3===c&&"mu"===o&&"h"===this.topState(c-3)&&(this.popState(),this.popState()),22}return 79;case 40:return this.begin("i"),80;case 41:return"i"===this.popState()?81:79;case 42:return 91;case 43:return 77;case 44:return 87;case 45:return 45;case 46:return t.yytext=t.yytext.substr(1,t.yyleng-2).replace(/\\"/g,'"'),89;case 47:return t.yytext=t.yytext.substr(1,t.yyleng-2).replace(/\\'/g,"'"),88;case 48:return 84;case 49:return 84;case 50:return 84;case 51:return 86;case 52:return 34;case 53:return this.begin("run"),34;case 54:return this.begin("h"),19;case 55:return this.popState(),79;case 56:return this.popState(),79;case 57:return this.popState(),79;case 58:return this.popState(),4;case 59:return 4}},rules:[/^(?:[^#]*?(?=\$))/,/^(?:[^\$]*?(?=#))/,/^(?:[^\x00]+)/,/^(?:#\*[\s\S]+?\*#)/,/^(?:#\[\[[\s\S]+?\]\]#)/,/^(?:##[^\n]+)/,/^(?:#(?=[a-zA-Z{]))/,/^(?:set[ ]*)/,/^(?:if[ ]*)/,/^(?:elseif[ ]*)/,/^(?:else\b)/,/^(?:\{else\})/,/^(?:end\b)/,/^(?:break\b)/,/^(?:foreach[ ]*)/,/^(?:noescape\b)/,/^(?:define[ ]*)/,/^(?:macro[ ]*)/,/^(?:in\b)/,/^(?:[%\+\-\*/])/,/^(?:<=)/,/^(?:>=)/,/^(?:[><])/,/^(?:==)/,/^(?:\|\|)/,/^(?:&&)/,/^(?:!=)/,/^(?:\$!(?=[{a-zA-Z_]))/,/^(?:\$(?=[{a-zA-Z_]))/,/^(?:!)/,/^(?:=)/,/^(?:[ ]+(?=[^,]))/,/^(?:\s+)/,/^(?:\{)/,/^(?:\})/,/^(?::[\s]*)/,/^(?:\{)/,/^(?:\})/,/^(?:\([\s]*(?=[$'"\[\{\-0-9\w()!]))/,/^(?:\))/,/^(?:\[[\s]*(?=[\-$"'0-9{\[\]]+))/,/^(?:\])/,/^(?:\.\.)/,/^(?:\.(?=[a-zA-Z_]))/,/^(?:\.(?=[\d]))/,/^(?:,[ ]*)/,/^(?:"(\\"|[^\"])*")/,/^(?:'(\\'|[^\'])*')/,/^(?:null\b)/,/^(?:false\b)/,/^(?:true\b)/,/^(?:[0-9]+)/,/^(?:[_a-zA-Z][a-zA-Z0-9_\-]*)/,/^(?:[_a-zA-Z][a-zA-Z0-9_\-]*[ ]*(?=\())/,/^(?:#)/,/^(?:.)/,/^(?:\s+)/,/^(?:[\$#])/,/^(?:$)/,/^(?:$)/],conditions:{mu:{rules:[5,27,28,36,37,38,39,40,41,43,52,54,55,56,58],inclusive:!1},c:{rules:[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,38,39,40,41,43,44,45,46,47,48,49,50,51,52],inclusive:!1},i:{rules:[18,19,20,21,22,23,24,25,26,27,28,29,30,32,33,33,34,34,35,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52],inclusive:!1},h:{rules:[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,27,28,29,30,35,38,39,40,41,43,51,53,55,56,58],inclusive:!1},esc:{rules:[57],inclusive:!1},run:{rules:[27,28,29,31,32,33,34,35,38,39,40,41,43,44,45,46,47,48,49,50,51,52,55,56,58],inclusive:!1},INITIAL:{rules:[0,1,2,59],inclusive:!0}}};return e}();return lt.lexer=ut,e.prototype=lt,lt.Parser=e,new e}();"undefined"!=typeof e&&"undefined"!=typeof t&&(t.parser=i,t.Parser=i.Parser,t.parse=function(){return i.parse.apply(i,arguments)},t.main=function(s){s[1]||(console.log("Usage: "+s[0]+" FILE"),process.exit(1));var i=e("fs").readFileSync(e("path").normalize(s[1]),"utf8");return t.parser.parse(i)},"undefined"!=typeof s&&e.main===s&&t.main(process.argv.slice(1)))}),define("velocityjs/0.4.10/src/utils",[],function(e,t,s){"use strict";function i(e,t){var s={"if":1,foreach:1,macro:1,noescape:1,define:1},r=e.length;t=t||0;for(var n=[],a=t-1,c=t;r>c;c++)if(!(a>=c)){var o=e[c],h=o.type;if(s[h]||"end"===h){if("end"===h)return{arr:n,step:c};var l=i(e,c+1);a=l.step,l.arr.unshift(e[c]),n.push(l.arr)}else n.push(o)}return n}var r={};["forEach","some","every","filter","map"].forEach(function(e){r[e]=function(t,s,i){if(!t||"string"==typeof t)return t;if(i=i||this,t[e])return t[e](s,i);var r=Object.keys(t);return r[e](function(e){return s.call(i,t[e],e,t)},i)}});var n=0;r.guid=function(){return n++},r.mixin=function(e,t){return r.forEach(t,function(t,s){({}).toString.call(t);e[s]=r.isArray(t)||r.isObject(t)?r.mixin(t,e[s]||{}):t}),e},r.isArray=function(e){return"[object Array]"==={}.toString.call(e)},r.isObject=function(e){return"[object Object]"==={}.toString.call(e)},r.indexOf=function(e,t){return r.isArray(t)?t.indexOf(e):void 0},r.keys=Object.keys,r.now=Date.now,r.makeLevel=i,s.exports=r}),define("velocityjs/0.4.10/src/compile/index",[],function(e,t,s){function i(e,t){this.asts=e,this.config={escape:!0,unescape:{}},r.mixin(this.config,t),this.init()}var r=e("velocityjs/0.4.10/src/utils"),n=e("velocityjs/0.4.10/src/helper/index");i.Helper=n,i.prototype={constructor:i},e("velocityjs/0.4.10/src/compile/blocks")(i,r),e("velocityjs/0.4.10/src/compile/literal")(i,r),e("velocityjs/0.4.10/src/compile/references")(i,r),e("velocityjs/0.4.10/src/compile/set")(i,r),e("velocityjs/0.4.10/src/compile/expression")(i,r),e("velocityjs/0.4.10/src/compile/compile")(i,r),s.exports=i}),define("velocityjs/0.4.10/src/helper/index",[],function(e,t,s){var i={},r=e("velocityjs/0.4.10/src/utils");e("velocityjs/0.4.10/src/helper/text")(i,r),s.exports=i}),define("velocityjs/0.4.10/src/helper/text",[],function(e,t,s){s.exports=function(e,t){function s(e){var r=e.leader,n=void 0!==e.args;return"macro_call"===e.type&&(r="#"),e.isWraped&&(r+="{"),r+=n?i(e):e.id,t.forEach(e.path,function(e){if("method"==e.type)r+="."+i(e);else if("index"==e.type){var t="",n=e.id;if("integer"===n.type)t=n.value;else if("string"===n.type){var a=n.isEval?'"':"'";t=a+n.value+a}else t=s(n);r+="["+t+"]"}else"property"==e.type&&(r+="."+e.id)},this),e.isWraped&&(r+="}"),r}function i(e){var s=[],i="";return t.forEach(e.args,function(e){s.push(r(e))}),i+=e.id+"("+s.join(",")+")"}function r(e){var i="";switch(e.type){case"string":var n=e.isEval?'"':"'";i=n+e.value+n;break;case"integer":case"bool":i=e.value;break;case"array":i="[";var a=e.value.length-1;t.forEach(e.value,function(e,t){i+=r(e),t!==a&&(i+=", ")}),i+="]";break;default:i=s(e)}return i}e.getRefText=s}}),define("velocityjs/0.4.10/src/compile/blocks",[],function(e,t,s){s.exports=function(e,t){t.mixin(e.prototype,{getBlock:function(e){var t=e[0],s="";switch(t.type){case"if":s=this.getBlockIf(e);break;case"foreach":s=this.getBlockEach(e);break;case"macro":this.setBlockMacro(e);break;case"noescape":s=this._render(e.slice(1));break;case"define":this.setBlockDefine(e);break;default:s=this._render(e)}return s||""},setBlockDefine:function(e){var t=e[0],s=e.slice(1),i=this.defines;i[t.id]=s},setBlockMacro:function(e){var t=e[0],s=e.slice(1),i=this.macros;i[t.id]={asts:s,args:t.args}},getMacro:function(s){var i=this.macros[s.id],r="";if(i){var n=i.asts,a=i.args,c=s.args,o={},h=t.guid(),l="macro:"+s.id+":"+h;t.forEach(a,function(e,t){o[e.id]=c[t]?this.getLiteral(c[t]):void 0},this),r=this.eval(n,o,l)}else{var u=this.jsmacros;i=u[s.id];var p=[];if(i&&i.apply){t.forEach(s.args,function(e){p.push(this.getLiteral(e))},this);try{r=i.apply(this,p)}catch(y){var f=s.pos,g=e.Helper.getRefText(s),d="\n at "+g+" L/N "+f.first_line+":"+f.first_column;throw y.name="",y.message+=d,new Error(y)}}}return r},eval:function(s,i,r){if(!i)return t.isArray(s)?this._render(s):this.evalStr(s);var n=[],a=e.Parser;if(r=r||"eval:"+t.guid(),t.isArray(s)?n=s:a&&(n=a.parse(s)),n.length){this.local[r]=i;var c=this._render(n,r);return this.local[r]={},this.conditions.shift(),this.condition=this.conditions[0]||"",c}},getBlockEach:function(e){var s=e[0],i=this.getLiteral(s.from),r=e.slice(1),n=s.to,a={foreach:{count:0}},c="",o=t.guid(),h="foreach:"+o,l={}.toString.call(i);if(i&&("[object Array]"===l||"[object Object]"===l)){var u=t.isArray(i)?i.length:t.keys(i).length;return t.forEach(i,function(e,t){this.setBreak||(a[n]=e,a.foreach.count=t+1,a.foreach.index=t,a.foreach.hasNext=u>t+1,a.velocityCount=t+1,this.local[h]=a,c+=this._render(r,h))},this),this.setBreak=!1,this.local[h]={},this.conditions.shift(),this.condition=this.conditions[0]||"",c}},getBlockIf:function(e){var s=!1,i=[];return t.some(e,function(e){if(e.condition){if(s)return!0;s=this.getExpression(e.condition)}else if("else"===e.type){if(s)return!0;s=!0}else s&&i.push(e);return!1},this),this._render(i)}})}}),define("velocityjs/0.4.10/src/compile/literal",[],function(e,t,s){s.exports=function(e,t){t.mixin(e.prototype,{getLiteral:function(e){var s=e.type,i="";if("string"==s)i=this.getString(e);else if("integer"==s)i=parseInt(e.value,10);else if("decimal"==s)i=parseFloat(e.value,10);else if("array"==s)i=this.getArray(e);else if("map"==s){i={};var r=e.value;t.forEach(r,function(e,t){i[t]=this.getLiteral(e)},this)}else{if("bool"!=s)return this.getReferences(e);"null"===e.value?i=null:"false"===e.value?i=!1:"true"===e.value&&(i=!0)}return i},getString:function(e){var t=e.value,s=t; -return!e.isEval||-1===t.indexOf("#")&&-1===t.indexOf("$")||(s=this.evalStr(t)),s},getArray:function(e){var s=[];if(e.isRange){var i=e.value[0];"references"===i.type&&(i=this.getReferences(i));var r=e.value[1];"references"===r.type&&(r=this.getReferences(r)),r=parseInt(r,10),i=parseInt(i,10);var n;if(!isNaN(i)&&!isNaN(r))if(r>i)for(n=i;r>=n;n++)s.push(n);else for(n=i;n>=r;n--)s.push(n)}else t.forEach(e.value,function(e){s.push(this.getLiteral(e))},this);return s},evalStr:function(t){var s=e.Parser.parse(t);return this._render(s)}})}}),define("velocityjs/0.4.10/src/compile/references",[],function(e,t,s){s.exports=function(e,t){"use strict";function s(e){return t.isArray(e)?e.length:t.isObject(e)?t.keys(e).length:void 0}function i(e){if("string"!=typeof e)return e;var t,s,i,r="",n=!1;for(t=0;t=" "&&"~">=s||"\r"==s||"\n"==s?"&"==s?(i="&",n=!0):"<"==s?(i="<",n=!0):">"==s?(i=">",n=!0):i=s.toString():i="&#"+s.charCodeAt().toString()+";",r+=i;return n?r:e}t.mixin(e.prototype,{addIgnoreEscpape:function(e){t.isArray(e)||(e=[e]),t.forEach(e,function(e){this.config.unescape[e]=!0},this)},getReferences:function(s,r){if(s.prue){var n=this.defines[s.id];if(t.isArray(n))return this._render(n);s.id in this.config.unescape&&(s.prue=!1)}var a=this.config.escape,c=this.silence||"$!"===s.leader,o=void 0!==s.args,h=this.context,l=h[s.id],u=this.getLocal(s),p=e.Helper.getRefText(s);return p in h?s.prue&&a?i(h[p]):h[p]:(void 0!==l&&o&&(l=this.getPropMethod(s,h,s)),u.isLocaled&&(l=u.value),s.path&&void 0!==l&&t.some(s.path,function(e){l=this.getAttributes(e,l,s)},this),r&&void 0===l&&(l=c?"":e.Helper.getRefText(s)),l=s.prue&&a?i(l):l)},getLocal:function(e){var s=e.id,i=this.local,r=!1,n=t.some(this.conditions,function(e){var t=i[e];return s in t?(r=t[s],!0):!1},this);return{value:r,isLocaled:n}},getAttributes:function(e,t,s){var i,r=e.type,n=e.id;return i="method"===r?this.getPropMethod(e,t,s):"property"===r?t[n]:this.getPropIndex(e,t)},getPropIndex:function(e,t){var s,i=e.id;return s="references"===i.type?this.getReferences(i):"integer"===i.type?i.value:i.value,t[s]},getPropMethod:function(i,r,n){var a=i.id,c="",o=a.slice(3);if(!(0!==a.indexOf("get")||a in r))return o?c=r[o]:(o=this.getLiteral(i.args[0]),c=r[o]),c;if(0===a.indexOf("set")&&!r[a])return r[o]=this.getLiteral(i.args[0]),r.toString=function(){return""},r;if(!(0!==a.indexOf("is")||a in r))return o=a.slice(2),c=r[o];if("keySet"===a)return t.keys(r);if("entrySet"===a)return c=[],t.forEach(r,function(e,t){c.push({key:t,value:e})}),c;if("size"===a)return s(r);c=r[a];var h=[];if(t.forEach(i.args,function(e){h.push(this.getLiteral(e))},this),c&&c.call){var l=this;r.eval=function(){return l.eval.apply(l,arguments)};try{c=c.apply(r,h)}catch(u){var p=n.pos,y=e.Helper.getRefText(n),f=" on "+y+" at L/N "+p.first_line+":"+p.first_column;throw u.name="",u.message+=f,new Error(u)}}else c=void 0;return c}})}}),define("velocityjs/0.4.10/src/compile/set",[],function(e,t,s){s.exports=function(e,t){t.mixin(e.prototype,{getContext:function(){var e=this.condition,t=this.local;return e?t[e]:this.context},setValue:function(e){var s=e.equal[0],i=this.getContext();this.condition&&0===this.condition.indexOf("macro:")?i=this.context:null!=this.context[s.id]&&(i=this.context);var r,n=e.equal[1];if(r="math"===n.type?this.getExpression(n):this.getLiteral(e.equal[1]),s.path){var a=i[s.id];"object"!=typeof a&&(a={}),i[s.id]=a;var c=s.path?s.path.length:0;t.forEach(s.path,function(e,t){var s=c===t+1,i=e.id;"index"===e.type&&(i=i.value),a[i]=s?r:{},a=a[i]})}else i[s.id]=r}})}}),define("velocityjs/0.4.10/src/compile/expression",[],function(e,t,s){s.exports=function(e,t){t.mixin(e.prototype,{getExpression:function(e){var t,s=e.expression;if("math"===e.type){switch(e.operator){case"+":t=this.getExpression(s[0])+this.getExpression(s[1]);break;case"-":t=this.getExpression(s[0])-this.getExpression(s[1]);break;case"/":t=this.getExpression(s[0])/this.getExpression(s[1]);break;case"%":t=this.getExpression(s[0])%this.getExpression(s[1]);break;case"*":t=this.getExpression(s[0])*this.getExpression(s[1]);break;case"||":t=this.getExpression(s[0])||this.getExpression(s[1]);break;case"&&":t=this.getExpression(s[0])&&this.getExpression(s[1]);break;case">":t=this.getExpression(s[0])>this.getExpression(s[1]);break;case"<":t=this.getExpression(s[0])=":t=this.getExpression(s[0])>=this.getExpression(s[1]);break;case"<=":t=this.getExpression(s[0])<=this.getExpression(s[1]);break;case"!=":t=this.getExpression(s[0])!=this.getExpression(s[1]);break;case"minus":t=-this.getExpression(s[0]);break;case"not":t=!this.getExpression(s[0]);break;case"parenthesis":t=this.getExpression(s[0]);break;default:return}return t}return this.getLiteral(e)}})}}),define("velocityjs/0.4.10/src/compile/compile",[],function(e,t,s){s.exports=function(e,t){t.mixin(e.prototype,{init:function(){this.context={},this.macros={},this.defines={},this.conditions=[],this.local={},this.silence=!1,this.unescape={}},render:function(e,s,i){this.silence=!!i,this.context=e||{},this.jsmacros=s||{};var r=t.now(),n=this._render(),a=t.now(),c=a-r;return this.cost=c,n},_render:function(e,s){var i="";return e=e||this.asts,s?(s!==this.condition&&-1===t.indexOf(s,this.conditions)&&this.conditions.unshift(s),this.condition=s):this.condition=null,t.forEach(e,function(e){switch(e.type){case"references":i+=this.getReferences(e,!0);break;case"set":this.setValue(e);break;case"break":this.setBreak=!0;break;case"macro_call":i+=this.getMacro(e);break;case"comment":break;default:i+="string"==typeof e?e:this.getBlock(e)}},this),i}})}}); \ No newline at end of file +define('velocityjs/0.4.10/index', [], function(e, t, s) { + 'use strict'; + s.exports = e('velocityjs/0.4.10/src/velocity'); +}), + define('velocityjs/0.4.10/src/velocity', [], function(e, t, s) { + var i = e('velocityjs/0.4.10/src/parse/index'), + r = e('velocityjs/0.4.10/src/utils'), + n = e('velocityjs/0.4.10/src/compile/index'), + a = e('velocityjs/0.4.10/src/helper/index'); + (n.Parser = i), + (i._parse = i.parse), + (i.parse = function(e) { + var t = i._parse(e); + return ( + r.forEach(t, function(e, s) { + var i = /^[ \t]*\n/; + if (e.type && 'references' !== e.type) { + var r = t[s + 1]; + 'string' == typeof r && i.test(r) && (t[s + 1] = r.replace(i, '')); + } + }), + r.makeLevel(t) + ); + }); + var c = { Parser: i, Compile: n, Helper: a }; + (c.render = function(e, t, s) { + var r = i.parse(e), + a = new n(r); + return a.render(t, s); + }), + (s.exports = c); + }), + define('velocityjs/0.4.10/src/parse/index', [], function(e, t, s) { + var i = (function() { + function e() { + this.yy = {}; + } + var t = function(e, t, s, i) { + for (s = s || {}, i = e.length; i--; s[e[i]] = t); + return s; + }, + s = [1, 8], + i = [1, 18], + r = [1, 9], + n = [1, 22], + a = [1, 21], + c = [4, 10, 19, 33, 34, 79], + o = [1, 26], + h = [1, 29], + l = [1, 30], + u = [4, 10, 19, 22, 33, 34, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81, 91], + p = [1, 46], + y = [1, 51], + f = [1, 52], + g = [1, 66], + d = [1, 67], + v = [1, 78], + m = [1, 89], + b = [1, 81], + k = [1, 79], + x = [1, 84], + E = [1, 88], + _ = [1, 85], + S = [1, 86], + $ = [4, 10, 19, 22, 33, 34, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 71, 72, 77, 79, 80, 81, 91], + A = [1, 115], + I = [1, 111], + O = [1, 112], + j = [1, 123], + L = [22, 45, 81], + N = [2, 89], + R = [22, 44, 45, 72, 81], + P = [22, 44, 45, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81], + T = [22, 44, 45, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81, 93], + w = [2, 102], + C = [22, 44, 45, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 72, 79, 81, 91], + M = [2, 105], + B = [1, 132], + D = [1, 138], + F = [22, 44, 45], + H = [1, 143], + z = [1, 144], + G = [1, 145], + V = [1, 146], + Z = [1, 147], + K = [1, 148], + W = [1, 149], + q = [1, 150], + U = [1, 151], + Q = [1, 152], + Y = [1, 153], + J = [1, 154], + X = [1, 155], + et = [22, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61], + tt = [45, 81], + st = [2, 106], + it = [22, 33], + rt = [1, 202], + nt = [1, 201], + at = [45, 72], + ct = [22, 49, 50], + ot = [22, 49, 50, 51, 52, 56, 57, 58, 59, 60, 61], + ht = [22, 49, 50, 56, 57, 58, 59, 60, 61], + lt = { + trace: function() {}, + yy: {}, + symbols_: { + error: 2, + root: 3, + EOF: 4, + statements: 5, + statement: 6, + references: 7, + directives: 8, + content: 9, + COMMENT: 10, + set: 11, + if: 12, + elseif: 13, + else: 14, + end: 15, + foreach: 16, + break: 17, + define: 18, + HASH: 19, + NOESCAPE: 20, + PARENTHESIS: 21, + CLOSE_PARENTHESIS: 22, + macro: 23, + macro_call: 24, + SET: 25, + equal: 26, + IF: 27, + expression: 28, + ELSEIF: 29, + ELSE: 30, + END: 31, + FOREACH: 32, + DOLLAR: 33, + ID: 34, + IN: 35, + array: 36, + BREAK: 37, + DEFINE: 38, + MACRO: 39, + macro_args: 40, + macro_call_args_all: 41, + macro_call_args: 42, + literals: 43, + SPACE: 44, + COMMA: 45, + EQUAL: 46, + map: 47, + math: 48, + '||': 49, + '&&': 50, + '+': 51, + '-': 52, + '*': 53, + '/': 54, + '%': 55, + '>': 56, + '<': 57, + '==': 58, + '>=': 59, + '<=': 60, + '!=': 61, + parenthesis: 62, + '!': 63, + literal: 64, + brace_begin: 65, + attributes: 66, + brace_end: 67, + methodbd: 68, + VAR_BEGIN: 69, + MAP_BEGIN: 70, + VAR_END: 71, + MAP_END: 72, + attribute: 73, + method: 74, + index: 75, + property: 76, + DOT: 77, + params: 78, + CONTENT: 79, + BRACKET: 80, + CLOSE_BRACKET: 81, + string: 82, + number: 83, + BOOL: 84, + integer: 85, + INTEGER: 86, + DECIMAL_POINT: 87, + STRING: 88, + EVAL_STRING: 89, + range: 90, + RANGE: 91, + map_item: 92, + MAP_SPLIT: 93, + $accept: 0, + $end: 1, + }, + terminals_: { + 2: 'error', + 4: 'EOF', + 10: 'COMMENT', + 19: 'HASH', + 20: 'NOESCAPE', + 21: 'PARENTHESIS', + 22: 'CLOSE_PARENTHESIS', + 25: 'SET', + 27: 'IF', + 29: 'ELSEIF', + 30: 'ELSE', + 31: 'END', + 32: 'FOREACH', + 33: 'DOLLAR', + 34: 'ID', + 35: 'IN', + 37: 'BREAK', + 38: 'DEFINE', + 39: 'MACRO', + 44: 'SPACE', + 45: 'COMMA', + 46: 'EQUAL', + 49: '||', + 50: '&&', + 51: '+', + 52: '-', + 53: '*', + 54: '/', + 55: '%', + 56: '>', + 57: '<', + 58: '==', + 59: '>=', + 60: '<=', + 61: '!=', + 63: '!', + 69: 'VAR_BEGIN', + 70: 'MAP_BEGIN', + 71: 'VAR_END', + 72: 'MAP_END', + 77: 'DOT', + 79: 'CONTENT', + 80: 'BRACKET', + 81: 'CLOSE_BRACKET', + 84: 'BOOL', + 86: 'INTEGER', + 87: 'DECIMAL_POINT', + 88: 'STRING', + 89: 'EVAL_STRING', + 91: 'RANGE', + 93: 'MAP_SPLIT', + }, + productions_: [ + 0, + [3, 1], + [3, 2], + [5, 1], + [5, 2], + [6, 1], + [6, 1], + [6, 1], + [6, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 1], + [8, 4], + [8, 1], + [8, 1], + [11, 5], + [12, 5], + [13, 5], + [14, 2], + [15, 2], + [16, 8], + [16, 8], + [17, 2], + [18, 6], + [23, 6], + [23, 5], + [40, 1], + [40, 2], + [24, 5], + [24, 4], + [42, 1], + [42, 1], + [42, 3], + [42, 3], + [42, 3], + [42, 3], + [41, 1], + [41, 2], + [41, 3], + [41, 2], + [26, 3], + [28, 1], + [28, 1], + [28, 1], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 3], + [48, 1], + [48, 2], + [48, 2], + [48, 1], + [48, 1], + [62, 3], + [7, 5], + [7, 3], + [7, 5], + [7, 3], + [7, 2], + [7, 4], + [7, 2], + [7, 4], + [65, 1], + [65, 1], + [67, 1], + [67, 1], + [66, 1], + [66, 2], + [73, 1], + [73, 1], + [73, 1], + [74, 2], + [68, 4], + [68, 3], + [78, 1], + [78, 1], + [78, 3], + [78, 3], + [76, 2], + [76, 2], + [75, 3], + [75, 3], + [75, 3], + [75, 2], + [75, 2], + [64, 1], + [64, 1], + [64, 1], + [83, 1], + [83, 3], + [83, 4], + [85, 1], + [85, 2], + [82, 1], + [82, 1], + [43, 1], + [43, 1], + [43, 1], + [36, 3], + [36, 1], + [36, 2], + [90, 5], + [90, 5], + [90, 5], + [90, 5], + [47, 3], + [47, 2], + [92, 3], + [92, 3], + [92, 2], + [92, 5], + [92, 5], + [9, 1], + [9, 1], + [9, 2], + [9, 3], + [9, 3], + [9, 2], + ], + performAction: function(e, t, s, i, r, n) { + var a = n.length - 1; + switch (r) { + case 1: + return []; + case 2: + return n[a - 1]; + case 3: + case 31: + case 35: + case 36: + case 80: + case 88: + case 89: + this.$ = [n[a]]; + break; + case 4: + case 32: + case 81: + this.$ = [].concat(n[a - 1], n[a]); + break; + case 5: + (n[a].prue = !0), (n[a].pos = this._$), (this.$ = n[a]); + break; + case 6: + (n[a].pos = this._$), (this.$ = n[a]); + break; + case 7: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 18: + case 19: + case 41: + case 42: + case 46: + case 47: + case 48: + case 62: + case 65: + case 66: + case 76: + case 77: + case 78: + case 79: + case 85: + case 92: + case 99: + case 100: + case 105: + case 111: + case 113: + case 126: + case 127: + this.$ = n[a]; + break; + case 8: + this.$ = { type: 'comment', value: n[a] }; + break; + case 17: + this.$ = { type: 'noescape' }; + break; + case 20: + this.$ = { type: 'set', equal: n[a - 1] }; + break; + case 21: + this.$ = { type: 'if', condition: n[a - 1] }; + break; + case 22: + this.$ = { type: 'elseif', condition: n[a - 1] }; + break; + case 23: + this.$ = { type: 'else' }; + break; + case 24: + this.$ = { type: 'end' }; + break; + case 25: + case 26: + this.$ = { type: 'foreach', to: n[a - 3], from: n[a - 1] }; + break; + case 27: + this.$ = { type: n[a] }; + break; + case 28: + this.$ = { type: 'define', id: n[a - 1] }; + break; + case 29: + this.$ = { type: 'macro', id: n[a - 2], args: n[a - 1] }; + break; + case 30: + this.$ = { type: 'macro', id: n[a - 1] }; + break; + case 33: + this.$ = { type: 'macro_call', id: n[a - 3].replace(/^\s+|\s+$/g, ''), args: n[a - 1] }; + break; + case 34: + this.$ = { type: 'macro_call', id: n[a - 2].replace(/^\s+|\s+$/g, '') }; + break; + case 37: + case 38: + case 39: + case 40: + case 90: + case 91: + this.$ = [].concat(n[a - 2], n[a]); + break; + case 43: + case 44: + case 94: + case 95: + this.$ = n[a - 1]; + break; + case 45: + this.$ = [n[a - 2], n[a]]; + break; + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + case 58: + case 59: + case 60: + case 61: + this.$ = { type: 'math', expression: [n[a - 2], n[a]], operator: n[a - 1] }; + break; + case 63: + this.$ = { type: 'math', expression: [n[a]], operator: 'minus' }; + break; + case 64: + this.$ = { type: 'math', expression: [n[a]], operator: 'not' }; + break; + case 67: + this.$ = { type: 'math', expression: [n[a - 1]], operator: 'parenthesis' }; + break; + case 68: + this.$ = { type: 'references', id: n[a - 2], path: n[a - 1], isWraped: !0, leader: n[a - 4] }; + break; + case 69: + this.$ = { type: 'references', id: n[a - 1], path: n[a], leader: n[a - 2] }; + break; + case 70: + this.$ = { type: 'references', id: n[a - 2].id, path: n[a - 1], isWraped: !0, leader: n[a - 4], args: n[a - 2].args }; + break; + case 71: + this.$ = { type: 'references', id: n[a - 1].id, path: n[a], leader: n[a - 2], args: n[a - 1].args }; + break; + case 72: + this.$ = { type: 'references', id: n[a], leader: n[a - 1] }; + break; + case 73: + this.$ = { type: 'references', id: n[a - 1], isWraped: !0, leader: n[a - 3] }; + break; + case 74: + this.$ = { type: 'references', id: n[a].id, leader: n[a - 1], args: n[a].args }; + break; + case 75: + this.$ = { type: 'references', id: n[a - 1].id, isWraped: !0, args: n[a - 1].args, leader: n[a - 3] }; + break; + case 82: + this.$ = { type: 'method', id: n[a].id, args: n[a].args }; + break; + case 83: + this.$ = { type: 'index', id: n[a] }; + break; + case 84: + (this.$ = { type: 'property', id: n[a] }), 'content' === n[a].type && (this.$ = n[a]); + break; + case 86: + this.$ = { id: n[a - 3], args: n[a - 1] }; + break; + case 87: + this.$ = { id: n[a - 2], args: !1 }; + break; + case 93: + this.$ = { type: 'content', value: n[a - 1] + n[a] }; + break; + case 96: + this.$ = { type: 'content', value: n[a - 2] + n[a - 1].value + n[a] }; + break; + case 97: + case 98: + this.$ = { type: 'content', value: n[a - 1] + n[a] }; + break; + case 101: + this.$ = { type: 'bool', value: n[a] }; + break; + case 102: + this.$ = { type: 'integer', value: n[a] }; + break; + case 103: + this.$ = { type: 'decimal', value: +(n[a - 2] + '.' + n[a]) }; + break; + case 104: + this.$ = { type: 'decimal', value: -(n[a - 2] + '.' + n[a]) }; + break; + case 106: + this.$ = -parseInt(n[a], 10); + break; + case 107: + this.$ = { type: 'string', value: n[a] }; + break; + case 108: + this.$ = { type: 'string', value: n[a], isEval: !0 }; + break; + case 109: + case 110: + this.$ = n[a]; + break; + case 112: + this.$ = { type: 'array', value: n[a - 1] }; + break; + case 114: + this.$ = { type: 'array', value: [] }; + break; + case 115: + case 116: + case 117: + case 118: + this.$ = { type: 'array', isRange: !0, value: [n[a - 3], n[a - 1]] }; + break; + case 119: + this.$ = { type: 'map', value: n[a - 1] }; + break; + case 120: + this.$ = { type: 'map' }; + break; + case 121: + case 122: + (this.$ = {}), (this.$[n[a - 2].value] = n[a]); + break; + case 123: + (this.$ = {}), (this.$[n[a - 1].value] = n[$01]); + break; + case 124: + case 125: + (this.$ = n[a - 4]), (this.$[n[a - 2].value] = n[a]); + break; + case 128: + case 131: + this.$ = n[a - 1] + n[a]; + break; + case 129: + this.$ = n[a - 2] + n[a - 1] + n[a]; + break; + case 130: + this.$ = n[a - 2] + n[a - 1]; + } + }, + table: [ + { + 3: 1, + 4: [1, 2], + 5: 3, + 6: 4, + 7: 5, + 8: 6, + 9: 7, + 10: s, + 11: 10, + 12: 11, + 13: 12, + 14: 13, + 15: 14, + 16: 15, + 17: 16, + 18: 17, + 19: i, + 23: 19, + 24: 20, + 33: r, + 34: n, + 79: a, + }, + { 1: [3] }, + { 1: [2, 1] }, + { + 4: [1, 23], + 6: 24, + 7: 5, + 8: 6, + 9: 7, + 10: s, + 11: 10, + 12: 11, + 13: 12, + 14: 13, + 15: 14, + 16: 15, + 17: 16, + 18: 17, + 19: i, + 23: 19, + 24: 20, + 33: r, + 34: n, + 79: a, + }, + t(c, [2, 3]), + t(c, [2, 5]), + t(c, [2, 6]), + t(c, [2, 7]), + t(c, [2, 8]), + { 34: o, 65: 25, 68: 27, 69: h, 70: l, 79: [1, 28] }, + t(c, [2, 9]), + t(c, [2, 10]), + t(c, [2, 11]), + t(c, [2, 12]), + t(c, [2, 13]), + t(c, [2, 14]), + t(c, [2, 15]), + t(c, [2, 16]), + { + 20: [1, 31], + 25: [1, 34], + 27: [1, 35], + 29: [1, 36], + 30: [1, 37], + 31: [1, 38], + 32: [1, 39], + 34: [1, 33], + 37: [1, 40], + 38: [1, 41], + 39: [1, 42], + 79: [1, 32], + }, + t(c, [2, 18]), + t(c, [2, 19]), + t(c, [2, 126]), + t(c, [2, 127]), + { 1: [2, 2] }, + t(c, [2, 4]), + { 34: [1, 43], 68: 44 }, + t(u, [2, 72], { 66: 45, 73: 47, 74: 48, 75: 49, 76: 50, 21: p, 77: y, 80: f }), + t(u, [2, 74], { 73: 47, 74: 48, 75: 49, 76: 50, 66: 53, 77: y, 80: f }), + t(c, [2, 131]), + { 34: [2, 76] }, + { 34: [2, 77] }, + { 21: [1, 54] }, + t(c, [2, 128]), + { 4: [1, 56], 21: [1, 57], 79: [1, 55] }, + { 21: [1, 58] }, + { 21: [1, 59] }, + { 21: [1, 60] }, + t(c, [2, 23]), + t(c, [2, 24]), + { 21: [1, 61] }, + t(c, [2, 27]), + { 21: [1, 62] }, + { 21: [1, 63] }, + { 21: p, 66: 64, 67: 65, 71: g, 72: d, 73: 47, 74: 48, 75: 49, 76: 50, 77: y, 80: f }, + { 66: 68, 67: 69, 71: g, 72: d, 73: 47, 74: 48, 75: 49, 76: 50, 77: y, 80: f }, + t(u, [2, 69], { 74: 48, 75: 49, 76: 50, 73: 70, 77: y, 80: f }), + { + 7: 74, + 22: [1, 72], + 33: v, + 36: 75, + 43: 73, + 47: 76, + 52: m, + 64: 77, + 70: b, + 78: 71, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + t($, [2, 80]), + t($, [2, 82]), + t($, [2, 83]), + t($, [2, 84]), + { 34: [1, 91], 68: 90, 79: [1, 92] }, + { 7: 94, 33: v, 52: m, 64: 93, 79: [1, 95], 81: [1, 96], 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + t(u, [2, 71], { 74: 48, 75: 49, 76: 50, 73: 70, 77: y, 80: f }), + { 22: [1, 97] }, + t(c, [2, 129]), + t(c, [2, 130]), + { + 7: 103, + 22: [1, 99], + 33: v, + 36: 75, + 41: 98, + 42: 100, + 43: 102, + 44: [1, 101], + 47: 76, + 52: m, + 64: 77, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + { 7: 105, 26: 104, 33: v }, + { + 7: 113, + 21: A, + 28: 106, + 33: v, + 36: 107, + 47: 108, + 48: 109, + 52: I, + 62: 110, + 63: O, + 64: 114, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + { + 7: 113, + 21: A, + 28: 116, + 33: v, + 36: 107, + 47: 108, + 48: 109, + 52: I, + 62: 110, + 63: O, + 64: 114, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + { 33: [1, 117] }, + { 33: [1, 118] }, + { 34: [1, 119] }, + { 67: 120, 71: g, 72: d, 73: 70, 74: 48, 75: 49, 76: 50, 77: y, 80: f }, + t(u, [2, 73]), + t(u, [2, 78]), + t(u, [2, 79]), + { 67: 121, 71: g, 72: d, 73: 70, 74: 48, 75: 49, 76: 50, 77: y, 80: f }, + t(u, [2, 75]), + t($, [2, 81]), + { 22: [1, 122], 45: j }, + t($, [2, 87]), + t(L, [2, 88]), + t([22, 45], N), + t(R, [2, 109]), + t(R, [2, 110]), + t(R, [2, 111]), + { 34: o, 65: 25, 68: 27, 69: h, 70: l }, + { + 7: 127, + 33: v, + 36: 75, + 43: 73, + 47: 76, + 52: m, + 64: 77, + 70: b, + 78: 124, + 80: k, + 81: [1, 125], + 82: 82, + 83: 83, + 84: x, + 85: 126, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + t(R, [2, 113]), + { 72: [1, 129], 82: 130, 88: _, 89: S, 92: 128 }, + t(P, [2, 99]), + t(P, [2, 100]), + t(P, [2, 101]), + t(T, [2, 107]), + t(T, [2, 108]), + t(P, w), + t(C, M, { 87: [1, 131] }), + { 86: B }, + t($, [2, 85]), + t($, [2, 92], { 21: p }), + t($, [2, 93]), + { 79: [1, 134], 81: [1, 133] }, + { 81: [1, 135] }, + t($, [2, 97]), + t($, [2, 98]), + t(c, [2, 17]), + { 22: [1, 136] }, + t(c, [2, 34]), + { 22: [2, 41], 44: [1, 137], 45: D }, + { + 7: 103, + 33: v, + 36: 75, + 42: 139, + 43: 102, + 47: 76, + 52: m, + 64: 77, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + t(F, [2, 35]), + t(F, [2, 36]), + { 22: [1, 140] }, + { 46: [1, 141] }, + { 22: [1, 142] }, + { 22: [2, 46] }, + { 22: [2, 47] }, + { 22: [2, 48], 49: H, 50: z, 51: G, 52: V, 53: Z, 54: K, 55: W, 56: q, 57: U, 58: Q, 59: Y, 60: J, 61: X }, + t(et, [2, 62]), + { 21: A, 62: 156, 86: B }, + { 7: 113, 21: A, 33: v, 48: 157, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + t(et, [2, 65]), + t(et, [2, 66]), + { 7: 113, 21: A, 33: v, 48: 158, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 22: [1, 159] }, + { 34: [1, 160] }, + { 34: [1, 161] }, + { 7: 164, 22: [1, 163], 33: v, 40: 162 }, + t(u, [2, 68]), + t(u, [2, 70]), + t($, [2, 86]), + { + 7: 166, + 33: v, + 36: 75, + 43: 165, + 47: 76, + 52: m, + 64: 77, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + { 45: j, 81: [1, 167] }, + t(R, [2, 114]), + t(tt, w, { 91: [1, 168] }), + t(tt, N, { 91: [1, 169] }), + { 45: [1, 171], 72: [1, 170] }, + t(R, [2, 120]), + { 93: [1, 172] }, + { 86: [1, 173] }, + t(C, st, { 87: [1, 174] }), + t($, [2, 94]), + t($, [2, 96]), + t($, [2, 95]), + t(c, [2, 33]), + { + 7: 176, + 22: [2, 44], + 33: v, + 36: 75, + 43: 175, + 47: 76, + 52: m, + 64: 77, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + { + 7: 178, + 33: v, + 36: 75, + 43: 177, + 47: 76, + 52: m, + 64: 77, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + { 22: [2, 42], 44: [1, 179], 45: D }, + t(c, [2, 20]), + { + 7: 113, + 21: A, + 28: 180, + 33: v, + 36: 107, + 47: 108, + 48: 109, + 52: I, + 62: 110, + 63: O, + 64: 114, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + t(c, [2, 21]), + { 7: 113, 21: A, 33: v, 48: 181, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 182, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 183, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 184, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 185, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 186, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 187, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 188, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 189, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 190, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 191, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 192, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + { 7: 113, 21: A, 33: v, 48: 193, 52: I, 62: 110, 63: O, 64: 114, 82: 82, 83: 83, 84: x, 85: 87, 86: E, 88: _, 89: S }, + t(et, [2, 63]), + t(et, [2, 64]), + { 22: [1, 194], 49: H, 50: z, 51: G, 52: V, 53: Z, 54: K, 55: W, 56: q, 57: U, 58: Q, 59: Y, 60: J, 61: X }, + t(c, [2, 22]), + { 35: [1, 195] }, + { 22: [1, 196] }, + { 7: 198, 22: [1, 197], 33: v }, + t(c, [2, 30]), + t(it, [2, 31]), + t(L, [2, 90]), + t(L, [2, 91]), + t(R, [2, 112]), + { 7: 200, 33: v, 52: rt, 85: 199, 86: nt }, + { 7: 204, 33: v, 52: rt, 85: 203, 86: nt }, + t(R, [2, 119]), + { 82: 205, 88: _, 89: S }, + t(at, [2, 123], { + 36: 75, + 47: 76, + 64: 77, + 90: 80, + 82: 82, + 83: 83, + 85: 87, + 43: 206, + 7: 207, + 33: v, + 52: m, + 70: b, + 80: k, + 84: x, + 86: E, + 88: _, + 89: S, + }), + t(P, [2, 103]), + { 86: [1, 208] }, + t(F, [2, 37]), + t(F, [2, 40]), + t(F, [2, 38]), + t(F, [2, 39]), + { + 7: 176, + 22: [2, 43], + 33: v, + 36: 75, + 43: 175, + 47: 76, + 52: m, + 64: 77, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + { 22: [2, 45] }, + t(ct, [2, 49], { 51: G, 52: V, 53: Z, 54: K, 55: W, 56: q, 57: U, 58: Q, 59: Y, 60: J, 61: X }), + t(ct, [2, 50], { 51: G, 52: V, 53: Z, 54: K, 55: W, 56: q, 57: U, 58: Q, 59: Y, 60: J, 61: X }), + t(ot, [2, 51], { 53: Z, 54: K, 55: W }), + t(ot, [2, 52], { 53: Z, 54: K, 55: W }), + t(et, [2, 53]), + t(et, [2, 54]), + t(et, [2, 55]), + t(ht, [2, 56], { 51: G, 52: V, 53: Z, 54: K, 55: W }), + t(ht, [2, 57], { 51: G, 52: V, 53: Z, 54: K, 55: W }), + t(ht, [2, 58], { 51: G, 52: V, 53: Z, 54: K, 55: W }), + t(ht, [2, 59], { 51: G, 52: V, 53: Z, 54: K, 55: W }), + t(ht, [2, 60], { 51: G, 52: V, 53: Z, 54: K, 55: W }), + t(ht, [2, 61], { 51: G, 52: V, 53: Z, 54: K, 55: W }), + t(et, [2, 67]), + { 7: 209, 33: v, 36: 210, 80: k, 90: 80 }, + t(c, [2, 28]), + t(c, [2, 29]), + t(it, [2, 32]), + { 81: [1, 211] }, + { 81: [1, 212] }, + { 81: M }, + { 86: [1, 213] }, + { 81: [1, 214] }, + { 81: [1, 215] }, + { 93: [1, 216] }, + t(at, [2, 121]), + t(at, [2, 122]), + t(P, [2, 104]), + { 22: [1, 217] }, + { 22: [1, 218] }, + t(R, [2, 115]), + t(R, [2, 117]), + { 81: st }, + t(R, [2, 116]), + t(R, [2, 118]), + { + 7: 219, + 33: v, + 36: 75, + 43: 220, + 47: 76, + 52: m, + 64: 77, + 70: b, + 80: k, + 82: 82, + 83: 83, + 84: x, + 85: 87, + 86: E, + 88: _, + 89: S, + 90: 80, + }, + t(c, [2, 25]), + t(c, [2, 26]), + t(at, [2, 124]), + t(at, [2, 125]), + ], + defaultActions: { + 2: [2, 1], + 23: [2, 2], + 29: [2, 76], + 30: [2, 77], + 107: [2, 46], + 108: [2, 47], + 180: [2, 45], + 201: [2, 105], + 213: [2, 106], + }, + parseError: function(e, t) { + if (!t.recoverable) throw new Error(e); + this.trace(e); + }, + parse: function(e) { + function t() { + var e; + return (e = f.lex() || p), 'number' != typeof e && (e = s.symbols_[e] || e), e; + } + var s = this, + i = [0], + r = [null], + n = [], + a = this.table, + c = '', + o = 0, + h = 0, + l = 0, + u = 2, + p = 1, + y = n.slice.call(arguments, 1), + f = Object.create(this.lexer), + g = { yy: {} }; + for (var d in this.yy) Object.prototype.hasOwnProperty.call(this.yy, d) && (g.yy[d] = this.yy[d]); + f.setInput(e, g.yy), (g.yy.lexer = f), (g.yy.parser = this), 'undefined' == typeof f.yylloc && (f.yylloc = {}); + var v = f.yylloc; + n.push(v); + var m = f.options && f.options.ranges; + this.parseError = 'function' == typeof g.yy.parseError ? g.yy.parseError : Object.getPrototypeOf(this).parseError; + for (var b, k, x, E, _, S, $, A, I, O = {}; ; ) { + if ( + ((x = i[i.length - 1]), + this.defaultActions[x] + ? (E = this.defaultActions[x]) + : ((null === b || 'undefined' == typeof b) && (b = t()), (E = a[x] && a[x][b])), + 'undefined' == typeof E || !E.length || !E[0]) + ) { + var j = ''; + I = []; + for (S in a[x]) this.terminals_[S] && S > u && I.push("'" + this.terminals_[S] + "'"); + (j = f.showPosition + ? 'Parse error on line ' + + (o + 1) + + ':\n' + + f.showPosition() + + '\nExpecting ' + + I.join(', ') + + ", got '" + + (this.terminals_[b] || b) + + "'" + : 'Parse error on line ' + (o + 1) + ': Unexpected ' + (b == p ? 'end of input' : "'" + (this.terminals_[b] || b) + "'")), + this.parseError(j, { text: f.match, token: this.terminals_[b] || b, line: f.yylineno, loc: v, expected: I }); + } + if (E[0] instanceof Array && E.length > 1) + throw new Error('Parse Error: multiple actions possible at state: ' + x + ', token: ' + b); + switch (E[0]) { + case 1: + i.push(b), + r.push(f.yytext), + n.push(f.yylloc), + i.push(E[1]), + (b = null), + k ? ((b = k), (k = null)) : ((h = f.yyleng), (c = f.yytext), (o = f.yylineno), (v = f.yylloc), l > 0 && l--); + break; + case 2: + if ( + (($ = this.productions_[E[1]][1]), + (O.$ = r[r.length - $]), + (O._$ = { + first_line: n[n.length - ($ || 1)].first_line, + last_line: n[n.length - 1].last_line, + first_column: n[n.length - ($ || 1)].first_column, + last_column: n[n.length - 1].last_column, + }), + m && (O._$.range = [n[n.length - ($ || 1)].range[0], n[n.length - 1].range[1]]), + (_ = this.performAction.apply(O, [c, h, o, g.yy, E[1], r, n].concat(y))), + 'undefined' != typeof _) + ) + return _; + $ && ((i = i.slice(0, -1 * $ * 2)), (r = r.slice(0, -1 * $)), (n = n.slice(0, -1 * $))), + i.push(this.productions_[E[1]][0]), + r.push(O.$), + n.push(O._$), + (A = a[i[i.length - 2]][i[i.length - 1]]), + i.push(A); + break; + case 3: + return !0; + } + } + return !0; + }, + }, + ut = (function() { + var e = { + EOF: 1, + parseError: function(e, t) { + if (!this.yy.parser) throw new Error(e); + this.yy.parser.parseError(e, t); + }, + setInput: function(e, t) { + return ( + (this.yy = t || this.yy || {}), + (this._input = e), + (this._more = this._backtrack = this.done = !1), + (this.yylineno = this.yyleng = 0), + (this.yytext = this.matched = this.match = ''), + (this.conditionStack = ['INITIAL']), + (this.yylloc = { first_line: 1, first_column: 0, last_line: 1, last_column: 0 }), + this.options.ranges && (this.yylloc.range = [0, 0]), + (this.offset = 0), + this + ); + }, + input: function() { + var e = this._input[0]; + (this.yytext += e), this.yyleng++, this.offset++, (this.match += e), (this.matched += e); + var t = e.match(/(?:\r\n?|\n).*/g); + return ( + t ? (this.yylineno++, this.yylloc.last_line++) : this.yylloc.last_column++, + this.options.ranges && this.yylloc.range[1]++, + (this._input = this._input.slice(1)), + e + ); + }, + unput: function(e) { + var t = e.length, + s = e.split(/(?:\r\n?|\n)/g); + (this._input = e + this._input), (this.yytext = this.yytext.substr(0, this.yytext.length - t)), (this.offset -= t); + var i = this.match.split(/(?:\r\n?|\n)/g); + (this.match = this.match.substr(0, this.match.length - 1)), + (this.matched = this.matched.substr(0, this.matched.length - 1)), + s.length - 1 && (this.yylineno -= s.length - 1); + var r = this.yylloc.range; + return ( + (this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: s + ? (s.length === i.length ? this.yylloc.first_column : 0) + i[i.length - s.length].length - s[0].length + : this.yylloc.first_column - t, + }), + this.options.ranges && (this.yylloc.range = [r[0], r[0] + this.yyleng - t]), + (this.yyleng = this.yytext.length), + this + ); + }, + more: function() { + return (this._more = !0), this; + }, + reject: function() { + return this.options.backtrack_lexer + ? ((this._backtrack = !0), this) + : this.parseError( + 'Lexical error on line ' + + (this.yylineno + 1) + + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + + this.showPosition(), + { text: '', token: null, line: this.yylineno } + ); + }, + less: function(e) { + this.unput(this.match.slice(e)); + }, + pastInput: function() { + var e = this.matched.substr(0, this.matched.length - this.match.length); + return (e.length > 20 ? '...' : '') + e.substr(-20).replace(/\n/g, ''); + }, + upcomingInput: function() { + var e = this.match; + return ( + e.length < 20 && (e += this._input.substr(0, 20 - e.length)), + (e.substr(0, 20) + (e.length > 20 ? '...' : '')).replace(/\n/g, '') + ); + }, + showPosition: function() { + var e = this.pastInput(), + t = new Array(e.length + 1).join('-'); + return e + this.upcomingInput() + '\n' + t + '^'; + }, + test_match: function(e, t) { + var s, i, r; + if ( + (this.options.backtrack_lexer && + ((r = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column, + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done, + }), + this.options.ranges && (r.yylloc.range = this.yylloc.range.slice(0))), + (i = e[0].match(/(?:\r\n?|\n).*/g)), + i && (this.yylineno += i.length), + (this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: i + ? i[i.length - 1].length - i[i.length - 1].match(/\r?\n?/)[0].length + : this.yylloc.last_column + e[0].length, + }), + (this.yytext += e[0]), + (this.match += e[0]), + (this.matches = e), + (this.yyleng = this.yytext.length), + this.options.ranges && (this.yylloc.range = [this.offset, (this.offset += this.yyleng)]), + (this._more = !1), + (this._backtrack = !1), + (this._input = this._input.slice(e[0].length)), + (this.matched += e[0]), + (s = this.performAction.call(this, this.yy, this, t, this.conditionStack[this.conditionStack.length - 1])), + this.done && this._input && (this.done = !1), + s) + ) + return s; + if (this._backtrack) { + for (var n in r) this[n] = r[n]; + return !1; + } + return !1; + }, + next: function() { + if (this.done) return this.EOF; + this._input || (this.done = !0); + var e, t, s, i; + this._more || ((this.yytext = ''), (this.match = '')); + for (var r = this._currentRules(), n = 0; n < r.length; n++) + if (((s = this._input.match(this.rules[r[n]])), s && (!t || s[0].length > t[0].length))) { + if (((t = s), (i = n), this.options.backtrack_lexer)) { + if (((e = this.test_match(s, r[n])), e !== !1)) return e; + if (this._backtrack) { + t = !1; + continue; + } + return !1; + } + if (!this.options.flex) break; + } + return t + ? ((e = this.test_match(t, r[i])), e !== !1 ? e : !1) + : '' === this._input + ? this.EOF + : this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: '', + token: null, + line: this.yylineno, + }); + }, + lex: function() { + var e = this.next(); + return e ? e : this.lex(); + }, + begin: function(e) { + this.conditionStack.push(e); + }, + popState: function() { + var e = this.conditionStack.length - 1; + return e > 0 ? this.conditionStack.pop() : this.conditionStack[0]; + }, + _currentRules: function() { + return this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1] + ? this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules + : this.conditions.INITIAL.rules; + }, + topState: function(e) { + return (e = this.conditionStack.length - 1 - Math.abs(e || 0)), e >= 0 ? this.conditionStack[e] : 'INITIAL'; + }, + pushState: function(e) { + this.begin(e); + }, + stateStackSize: function() { + return this.conditionStack.length; + }, + options: {}, + performAction: function(e, t, s, i) { + switch (s) { + case 0: + var r = /\\+$/, + n = t.yytext.match(r), + a = n ? n[0].length : null; + if ( + (a && a % 2 ? ((t.yytext = t.yytext.replace(/\\$/, '')), this.begin('esc')) : this.begin('mu'), + a > 1 && (t.yytext = t.yytext.replace(/(\\\\)+$/, '\\')), + t.yytext) + ) + return 79; + break; + case 1: + var r = /\\+$/, + n = t.yytext.match(r), + a = n ? n[0].length : null; + if ( + (a && a % 2 ? ((t.yytext = t.yytext.replace(/\\$/, '')), this.begin('esc')) : this.begin('h'), + a > 1 && (t.yytext = t.yytext.replace(/(\\\\)+$/, '\\')), + t.yytext) + ) + return 79; + break; + case 2: + return 79; + case 3: + return this.popState(), 10; + case 4: + return this.popState(), (t.yytext = t.yytext.replace(/^#\[\[|\]\]#$/g, '')), 79; + case 5: + return this.popState(), 10; + case 6: + return 19; + case 7: + return 25; + case 8: + return 27; + case 9: + return 29; + case 10: + return this.popState(), 30; + case 11: + return this.popState(), 30; + case 12: + return this.popState(), 31; + case 13: + return this.popState(), 37; + case 14: + return 32; + case 15: + return 20; + case 16: + return 38; + case 17: + return 39; + case 18: + return 35; + case 19: + return t.yytext; + case 20: + return t.yytext; + case 21: + return t.yytext; + case 22: + return t.yytext; + case 23: + return t.yytext; + case 24: + return t.yytext; + case 25: + return t.yytext; + case 26: + return t.yytext; + case 27: + return 33; + case 28: + return 33; + case 29: + return t.yytext; + case 30: + return 46; + case 31: + var c = this.stateStackSize(); + if (c >= 2 && 'c' === this.topState() && 'run' === this.topState(1)) return 44; + break; + case 32: + break; + case 33: + return 70; + case 34: + return 72; + case 35: + return 93; + case 36: + return (e.begin = !0), 69; + case 37: + return this.popState(), e.begin === !0 ? ((e.begin = !1), 71) : 79; + case 38: + return this.begin('c'), 21; + case 39: + if ('c' === this.popState()) { + var c = this.stateStackSize(); + 'run' === this.topState() && (this.popState(), (c -= 1)); + var o = this.topState(c - 2); + return ( + 2 === c && 'h' === o + ? this.popState() + : 3 === c && 'mu' === o && 'h' === this.topState(c - 3) && (this.popState(), this.popState()), + 22 + ); + } + return 79; + case 40: + return this.begin('i'), 80; + case 41: + return 'i' === this.popState() ? 81 : 79; + case 42: + return 91; + case 43: + return 77; + case 44: + return 87; + case 45: + return 45; + case 46: + return (t.yytext = t.yytext.substr(1, t.yyleng - 2).replace(/\\"/g, '"')), 89; + case 47: + return (t.yytext = t.yytext.substr(1, t.yyleng - 2).replace(/\\'/g, "'")), 88; + case 48: + return 84; + case 49: + return 84; + case 50: + return 84; + case 51: + return 86; + case 52: + return 34; + case 53: + return this.begin('run'), 34; + case 54: + return this.begin('h'), 19; + case 55: + return this.popState(), 79; + case 56: + return this.popState(), 79; + case 57: + return this.popState(), 79; + case 58: + return this.popState(), 4; + case 59: + return 4; + } + }, + rules: [ + /^(?:[^#]*?(?=\$))/, + /^(?:[^\$]*?(?=#))/, + /^(?:[^\x00]+)/, + /^(?:#\*[\s\S]+?\*#)/, + /^(?:#\[\[[\s\S]+?\]\]#)/, + /^(?:##[^\n]+)/, + /^(?:#(?=[a-zA-Z{]))/, + /^(?:set[ ]*)/, + /^(?:if[ ]*)/, + /^(?:elseif[ ]*)/, + /^(?:else\b)/, + /^(?:\{else\})/, + /^(?:end\b)/, + /^(?:break\b)/, + /^(?:foreach[ ]*)/, + /^(?:noescape\b)/, + /^(?:define[ ]*)/, + /^(?:macro[ ]*)/, + /^(?:in\b)/, + /^(?:[%\+\-\*/])/, + /^(?:<=)/, + /^(?:>=)/, + /^(?:[><])/, + /^(?:==)/, + /^(?:\|\|)/, + /^(?:&&)/, + /^(?:!=)/, + /^(?:\$!(?=[{a-zA-Z_]))/, + /^(?:\$(?=[{a-zA-Z_]))/, + /^(?:!)/, + /^(?:=)/, + /^(?:[ ]+(?=[^,]))/, + /^(?:\s+)/, + /^(?:\{)/, + /^(?:\})/, + /^(?::[\s]*)/, + /^(?:\{)/, + /^(?:\})/, + /^(?:\([\s]*(?=[$'"\[\{\-0-9\w()!]))/, + /^(?:\))/, + /^(?:\[[\s]*(?=[\-$"'0-9{\[\]]+))/, + /^(?:\])/, + /^(?:\.\.)/, + /^(?:\.(?=[a-zA-Z_]))/, + /^(?:\.(?=[\d]))/, + /^(?:,[ ]*)/, + /^(?:"(\\"|[^\"])*")/, + /^(?:'(\\'|[^\'])*')/, + /^(?:null\b)/, + /^(?:false\b)/, + /^(?:true\b)/, + /^(?:[0-9]+)/, + /^(?:[_a-zA-Z][a-zA-Z0-9_\-]*)/, + /^(?:[_a-zA-Z][a-zA-Z0-9_\-]*[ ]*(?=\())/, + /^(?:#)/, + /^(?:.)/, + /^(?:\s+)/, + /^(?:[\$#])/, + /^(?:$)/, + /^(?:$)/, + ], + conditions: { + mu: { rules: [5, 27, 28, 36, 37, 38, 39, 40, 41, 43, 52, 54, 55, 56, 58], inclusive: !1 }, + c: { + rules: [ + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 38, + 39, + 40, + 41, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + ], + inclusive: !1, + }, + i: { + rules: [ + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 32, + 33, + 33, + 34, + 34, + 35, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + ], + inclusive: !1, + }, + h: { + rules: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 27, 28, 29, 30, 35, 38, 39, 40, 41, 43, 51, 53, 55, 56, 58], + inclusive: !1, + }, + esc: { rules: [57], inclusive: !1 }, + run: { + rules: [27, 28, 29, 31, 32, 33, 34, 35, 38, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 55, 56, 58], + inclusive: !1, + }, + INITIAL: { rules: [0, 1, 2, 59], inclusive: !0 }, + }, + }; + return e; + })(); + return (lt.lexer = ut), (e.prototype = lt), (lt.Parser = e), new e(); + })(); + 'undefined' != typeof e && + 'undefined' != typeof t && + ((t.parser = i), + (t.Parser = i.Parser), + (t.parse = function() { + return i.parse.apply(i, arguments); + }), + (t.main = function(s) { + s[1] || (console.log('Usage: ' + s[0] + ' FILE'), process.exit(1)); + var i = e('fs').readFileSync(e('path').normalize(s[1]), 'utf8'); + return t.parser.parse(i); + }), + 'undefined' != typeof s && e.main === s && t.main(process.argv.slice(1))); + }), + define('velocityjs/0.4.10/src/utils', [], function(e, t, s) { + 'use strict'; + function i(e, t) { + var s = { if: 1, foreach: 1, macro: 1, noescape: 1, define: 1 }, + r = e.length; + t = t || 0; + for (var n = [], a = t - 1, c = t; r > c; c++) + if (!(a >= c)) { + var o = e[c], + h = o.type; + if (s[h] || 'end' === h) { + if ('end' === h) return { arr: n, step: c }; + var l = i(e, c + 1); + (a = l.step), l.arr.unshift(e[c]), n.push(l.arr); + } else n.push(o); + } + return n; + } + var r = {}; + ['forEach', 'some', 'every', 'filter', 'map'].forEach(function(e) { + r[e] = function(t, s, i) { + if (!t || 'string' == typeof t) return t; + if (((i = i || this), t[e])) return t[e](s, i); + var r = Object.keys(t); + return r[e](function(e) { + return s.call(i, t[e], e, t); + }, i); + }; + }); + var n = 0; + (r.guid = function() { + return n++; + }), + (r.mixin = function(e, t) { + return ( + r.forEach(t, function(t, s) { + ({}.toString.call(t)); + e[s] = r.isArray(t) || r.isObject(t) ? r.mixin(t, e[s] || {}) : t; + }), + e + ); + }), + (r.isArray = function(e) { + return '[object Array]' === {}.toString.call(e); + }), + (r.isObject = function(e) { + return '[object Object]' === {}.toString.call(e); + }), + (r.indexOf = function(e, t) { + return r.isArray(t) ? t.indexOf(e) : void 0; + }), + (r.keys = Object.keys), + (r.now = Date.now), + (r.makeLevel = i), + (s.exports = r); + }), + define('velocityjs/0.4.10/src/compile/index', [], function(e, t, s) { + function i(e, t) { + (this.asts = e), (this.config = { escape: !0, unescape: {} }), r.mixin(this.config, t), this.init(); + } + var r = e('velocityjs/0.4.10/src/utils'), + n = e('velocityjs/0.4.10/src/helper/index'); + (i.Helper = n), + (i.prototype = { constructor: i }), + e('velocityjs/0.4.10/src/compile/blocks')(i, r), + e('velocityjs/0.4.10/src/compile/literal')(i, r), + e('velocityjs/0.4.10/src/compile/references')(i, r), + e('velocityjs/0.4.10/src/compile/set')(i, r), + e('velocityjs/0.4.10/src/compile/expression')(i, r), + e('velocityjs/0.4.10/src/compile/compile')(i, r), + (s.exports = i); + }), + define('velocityjs/0.4.10/src/helper/index', [], function(e, t, s) { + var i = {}, + r = e('velocityjs/0.4.10/src/utils'); + e('velocityjs/0.4.10/src/helper/text')(i, r), (s.exports = i); + }), + define('velocityjs/0.4.10/src/helper/text', [], function(e, t, s) { + s.exports = function(e, t) { + function s(e) { + var r = e.leader, + n = void 0 !== e.args; + return ( + 'macro_call' === e.type && (r = '#'), + e.isWraped && (r += '{'), + (r += n ? i(e) : e.id), + t.forEach( + e.path, + function(e) { + if ('method' == e.type) r += '.' + i(e); + else if ('index' == e.type) { + var t = '', + n = e.id; + if ('integer' === n.type) t = n.value; + else if ('string' === n.type) { + var a = n.isEval ? '"' : "'"; + t = a + n.value + a; + } else t = s(n); + r += '[' + t + ']'; + } else 'property' == e.type && (r += '.' + e.id); + }, + this + ), + e.isWraped && (r += '}'), + r + ); + } + function i(e) { + var s = [], + i = ''; + return ( + t.forEach(e.args, function(e) { + s.push(r(e)); + }), + (i += e.id + '(' + s.join(',') + ')') + ); + } + function r(e) { + var i = ''; + switch (e.type) { + case 'string': + var n = e.isEval ? '"' : "'"; + i = n + e.value + n; + break; + case 'integer': + case 'bool': + i = e.value; + break; + case 'array': + i = '['; + var a = e.value.length - 1; + t.forEach(e.value, function(e, t) { + (i += r(e)), t !== a && (i += ', '); + }), + (i += ']'); + break; + default: + i = s(e); + } + return i; + } + e.getRefText = s; + }; + }), + define('velocityjs/0.4.10/src/compile/blocks', [], function(e, t, s) { + s.exports = function(e, t) { + t.mixin(e.prototype, { + getBlock: function(e) { + var t = e[0], + s = ''; + switch (t.type) { + case 'if': + s = this.getBlockIf(e); + break; + case 'foreach': + s = this.getBlockEach(e); + break; + case 'macro': + this.setBlockMacro(e); + break; + case 'noescape': + s = this._render(e.slice(1)); + break; + case 'define': + this.setBlockDefine(e); + break; + default: + s = this._render(e); + } + return s || ''; + }, + setBlockDefine: function(e) { + var t = e[0], + s = e.slice(1), + i = this.defines; + i[t.id] = s; + }, + setBlockMacro: function(e) { + var t = e[0], + s = e.slice(1), + i = this.macros; + i[t.id] = { asts: s, args: t.args }; + }, + getMacro: function(s) { + var i = this.macros[s.id], + r = ''; + if (i) { + var n = i.asts, + a = i.args, + c = s.args, + o = {}, + h = t.guid(), + l = 'macro:' + s.id + ':' + h; + t.forEach( + a, + function(e, t) { + o[e.id] = c[t] ? this.getLiteral(c[t]) : void 0; + }, + this + ), + (r = this.eval(n, o, l)); + } else { + var u = this.jsmacros; + i = u[s.id]; + var p = []; + if (i && i.apply) { + t.forEach( + s.args, + function(e) { + p.push(this.getLiteral(e)); + }, + this + ); + try { + r = i.apply(this, p); + } catch (y) { + var f = s.pos, + g = e.Helper.getRefText(s), + d = '\n at ' + g + ' L/N ' + f.first_line + ':' + f.first_column; + throw ((y.name = ''), (y.message += d), new Error(y)); + } + } + } + return r; + }, + eval: function(s, i, r) { + if (!i) return t.isArray(s) ? this._render(s) : this.evalStr(s); + var n = [], + a = e.Parser; + if (((r = r || 'eval:' + t.guid()), t.isArray(s) ? (n = s) : a && (n = a.parse(s)), n.length)) { + this.local[r] = i; + var c = this._render(n, r); + return (this.local[r] = {}), this.conditions.shift(), (this.condition = this.conditions[0] || ''), c; + } + }, + getBlockEach: function(e) { + var s = e[0], + i = this.getLiteral(s.from), + r = e.slice(1), + n = s.to, + a = { foreach: { count: 0 } }, + c = '', + o = t.guid(), + h = 'foreach:' + o, + l = {}.toString.call(i); + if (i && ('[object Array]' === l || '[object Object]' === l)) { + var u = t.isArray(i) ? i.length : t.keys(i).length; + return ( + t.forEach( + i, + function(e, t) { + this.setBreak || + ((a[n] = e), + (a.foreach.count = t + 1), + (a.foreach.index = t), + (a.foreach.hasNext = u > t + 1), + (a.velocityCount = t + 1), + (this.local[h] = a), + (c += this._render(r, h))); + }, + this + ), + (this.setBreak = !1), + (this.local[h] = {}), + this.conditions.shift(), + (this.condition = this.conditions[0] || ''), + c + ); + } + }, + getBlockIf: function(e) { + var s = !1, + i = []; + return ( + t.some( + e, + function(e) { + if (e.condition) { + if (s) return !0; + s = this.getExpression(e.condition); + } else if ('else' === e.type) { + if (s) return !0; + s = !0; + } else s && i.push(e); + return !1; + }, + this + ), + this._render(i) + ); + }, + }); + }; + }), + define('velocityjs/0.4.10/src/compile/literal', [], function(e, t, s) { + s.exports = function(e, t) { + t.mixin(e.prototype, { + getLiteral: function(e) { + var s = e.type, + i = ''; + if ('string' == s) i = this.getString(e); + else if ('integer' == s) i = parseInt(e.value, 10); + else if ('decimal' == s) i = parseFloat(e.value, 10); + else if ('array' == s) i = this.getArray(e); + else if ('map' == s) { + i = {}; + var r = e.value; + t.forEach( + r, + function(e, t) { + i[t] = this.getLiteral(e); + }, + this + ); + } else { + if ('bool' != s) return this.getReferences(e); + 'null' === e.value ? (i = null) : 'false' === e.value ? (i = !1) : 'true' === e.value && (i = !0); + } + return i; + }, + getString: function(e) { + var t = e.value, + s = t; + return !e.isEval || (-1 === t.indexOf('#') && -1 === t.indexOf('$')) || (s = this.evalStr(t)), s; + }, + getArray: function(e) { + var s = []; + if (e.isRange) { + var i = e.value[0]; + 'references' === i.type && (i = this.getReferences(i)); + var r = e.value[1]; + 'references' === r.type && (r = this.getReferences(r)), (r = parseInt(r, 10)), (i = parseInt(i, 10)); + var n; + if (!isNaN(i) && !isNaN(r)) + if (r > i) for (n = i; r >= n; n++) s.push(n); + else for (n = i; n >= r; n--) s.push(n); + } else + t.forEach( + e.value, + function(e) { + s.push(this.getLiteral(e)); + }, + this + ); + return s; + }, + evalStr: function(t) { + var s = e.Parser.parse(t); + return this._render(s); + }, + }); + }; + }), + define('velocityjs/0.4.10/src/compile/references', [], function(e, t, s) { + s.exports = function(e, t) { + 'use strict'; + function s(e) { + return t.isArray(e) ? e.length : t.isObject(e) ? t.keys(e).length : void 0; + } + function i(e) { + if ('string' != typeof e) return e; + var t, + s, + i, + r = '', + n = !1; + for (t = 0; t < e.length; t++) + (s = e.charAt(t)), + (s >= ' ' && '~' >= s) || '\r' == s || '\n' == s + ? '&' == s + ? ((i = '&'), (n = !0)) + : '<' == s + ? ((i = '<'), (n = !0)) + : '>' == s + ? ((i = '>'), (n = !0)) + : (i = s.toString()) + : (i = '&#' + s.charCodeAt().toString() + ';'), + (r += i); + return n ? r : e; + } + t.mixin(e.prototype, { + addIgnoreEscpape: function(e) { + t.isArray(e) || (e = [e]), + t.forEach( + e, + function(e) { + this.config.unescape[e] = !0; + }, + this + ); + }, + getReferences: function(s, r) { + if (s.prue) { + var n = this.defines[s.id]; + if (t.isArray(n)) return this._render(n); + s.id in this.config.unescape && (s.prue = !1); + } + var a = this.config.escape, + c = this.silence || '$!' === s.leader, + o = void 0 !== s.args, + h = this.context, + l = h[s.id], + u = this.getLocal(s), + p = e.Helper.getRefText(s); + return p in h + ? s.prue && a + ? i(h[p]) + : h[p] + : (void 0 !== l && o && (l = this.getPropMethod(s, h, s)), + u.isLocaled && (l = u.value), + s.path && + void 0 !== l && + t.some( + s.path, + function(e) { + l = this.getAttributes(e, l, s); + }, + this + ), + r && void 0 === l && (l = c ? '' : e.Helper.getRefText(s)), + (l = s.prue && a ? i(l) : l)); + }, + getLocal: function(e) { + var s = e.id, + i = this.local, + r = !1, + n = t.some( + this.conditions, + function(e) { + var t = i[e]; + return s in t ? ((r = t[s]), !0) : !1; + }, + this + ); + return { value: r, isLocaled: n }; + }, + getAttributes: function(e, t, s) { + var i, + r = e.type, + n = e.id; + return (i = 'method' === r ? this.getPropMethod(e, t, s) : 'property' === r ? t[n] : this.getPropIndex(e, t)); + }, + getPropIndex: function(e, t) { + var s, + i = e.id; + return (s = 'references' === i.type ? this.getReferences(i) : 'integer' === i.type ? i.value : i.value), t[s]; + }, + getPropMethod: function(i, r, n) { + var a = i.id, + c = '', + o = a.slice(3); + if (!(0 !== a.indexOf('get') || a in r)) return o ? (c = r[o]) : ((o = this.getLiteral(i.args[0])), (c = r[o])), c; + if (0 === a.indexOf('set') && !r[a]) + return ( + (r[o] = this.getLiteral(i.args[0])), + (r.toString = function() { + return ''; + }), + r + ); + if (!(0 !== a.indexOf('is') || a in r)) return (o = a.slice(2)), (c = r[o]); + if ('keySet' === a) return t.keys(r); + if ('entrySet' === a) + return ( + (c = []), + t.forEach(r, function(e, t) { + c.push({ key: t, value: e }); + }), + c + ); + if ('size' === a) return s(r); + c = r[a]; + var h = []; + if ( + (t.forEach( + i.args, + function(e) { + h.push(this.getLiteral(e)); + }, + this + ), + c && c.call) + ) { + var l = this; + r.eval = function() { + return l.eval.apply(l, arguments); + }; + try { + c = c.apply(r, h); + } catch (u) { + var p = n.pos, + y = e.Helper.getRefText(n), + f = ' on ' + y + ' at L/N ' + p.first_line + ':' + p.first_column; + throw ((u.name = ''), (u.message += f), new Error(u)); + } + } else c = void 0; + return c; + }, + }); + }; + }), + define('velocityjs/0.4.10/src/compile/set', [], function(e, t, s) { + s.exports = function(e, t) { + t.mixin(e.prototype, { + getContext: function() { + var e = this.condition, + t = this.local; + return e ? t[e] : this.context; + }, + setValue: function(e) { + var s = e.equal[0], + i = this.getContext(); + this.condition && 0 === this.condition.indexOf('macro:') ? (i = this.context) : null != this.context[s.id] && (i = this.context); + var r, + n = e.equal[1]; + if (((r = 'math' === n.type ? this.getExpression(n) : this.getLiteral(e.equal[1])), s.path)) { + var a = i[s.id]; + 'object' != typeof a && (a = {}), (i[s.id] = a); + var c = s.path ? s.path.length : 0; + t.forEach(s.path, function(e, t) { + var s = c === t + 1, + i = e.id; + 'index' === e.type && (i = i.value), (a[i] = s ? r : {}), (a = a[i]); + }); + } else i[s.id] = r; + }, + }); + }; + }), + define('velocityjs/0.4.10/src/compile/expression', [], function(e, t, s) { + s.exports = function(e, t) { + t.mixin(e.prototype, { + getExpression: function(e) { + var t, + s = e.expression; + if ('math' === e.type) { + switch (e.operator) { + case '+': + t = this.getExpression(s[0]) + this.getExpression(s[1]); + break; + case '-': + t = this.getExpression(s[0]) - this.getExpression(s[1]); + break; + case '/': + t = this.getExpression(s[0]) / this.getExpression(s[1]); + break; + case '%': + t = this.getExpression(s[0]) % this.getExpression(s[1]); + break; + case '*': + t = this.getExpression(s[0]) * this.getExpression(s[1]); + break; + case '||': + t = this.getExpression(s[0]) || this.getExpression(s[1]); + break; + case '&&': + t = this.getExpression(s[0]) && this.getExpression(s[1]); + break; + case '>': + t = this.getExpression(s[0]) > this.getExpression(s[1]); + break; + case '<': + t = this.getExpression(s[0]) < this.getExpression(s[1]); + break; + case '==': + t = this.getExpression(s[0]) == this.getExpression(s[1]); + break; + case '>=': + t = this.getExpression(s[0]) >= this.getExpression(s[1]); + break; + case '<=': + t = this.getExpression(s[0]) <= this.getExpression(s[1]); + break; + case '!=': + t = this.getExpression(s[0]) != this.getExpression(s[1]); + break; + case 'minus': + t = -this.getExpression(s[0]); + break; + case 'not': + t = !this.getExpression(s[0]); + break; + case 'parenthesis': + t = this.getExpression(s[0]); + break; + default: + return; + } + return t; + } + return this.getLiteral(e); + }, + }); + }; + }), + define('velocityjs/0.4.10/src/compile/compile', [], function(e, t, s) { + s.exports = function(e, t) { + t.mixin(e.prototype, { + init: function() { + (this.context = {}), + (this.macros = {}), + (this.defines = {}), + (this.conditions = []), + (this.local = {}), + (this.silence = !1), + (this.unescape = {}); + }, + render: function(e, s, i) { + (this.silence = !!i), (this.context = e || {}), (this.jsmacros = s || {}); + var r = t.now(), + n = this._render(), + a = t.now(), + c = a - r; + return (this.cost = c), n; + }, + _render: function(e, s) { + var i = ''; + return ( + (e = e || this.asts), + s + ? (s !== this.condition && -1 === t.indexOf(s, this.conditions) && this.conditions.unshift(s), (this.condition = s)) + : (this.condition = null), + t.forEach( + e, + function(e) { + switch (e.type) { + case 'references': + i += this.getReferences(e, !0); + break; + case 'set': + this.setValue(e); + break; + case 'break': + this.setBreak = !0; + break; + case 'macro_call': + i += this.getMacro(e); + break; + case 'comment': + break; + default: + i += 'string' == typeof e ? e : this.getBlock(e); + } + }, + this + ), + i + ); + }, + }); + }; + }); diff --git a/packages/amplify-velocity-template/tests/runner/mocha.js b/packages/amplify-velocity-template/tests/runner/mocha.js index de329bdbea..5584fc6fba 100644 --- a/packages/amplify-velocity-template/tests/runner/mocha.js +++ b/packages/amplify-velocity-template/tests/runner/mocha.js @@ -1,11 +1,9 @@ -;(function(){ +(function() { + // CommonJS require() - -// CommonJS require() - -function require(p){ - var path = require.resolve(p) - , mod = require.modules[path]; + function require(p) { + var path = require.resolve(p), + mod = require.modules[path]; if (!mod) throw new Error('failed to require "' + p + '"'); if (!mod.exports) { mod.exports = {}; @@ -14,29 +12,27 @@ function require(p){ return mod.exports; } -require.modules = {}; + require.modules = {}; -require.resolve = function (path){ - var orig = path - , reg = path + '.js' - , index = path + '/index.js'; - return require.modules[reg] && reg - || require.modules[index] && index - || orig; + require.resolve = function(path) { + var orig = path, + reg = path + '.js', + index = path + '/index.js'; + return (require.modules[reg] && reg) || (require.modules[index] && index) || orig; }; -require.register = function (path, fn){ + require.register = function(path, fn) { require.modules[path] = fn; }; -require.relative = function (parent) { - return function(p){ + require.relative = function(parent) { + return function(p) { if ('.' != p.charAt(0)) return require(p); - - var path = parent.split('/') - , segs = p.split('/'); + + var path = parent.split('/'), + segs = p.split('/'); path.pop(); - + for (var i = 0; i < segs.length; i++) { var seg = segs[i]; if ('..' == seg) path.pop(); @@ -47,4953 +43,4865 @@ require.relative = function (parent) { }; }; + require.register('browser/debug.js', function(module, exports, require) { + module.exports = function(type) { + return function() {}; + }; + }); // module: browser/debug.js -require.register("browser/debug.js", function(module, exports, require){ - -module.exports = function(type){ - return function(){ - - } -}; -}); // module: browser/debug.js + require.register('browser/diff.js', function(module, exports, require) {}); // module: browser/diff.js -require.register("browser/diff.js", function(module, exports, require){ + require.register('browser/events.js', function(module, exports, require) { + /** + * Module exports. + */ -}); // module: browser/diff.js + exports.EventEmitter = EventEmitter; -require.register("browser/events.js", function(module, exports, require){ + /** + * Check if `obj` is an array. + */ -/** - * Module exports. - */ + function isArray(obj) { + return '[object Array]' == {}.toString.call(obj); + } -exports.EventEmitter = EventEmitter; + /** + * Event emitter constructor. + * + * @api public + */ -/** - * Check if `obj` is an array. - */ + function EventEmitter() {} -function isArray(obj) { - return '[object Array]' == {}.toString.call(obj); -} + /** + * Adds a listener. + * + * @api public + */ -/** - * Event emitter constructor. - * - * @api public - */ + EventEmitter.prototype.on = function(name, fn) { + if (!this.$events) { + this.$events = {}; + } -function EventEmitter(){}; + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } -/** - * Adds a listener. - * - * @api public - */ + return this; + }; -EventEmitter.prototype.on = function (name, fn) { - if (!this.$events) { - this.$events = {}; - } + EventEmitter.prototype.addListener = EventEmitter.prototype.on; - if (!this.$events[name]) { - this.$events[name] = fn; - } else if (isArray(this.$events[name])) { - this.$events[name].push(fn); - } else { - this.$events[name] = [this.$events[name], fn]; - } + /** + * Adds a volatile listener. + * + * @api public + */ - return this; -}; + EventEmitter.prototype.once = function(name, fn) { + var self = this; -EventEmitter.prototype.addListener = EventEmitter.prototype.on; + function on() { + self.removeListener(name, on); + fn.apply(this, arguments); + } -/** - * Adds a volatile listener. - * - * @api public - */ + on.listener = fn; + this.on(name, on); -EventEmitter.prototype.once = function (name, fn) { - var self = this; + return this; + }; - function on () { - self.removeListener(name, on); - fn.apply(this, arguments); - }; + /** + * Removes a listener. + * + * @api public + */ - on.listener = fn; - this.on(name, on); + EventEmitter.prototype.removeListener = function(name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; - return this; -}; + if (isArray(list)) { + var pos = -1; -/** - * Removes a listener. - * - * @api public - */ + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } -EventEmitter.prototype.removeListener = function (name, fn) { - if (this.$events && this.$events[name]) { - var list = this.$events[name]; + if (pos < 0) { + return this; + } - if (isArray(list)) { - var pos = -1; + list.splice(pos, 1); - for (var i = 0, l = list.length; i < l; i++) { - if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { - pos = i; - break; + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; } } - if (pos < 0) { - return this; - } + return this; + }; - list.splice(pos, 1); + /** + * Removes all listeners for an event. + * + * @api public + */ - if (!list.length) { - delete this.$events[name]; + EventEmitter.prototype.removeAllListeners = function(name) { + if (name === undefined) { + this.$events = {}; + return this; } - } else if (list === fn || (list.listener && list.listener === fn)) { - delete this.$events[name]; - } - } - return this; -}; + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } -/** - * Removes all listeners for an event. - * - * @api public - */ + return this; + }; -EventEmitter.prototype.removeAllListeners = function (name) { - if (name === undefined) { - this.$events = {}; - return this; - } + /** + * Gets all listeners for a certain event. + * + * @api public + */ - if (this.$events && this.$events[name]) { - this.$events[name] = null; - } + EventEmitter.prototype.listeners = function(name) { + if (!this.$events) { + this.$events = {}; + } - return this; -}; + if (!this.$events[name]) { + this.$events[name] = []; + } -/** - * Gets all listeners for a certain event. - * - * @api public - */ + if (!isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } -EventEmitter.prototype.listeners = function (name) { - if (!this.$events) { - this.$events = {}; - } + return this.$events[name]; + }; - if (!this.$events[name]) { - this.$events[name] = []; - } + /** + * Emits an event. + * + * @api public + */ - if (!isArray(this.$events[name])) { - this.$events[name] = [this.$events[name]]; - } + EventEmitter.prototype.emit = function(name) { + if (!this.$events) { + return false; + } - return this.$events[name]; -}; + var handler = this.$events[name]; -/** - * Emits an event. - * - * @api public - */ + if (!handler) { + return false; + } -EventEmitter.prototype.emit = function (name) { - if (!this.$events) { - return false; - } + var args = [].slice.call(arguments, 1); - var handler = this.$events[name]; + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (isArray(handler)) { + var listeners = handler.slice(); - if (!handler) { - return false; - } + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } - var args = [].slice.call(arguments, 1); + return true; + }; + }); // module: browser/events.js - if ('function' == typeof handler) { - handler.apply(this, args); - } else if (isArray(handler)) { - var listeners = handler.slice(); + require.register('browser/fs.js', function(module, exports, require) {}); // module: browser/fs.js - for (var i = 0, l = listeners.length; i < l; i++) { - listeners[i].apply(this, args); - } - } else { - return false; - } + require.register('browser/path.js', function(module, exports, require) {}); // module: browser/path.js - return true; -}; -}); // module: browser/events.js - -require.register("browser/fs.js", function(module, exports, require){ - -}); // module: browser/fs.js - -require.register("browser/path.js", function(module, exports, require){ - -}); // module: browser/path.js - -require.register("browser/progress.js", function(module, exports, require){ - -/** - * Expose `Progress`. - */ - -module.exports = Progress; - -/** - * Initialize a new `Progress` indicator. - */ - -function Progress() { - this.percent = 0; - this.size(0); - this.fontSize(11); - this.font('helvetica, arial, sans-serif'); -} - -/** - * Set progress size to `n`. - * - * @param {Number} n - * @return {Progress} for chaining - * @api public - */ - -Progress.prototype.size = function(n){ - this._size = n; - return this; -}; - -/** - * Set text to `str`. - * - * @param {String} str - * @return {Progress} for chaining - * @api public - */ - -Progress.prototype.text = function(str){ - this._text = str; - return this; -}; - -/** - * Set font size to `n`. - * - * @param {Number} n - * @return {Progress} for chaining - * @api public - */ - -Progress.prototype.fontSize = function(n){ - this._fontSize = n; - return this; -}; - -/** - * Set font `family`. - * - * @param {String} family - * @return {Progress} for chaining - */ - -Progress.prototype.font = function(family){ - this._font = family; - return this; -}; - -/** - * Update percentage to `n`. - * - * @param {Number} n - * @return {Progress} for chaining - */ - -Progress.prototype.update = function(n){ - this.percent = n; - return this; -}; - -/** - * Draw on `ctx`. - * - * @param {CanvasRenderingContext2d} ctx - * @return {Progress} for chaining - */ - -Progress.prototype.draw = function(ctx){ - var percent = Math.min(this.percent, 100) - , size = this._size - , half = size / 2 - , x = half - , y = half - , rad = half - 1 - , fontSize = this._fontSize; - - ctx.font = fontSize + 'px ' + this._font; - - var angle = Math.PI * 2 * (percent / 100); - ctx.clearRect(0, 0, size, size); - - // outer circle - ctx.strokeStyle = '#9f9f9f'; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, angle, false); - ctx.stroke(); - - // inner circle - ctx.strokeStyle = '#eee'; - ctx.beginPath(); - ctx.arc(x, y, rad - 1, 0, angle, true); - ctx.stroke(); - - // text - var text = this._text || (percent | 0) + '%' - , w = ctx.measureText(text).width; - - ctx.fillText( - text - , x - w / 2 + 1 - , y + fontSize / 2 - 1); - - return this; -}; - -}); // module: browser/progress.js - -require.register("browser/tty.js", function(module, exports, require){ - -exports.isatty = function(){ - return true; -}; - -exports.getWindowSize = function(){ - return [window.innerHeight, window.innerWidth]; -}; -}); // module: browser/tty.js - -require.register("context.js", function(module, exports, require){ - -/** - * Expose `Context`. - */ - -module.exports = Context; - -/** - * Initialize a new `Context`. - * - * @api private - */ - -function Context(){} - -/** - * Set or get the context `Runnable` to `runnable`. - * - * @param {Runnable} runnable - * @return {Context} - * @api private - */ - -Context.prototype.runnable = function(runnable){ - if (0 == arguments.length) return this._runnable; - this.test = this._runnable = runnable; - return this; -}; - -/** - * Set test timeout `ms`. - * - * @param {Number} ms - * @return {Context} self - * @api private - */ - -Context.prototype.timeout = function(ms){ - this.runnable().timeout(ms); - return this; -}; - -/** - * Set test slowness threshold `ms`. - * - * @param {Number} ms - * @return {Context} self - * @api private - */ - -Context.prototype.slow = function(ms){ - this.runnable().slow(ms); - return this; -}; - -/** - * Inspect the context void of `._runnable`. - * - * @return {String} - * @api private - */ - -Context.prototype.inspect = function(){ - return JSON.stringify(this, function(key, val){ - if ('_runnable' == key) return; - if ('test' == key) return; - return val; - }, 2); -}; - -}); // module: context.js - -require.register("hook.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Runnable = require('./runnable'); - -/** - * Expose `Hook`. - */ - -module.exports = Hook; - -/** - * Initialize a new `Hook` with the given `title` and callback `fn`. - * - * @param {String} title - * @param {Function} fn - * @api private - */ - -function Hook(title, fn) { - Runnable.call(this, title, fn); - this.type = 'hook'; -} - -/** - * Inherit from `Runnable.prototype`. - */ - -Hook.prototype = new Runnable; -Hook.prototype.constructor = Hook; - - -/** - * Get or set the test `err`. - * - * @param {Error} err - * @return {Error} - * @api public - */ - -Hook.prototype.error = function(err){ - if (0 == arguments.length) { - var err = this._error; - this._error = null; - return err; - } + require.register('browser/progress.js', function(module, exports, require) { + /** + * Expose `Progress`. + */ - this._error = err; -}; + module.exports = Progress; + /** + * Initialize a new `Progress` indicator. + */ -}); // module: hook.js + function Progress() { + this.percent = 0; + this.size(0); + this.fontSize(11); + this.font('helvetica, arial, sans-serif'); + } -require.register("interfaces/bdd.js", function(module, exports, require){ + /** + * Set progress size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ -/** - * Module dependencies. - */ + Progress.prototype.size = function(n) { + this._size = n; + return this; + }; -var Suite = require('../suite') - , Test = require('../test'); + /** + * Set text to `str`. + * + * @param {String} str + * @return {Progress} for chaining + * @api public + */ -/** - * BDD-style interface: - * - * describe('Array', function(){ - * describe('#indexOf()', function(){ - * it('should return -1 when not present', function(){ - * - * }); - * - * it('should return the index when present', function(){ - * - * }); - * }); - * }); - * - */ + Progress.prototype.text = function(str) { + this._text = str; + return this; + }; -module.exports = function(suite){ - var suites = [suite]; + /** + * Set font size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ - suite.on('pre-require', function(context, file, mocha){ + Progress.prototype.fontSize = function(n) { + this._fontSize = n; + return this; + }; /** - * Execute before running tests. + * Set font `family`. + * + * @param {String} family + * @return {Progress} for chaining */ - context.before = function(fn){ - suites[0].beforeAll(fn); + Progress.prototype.font = function(family) { + this._font = family; + return this; }; /** - * Execute after running tests. + * Update percentage to `n`. + * + * @param {Number} n + * @return {Progress} for chaining */ - context.after = function(fn){ - suites[0].afterAll(fn); + Progress.prototype.update = function(n) { + this.percent = n; + return this; }; /** - * Execute before each test case. + * Draw on `ctx`. + * + * @param {CanvasRenderingContext2d} ctx + * @return {Progress} for chaining */ - context.beforeEach = function(fn){ - suites[0].beforeEach(fn); + Progress.prototype.draw = function(ctx) { + var percent = Math.min(this.percent, 100), + size = this._size, + half = size / 2, + x = half, + y = half, + rad = half - 1, + fontSize = this._fontSize; + + ctx.font = fontSize + 'px ' + this._font; + + var angle = Math.PI * 2 * (percent / 100); + ctx.clearRect(0, 0, size, size); + + // outer circle + ctx.strokeStyle = '#9f9f9f'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, angle, false); + ctx.stroke(); + + // inner circle + ctx.strokeStyle = '#eee'; + ctx.beginPath(); + ctx.arc(x, y, rad - 1, 0, angle, true); + ctx.stroke(); + + // text + var text = this._text || (percent | 0) + '%', + w = ctx.measureText(text).width; + + ctx.fillText(text, x - w / 2 + 1, y + fontSize / 2 - 1); + + return this; + }; + }); // module: browser/progress.js + + require.register('browser/tty.js', function(module, exports, require) { + exports.isatty = function() { + return true; + }; + + exports.getWindowSize = function() { + return [window.innerHeight, window.innerWidth]; }; + }); // module: browser/tty.js + require.register('context.js', function(module, exports, require) { /** - * Execute after each test case. + * Expose `Context`. */ - context.afterEach = function(fn){ - suites[0].afterEach(fn); - }; + module.exports = Context; /** - * Describe a "suite" with the given `title` - * and callback `fn` containing nested suites - * and/or tests. + * Initialize a new `Context`. + * + * @api private */ - - context.describe = context.context = function(title, fn){ - var suite = Suite.create(suites[0], title); - suites.unshift(suite); - fn.call(suite); - suites.shift(); - return suite; - }; + + function Context() {} /** - * Pending describe. + * Set or get the context `Runnable` to `runnable`. + * + * @param {Runnable} runnable + * @return {Context} + * @api private */ - context.xdescribe = - context.xcontext = - context.describe.skip = function(title, fn){ - var suite = Suite.create(suites[0], title); - suite.pending = true; - suites.unshift(suite); - fn.call(suite); - suites.shift(); + Context.prototype.runnable = function(runnable) { + if (0 == arguments.length) return this._runnable; + this.test = this._runnable = runnable; + return this; }; /** - * Exclusive suite. + * Set test timeout `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private */ - context.describe.only = function(title, fn){ - var suite = context.describe(title, fn); - mocha.grep(suite.fullTitle()); + Context.prototype.timeout = function(ms) { + this.runnable().timeout(ms); + return this; }; /** - * Describe a specification or test-case - * with the given `title` and callback `fn` - * acting as a thunk. + * Set test slowness threshold `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private */ - context.it = context.specify = function(title, fn){ - var suite = suites[0]; - if (suite.pending) var fn = null; - var test = new Test(title, fn); - suite.addTest(test); - return test; + Context.prototype.slow = function(ms) { + this.runnable().slow(ms); + return this; }; /** - * Exclusive test-case. + * Inspect the context void of `._runnable`. + * + * @return {String} + * @api private */ - context.it.only = function(title, fn){ - var test = context.it(title, fn); - mocha.grep(test.fullTitle()); + Context.prototype.inspect = function() { + return JSON.stringify( + this, + function(key, val) { + if ('_runnable' == key) return; + if ('test' == key) return; + return val; + }, + 2 + ); }; + }); // module: context.js + require.register('hook.js', function(module, exports, require) { /** - * Pending test case. + * Module dependencies. */ - context.xit = - context.xspecify = - context.it.skip = function(title){ - context.it(title); - }; - }); -}; - -}); // module: interfaces/bdd.js - -require.register("interfaces/exports.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Suite = require('../suite') - , Test = require('../test'); - -/** - * TDD-style interface: - * - * exports.Array = { - * '#indexOf()': { - * 'should return -1 when the value is not present': function(){ - * - * }, - * - * 'should return the correct index when the value is present': function(){ - * - * } - * } - * }; - * - */ - -module.exports = function(suite){ - var suites = [suite]; - - suite.on('require', visit); - - function visit(obj) { - var suite; - for (var key in obj) { - if ('function' == typeof obj[key]) { - var fn = obj[key]; - switch (key) { - case 'before': - suites[0].beforeAll(fn); - break; - case 'after': - suites[0].afterAll(fn); - break; - case 'beforeEach': - suites[0].beforeEach(fn); - break; - case 'afterEach': - suites[0].afterEach(fn); - break; - default: - suites[0].addTest(new Test(key, fn)); - } - } else { - var suite = Suite.create(suites[0], key); - suites.unshift(suite); - visit(obj[key]); - suites.shift(); - } - } - } -}; -}); // module: interfaces/exports.js - -require.register("interfaces/index.js", function(module, exports, require){ - -exports.bdd = require('./bdd'); -exports.tdd = require('./tdd'); -exports.qunit = require('./qunit'); -exports.exports = require('./exports'); - -}); // module: interfaces/index.js - -require.register("interfaces/qunit.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Suite = require('../suite') - , Test = require('../test'); - -/** - * QUnit-style interface: - * - * suite('Array'); - * - * test('#length', function(){ - * var arr = [1,2,3]; - * ok(arr.length == 3); - * }); - * - * test('#indexOf()', function(){ - * var arr = [1,2,3]; - * ok(arr.indexOf(1) == 0); - * ok(arr.indexOf(2) == 1); - * ok(arr.indexOf(3) == 2); - * }); - * - * suite('String'); - * - * test('#length', function(){ - * ok('foo'.length == 3); - * }); - * - */ - -module.exports = function(suite){ - var suites = [suite]; - - suite.on('pre-require', function(context){ - - /** - * Execute before running tests. - */ - - context.before = function(fn){ - suites[0].beforeAll(fn); - }; + var Runnable = require('./runnable'); /** - * Execute after running tests. + * Expose `Hook`. */ - context.after = function(fn){ - suites[0].afterAll(fn); - }; + module.exports = Hook; /** - * Execute before each test case. + * Initialize a new `Hook` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private */ - context.beforeEach = function(fn){ - suites[0].beforeEach(fn); - }; + function Hook(title, fn) { + Runnable.call(this, title, fn); + this.type = 'hook'; + } /** - * Execute after each test case. + * Inherit from `Runnable.prototype`. */ - context.afterEach = function(fn){ - suites[0].afterEach(fn); - }; + Hook.prototype = new Runnable(); + Hook.prototype.constructor = Hook; /** - * Describe a "suite" with the given `title`. + * Get or set the test `err`. + * + * @param {Error} err + * @return {Error} + * @api public */ - - context.suite = function(title){ - if (suites.length > 1) suites.shift(); - var suite = Suite.create(suites[0], title); - suites.unshift(suite); + + Hook.prototype.error = function(err) { + if (0 == arguments.length) { + var err = this._error; + this._error = null; + return err; + } + + this._error = err; }; + }); // module: hook.js + require.register('interfaces/bdd.js', function(module, exports, require) { /** - * Describe a specification or test-case - * with the given `title` and callback `fn` - * acting as a thunk. + * Module dependencies. */ - context.test = function(title, fn){ - suites[0].addTest(new Test(title, fn)); - }; - }); -}; - -}); // module: interfaces/qunit.js - -require.register("interfaces/tdd.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Suite = require('../suite') - , Test = require('../test'); - -/** - * TDD-style interface: - * - * suite('Array', function(){ - * suite('#indexOf()', function(){ - * suiteSetup(function(){ - * - * }); - * - * test('should return -1 when not present', function(){ - * - * }); - * - * test('should return the index when present', function(){ - * - * }); - * - * suiteTeardown(function(){ - * - * }); - * }); - * }); - * - */ - -module.exports = function(suite){ - var suites = [suite]; - - suite.on('pre-require', function(context, file, mocha){ - - /** - * Execute before each test case. - */ - - context.setup = function(fn){ - suites[0].beforeEach(fn); - }; + var Suite = require('../suite'), + Test = require('../test'); /** - * Execute after each test case. + * BDD-style interface: + * + * describe('Array', function(){ + * describe('#indexOf()', function(){ + * it('should return -1 when not present', function(){ + * + * }); + * + * it('should return the index when present', function(){ + * + * }); + * }); + * }); + * */ - context.teardown = function(fn){ - suites[0].afterEach(fn); + module.exports = function(suite) { + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha) { + /** + * Execute before running tests. + */ + + context.before = function(fn) { + suites[0].beforeAll(fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(fn) { + suites[0].afterAll(fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(fn) { + suites[0].beforeEach(fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(fn) { + suites[0].afterEach(fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.describe = context.context = function(title, fn) { + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending describe. + */ + + context.xdescribe = context.xcontext = context.describe.skip = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive suite. + */ + + context.describe.only = function(title, fn) { + var suite = context.describe(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.it = context.specify = function(title, fn) { + var suite = suites[0]; + if (suite.pending) var fn = null; + var test = new Test(title, fn); + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.it.only = function(title, fn) { + var test = context.it(title, fn); + mocha.grep(test.fullTitle()); + }; + + /** + * Pending test case. + */ + + context.xit = context.xspecify = context.it.skip = function(title) { + context.it(title); + }; + }); }; + }); // module: interfaces/bdd.js + require.register('interfaces/exports.js', function(module, exports, require) { /** - * Execute before the suite. + * Module dependencies. */ - context.suiteSetup = function(fn){ - suites[0].beforeAll(fn); - }; + var Suite = require('../suite'), + Test = require('../test'); /** - * Execute after the suite. + * TDD-style interface: + * + * exports.Array = { + * '#indexOf()': { + * 'should return -1 when the value is not present': function(){ + * + * }, + * + * 'should return the correct index when the value is present': function(){ + * + * } + * } + * }; + * */ - context.suiteTeardown = function(fn){ - suites[0].afterAll(fn); + module.exports = function(suite) { + var suites = [suite]; + + suite.on('require', visit); + + function visit(obj) { + var suite; + for (var key in obj) { + if ('function' == typeof obj[key]) { + var fn = obj[key]; + switch (key) { + case 'before': + suites[0].beforeAll(fn); + break; + case 'after': + suites[0].afterAll(fn); + break; + case 'beforeEach': + suites[0].beforeEach(fn); + break; + case 'afterEach': + suites[0].afterEach(fn); + break; + default: + suites[0].addTest(new Test(key, fn)); + } + } else { + var suite = Suite.create(suites[0], key); + suites.unshift(suite); + visit(obj[key]); + suites.shift(); + } + } + } }; + }); // module: interfaces/exports.js + require.register('interfaces/index.js', function(module, exports, require) { + exports.bdd = require('./bdd'); + exports.tdd = require('./tdd'); + exports.qunit = require('./qunit'); + exports.exports = require('./exports'); + }); // module: interfaces/index.js + + require.register('interfaces/qunit.js', function(module, exports, require) { /** - * Describe a "suite" with the given `title` - * and callback `fn` containing nested suites - * and/or tests. + * Module dependencies. */ - context.suite = function(title, fn){ - var suite = Suite.create(suites[0], title); - suites.unshift(suite); - fn.call(suite); - suites.shift(); - return suite; - }; + var Suite = require('../suite'), + Test = require('../test'); /** - * Exclusive test-case. + * QUnit-style interface: + * + * suite('Array'); + * + * test('#length', function(){ + * var arr = [1,2,3]; + * ok(arr.length == 3); + * }); + * + * test('#indexOf()', function(){ + * var arr = [1,2,3]; + * ok(arr.indexOf(1) == 0); + * ok(arr.indexOf(2) == 1); + * ok(arr.indexOf(3) == 2); + * }); + * + * suite('String'); + * + * test('#length', function(){ + * ok('foo'.length == 3); + * }); + * */ - context.suite.only = function(title, fn){ - var suite = context.suite(title, fn); - mocha.grep(suite.fullTitle()); + module.exports = function(suite) { + var suites = [suite]; + + suite.on('pre-require', function(context) { + /** + * Execute before running tests. + */ + + context.before = function(fn) { + suites[0].beforeAll(fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(fn) { + suites[0].afterAll(fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(fn) { + suites[0].beforeEach(fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(fn) { + suites[0].afterEach(fn); + }; + + /** + * Describe a "suite" with the given `title`. + */ + + context.suite = function(title) { + if (suites.length > 1) suites.shift(); + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn) { + suites[0].addTest(new Test(title, fn)); + }; + }); }; + }); // module: interfaces/qunit.js + require.register('interfaces/tdd.js', function(module, exports, require) { /** - * Describe a specification or test-case - * with the given `title` and callback `fn` - * acting as a thunk. + * Module dependencies. */ - context.test = function(title, fn){ - var test = new Test(title, fn); - suites[0].addTest(test); - return test; - }; + var Suite = require('../suite'), + Test = require('../test'); /** - * Exclusive test-case. + * TDD-style interface: + * + * suite('Array', function(){ + * suite('#indexOf()', function(){ + * suiteSetup(function(){ + * + * }); + * + * test('should return -1 when not present', function(){ + * + * }); + * + * test('should return the index when present', function(){ + * + * }); + * + * suiteTeardown(function(){ + * + * }); + * }); + * }); + * */ - context.test.only = function(title, fn){ - var test = context.test(title, fn); - mocha.grep(test.fullTitle()); + module.exports = function(suite) { + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha) { + /** + * Execute before each test case. + */ + + context.setup = function(fn) { + suites[0].beforeEach(fn); + }; + + /** + * Execute after each test case. + */ + + context.teardown = function(fn) { + suites[0].afterEach(fn); + }; + + /** + * Execute before the suite. + */ + + context.suiteSetup = function(fn) { + suites[0].beforeAll(fn); + }; + + /** + * Execute after the suite. + */ + + context.suiteTeardown = function(fn) { + suites[0].afterAll(fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.suite = function(title, fn) { + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn) { + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn) { + var test = new Test(title, fn); + suites[0].addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn) { + var test = context.test(title, fn); + mocha.grep(test.fullTitle()); + }; + + /** + * Pending test case. + */ + + context.test.skip = function(title) { + context.test(title); + }; + }); }; + }); // module: interfaces/tdd.js + + require.register('mocha.js', function(module, exports, require) { + /*! + * mocha + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ /** - * Pending test case. + * Module dependencies. */ - context.test.skip = function(title){ - context.test(title); - }; - }); -}; - -}); // module: interfaces/tdd.js - -require.register("mocha.js", function(module, exports, require){ -/*! - * mocha - * Copyright(c) 2011 TJ Holowaychuk - * MIT Licensed - */ - -/** - * Module dependencies. - */ - -var path = require('browser/path') - , utils = require('./utils'); - -/** - * Expose `Mocha`. - */ - -exports = module.exports = Mocha; - -/** - * Expose internals. - */ - -exports.utils = utils; -exports.interfaces = require('./interfaces'); -exports.reporters = require('./reporters'); -exports.Runnable = require('./runnable'); -exports.Context = require('./context'); -exports.Runner = require('./runner'); -exports.Suite = require('./suite'); -exports.Hook = require('./hook'); -exports.Test = require('./test'); - -/** - * Return image `name` path. - * - * @param {String} name - * @return {String} - * @api private - */ - -function image(name) { - return __dirname + '/../images/' + name + '.png'; -} - -/** - * Setup mocha with `options`. - * - * Options: - * - * - `ui` name "bdd", "tdd", "exports" etc - * - `reporter` reporter instance, defaults to `mocha.reporters.Dot` - * - `globals` array of accepted globals - * - `timeout` timeout in milliseconds - * - `slow` milliseconds to wait before considering a test slow - * - `ignoreLeaks` ignore global leaks - * - `grep` string or regexp to filter tests with - * - * @param {Object} options - * @api public - */ - -function Mocha(options) { - options = options || {}; - this.files = []; - this.options = options; - this.grep(options.grep); - this.suite = new exports.Suite('', new exports.Context); - this.ui(options.ui); - this.reporter(options.reporter); - if (options.timeout) this.timeout(options.timeout); - if (options.slow) this.slow(options.slow); -} - -/** - * Add test `file`. - * - * @param {String} file - * @api public - */ - -Mocha.prototype.addFile = function(file){ - this.files.push(file); - return this; -}; - -/** - * Set reporter to `reporter`, defaults to "dot". - * - * @param {String|Function} reporter name of a reporter or a reporter constructor - * @api public - */ - -Mocha.prototype.reporter = function(reporter){ - if ('function' == typeof reporter) { - this._reporter = reporter; - } else { - reporter = reporter || 'dot'; - try { - this._reporter = require('./reporters/' + reporter); - } catch (err) { - this._reporter = require(reporter); - } - if (!this._reporter) throw new Error('invalid reporter "' + reporter + '"'); - } - return this; -}; - -/** - * Set test UI `name`, defaults to "bdd". - * - * @param {String} bdd - * @api public - */ - -Mocha.prototype.ui = function(name){ - name = name || 'bdd'; - this._ui = exports.interfaces[name]; - if (!this._ui) throw new Error('invalid interface "' + name + '"'); - this._ui = this._ui(this.suite); - return this; -}; - -/** - * Load registered files. - * - * @api private - */ - -Mocha.prototype.loadFiles = function(fn){ - var self = this; - var suite = this.suite; - var pending = this.files.length; - this.files.forEach(function(file){ - file = path.resolve(file); - suite.emit('pre-require', global, file, self); - suite.emit('require', require(file), file, self); - suite.emit('post-require', global, file, self); - --pending || (fn && fn()); - }); -}; - -/** - * Enable growl support. - * - * @api private - */ - -Mocha.prototype._growl = function(runner, reporter) { - var notify = require('growl'); - - runner.on('end', function(){ - var stats = reporter.stats; - if (stats.failures) { - var msg = stats.failures + ' of ' + runner.total + ' tests failed'; - notify(msg, { name: 'mocha', title: 'Failed', image: image('error') }); - } else { - notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', { - name: 'mocha' - , title: 'Passed' - , image: image('ok') - }); - } - }); -}; - -/** - * Add regexp to grep, if `re` is a string it is escaped. - * - * @param {RegExp|String} re - * @return {Mocha} - * @api public - */ - -Mocha.prototype.grep = function(re){ - this.options.grep = 'string' == typeof re - ? new RegExp(utils.escapeRegexp(re)) - : re; - return this; -}; - -/** - * Invert `.grep()` matches. - * - * @return {Mocha} - * @api public - */ - -Mocha.prototype.invert = function(){ - this.options.invert = true; - return this; -}; - -/** - * Ignore global leaks. - * - * @return {Mocha} - * @api public - */ - -Mocha.prototype.ignoreLeaks = function(){ - this.options.ignoreLeaks = true; - return this; -}; - -/** - * Enable global leak checking. - * - * @return {Mocha} - * @api public - */ - -Mocha.prototype.checkLeaks = function(){ - this.options.ignoreLeaks = false; - return this; -}; - -/** - * Enable growl support. - * - * @return {Mocha} - * @api public - */ - -Mocha.prototype.growl = function(){ - this.options.growl = true; - return this; -}; - -/** - * Ignore `globals` array or string. - * - * @param {Array|String} globals - * @return {Mocha} - * @api public - */ - -Mocha.prototype.globals = function(globals){ - this.options.globals = (this.options.globals || []).concat(globals); - return this; -}; - -/** - * Set the timeout in milliseconds. - * - * @param {Number} timeout - * @return {Mocha} - * @api public - */ - -Mocha.prototype.timeout = function(timeout){ - this.suite.timeout(timeout); - return this; -}; - -/** - * Set slowness threshold in milliseconds. - * - * @param {Number} slow - * @return {Mocha} - * @api public - */ - -Mocha.prototype.slow = function(slow){ - this.suite.slow(slow); - return this; -}; - -/** - * Makes all tests async (accepting a callback) - * - * @return {Mocha} - * @api public - */ - -Mocha.prototype.asyncOnly = function(){ - this.options.asyncOnly = true; - return this; -}; - -/** - * Run tests and invoke `fn()` when complete. - * - * @param {Function} fn - * @return {Runner} - * @api public - */ - -Mocha.prototype.run = function(fn){ - if (this.files.length) this.loadFiles(); - var suite = this.suite; - var options = this.options; - var runner = new exports.Runner(suite); - var reporter = new this._reporter(runner); - runner.ignoreLeaks = options.ignoreLeaks; - runner.asyncOnly = options.asyncOnly; - if (options.grep) runner.grep(options.grep, options.invert); - if (options.globals) runner.globals(options.globals); - if (options.growl) this._growl(runner, reporter); - return runner.run(fn); -}; - -}); // module: mocha.js - -require.register("ms.js", function(module, exports, require){ - -/** - * Helpers. - */ - -var s = 1000; -var m = s * 60; -var h = m * 60; -var d = h * 24; - -/** - * Parse or format the given `val`. - * - * @param {String|Number} val - * @return {String|Number} - * @api public - */ - -module.exports = function(val){ - if ('string' == typeof val) return parse(val); - return format(val); -} - -/** - * Parse the given `str` and return milliseconds. - * - * @param {String} str - * @return {Number} - * @api private - */ - -function parse(str) { - var m = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str); - if (!m) return; - var n = parseFloat(m[1]); - var type = (m[2] || 'ms').toLowerCase(); - switch (type) { - case 'years': - case 'year': - case 'y': - return n * 31557600000; - case 'days': - case 'day': - case 'd': - return n * 86400000; - case 'hours': - case 'hour': - case 'h': - return n * 3600000; - case 'minutes': - case 'minute': - case 'm': - return n * 60000; - case 'seconds': - case 'second': - case 's': - return n * 1000; - case 'ms': - return n; - } -} - -/** - * Format the given `ms`. - * - * @param {Number} ms - * @return {String} - * @api public - */ - -function format(ms) { - if (ms == d) return Math.round(ms / d) + ' day'; - if (ms > d) return Math.round(ms / d) + ' days'; - if (ms == h) return Math.round(ms / h) + ' hour'; - if (ms > h) return Math.round(ms / h) + ' hours'; - if (ms == m) return Math.round(ms / m) + ' minute'; - if (ms > m) return Math.round(ms / m) + ' minutes'; - if (ms == s) return Math.round(ms / s) + ' second'; - if (ms > s) return Math.round(ms / s) + ' seconds'; - return ms + ' ms'; -} -}); // module: ms.js - -require.register("reporters/base.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var tty = require('browser/tty') - , diff = require('browser/diff') - , ms = require('../ms'); - -/** - * Save timer references to avoid Sinon interfering (see GH-237). - */ - -var Date = global.Date - , setTimeout = global.setTimeout - , setInterval = global.setInterval - , clearTimeout = global.clearTimeout - , clearInterval = global.clearInterval; - -/** - * Check if both stdio streams are associated with a tty. - */ - -var isatty = tty.isatty(1) && tty.isatty(2); - -/** - * Expose `Base`. - */ - -exports = module.exports = Base; - -/** - * Enable coloring by default. - */ - -exports.useColors = isatty; - -/** - * Default color map. - */ - -exports.colors = { - 'pass': 90 - , 'fail': 31 - , 'bright pass': 92 - , 'bright fail': 91 - , 'bright yellow': 93 - , 'pending': 36 - , 'suite': 0 - , 'error title': 0 - , 'error message': 31 - , 'error stack': 90 - , 'checkmark': 32 - , 'fast': 90 - , 'medium': 33 - , 'slow': 31 - , 'green': 32 - , 'light': 90 - , 'diff gutter': 90 - , 'diff added': 42 - , 'diff removed': 41 -}; - -/** - * Default symbol map. - */ - -exports.symbols = { - ok: '✓', - err: '✖', - dot: '․' -}; - -// With node.js on Windows: use symbols available in terminal default fonts -if ('win32' == process.platform) { - exports.symbols.ok = '\u221A'; - exports.symbols.err = '\u00D7'; - exports.symbols.dot = '.'; -} - -/** - * Color `str` with the given `type`, - * allowing colors to be disabled, - * as well as user-defined color - * schemes. - * - * @param {String} type - * @param {String} str - * @return {String} - * @api private - */ - -var color = exports.color = function(type, str) { - if (!exports.useColors) return str; - return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; -}; - -/** - * Expose term window size, with some - * defaults for when stderr is not a tty. - */ - -exports.window = { - width: isatty - ? process.stdout.getWindowSize - ? process.stdout.getWindowSize(1)[0] - : tty.getWindowSize()[1] - : 75 -}; - -/** - * Expose some basic cursor interactions - * that are common among reporters. - */ - -exports.cursor = { - hide: function(){ - process.stdout.write('\u001b[?25l'); - }, - - show: function(){ - process.stdout.write('\u001b[?25h'); - }, - - deleteLine: function(){ - process.stdout.write('\u001b[2K'); - }, - - beginningOfLine: function(){ - process.stdout.write('\u001b[0G'); - }, - - CR: function(){ - exports.cursor.deleteLine(); - exports.cursor.beginningOfLine(); - } -}; - -/** - * Outut the given `failures` as a list. - * - * @param {Array} failures - * @api public - */ - -exports.list = function(failures){ - console.error(); - failures.forEach(function(test, i){ - // format - var fmt = color('error title', ' %s) %s:\n') - + color('error message', ' %s') - + color('error stack', '\n%s\n'); - - // msg - var err = test.err - , message = err.message || '' - , stack = err.stack || message - , index = stack.indexOf(message) + message.length - , msg = stack.slice(0, index) - , actual = err.actual - , expected = err.expected - , escape = true; - - // explicitly show diff - if (err.showDiff) { - escape = false; - err.actual = actual = JSON.stringify(actual, null, 2); - err.expected = expected = JSON.stringify(expected, null, 2); - } + var path = require('browser/path'), + utils = require('./utils'); - // actual / expected diff - if ('string' == typeof actual && 'string' == typeof expected) { - var len = Math.max(actual.length, expected.length); + /** + * Expose `Mocha`. + */ - if (len < 20) msg = errorDiff(err, 'Chars', escape); - else msg = errorDiff(err, 'Words', escape); + exports = module.exports = Mocha; - // linenos - var lines = msg.split('\n'); - if (lines.length > 4) { - var width = String(lines.length).length; - msg = lines.map(function(str, i){ - return pad(++i, width) + ' |' + ' ' + str; - }).join('\n'); - } + /** + * Expose internals. + */ - // legend - msg = '\n' - + color('diff removed', 'actual') - + ' ' - + color('diff added', 'expected') - + '\n\n' - + msg - + '\n'; + exports.utils = utils; + exports.interfaces = require('./interfaces'); + exports.reporters = require('./reporters'); + exports.Runnable = require('./runnable'); + exports.Context = require('./context'); + exports.Runner = require('./runner'); + exports.Suite = require('./suite'); + exports.Hook = require('./hook'); + exports.Test = require('./test'); - // indent - msg = msg.replace(/^/gm, ' '); + /** + * Return image `name` path. + * + * @param {String} name + * @return {String} + * @api private + */ - fmt = color('error title', ' %s) %s:\n%s') - + color('error stack', '\n%s\n'); + function image(name) { + return __dirname + '/../images/' + name + '.png'; } - // indent stack trace without msg - stack = stack.slice(index ? index + 1 : index) - .replace(/^/gm, ' '); - - console.error(fmt, (i + 1), test.fullTitle(), msg, stack); - }); -}; - -/** - * Initialize a new `Base` reporter. - * - * All other reporters generally - * inherit from this reporter, providing - * stats such as test duration, number - * of tests passed / failed etc. - * - * @param {Runner} runner - * @api public - */ - -function Base(runner) { - var self = this - , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } - , failures = this.failures = []; - - if (!runner) return; - this.runner = runner; - - runner.stats = stats; - - runner.on('start', function(){ - stats.start = new Date; - }); - - runner.on('suite', function(suite){ - stats.suites = stats.suites || 0; - suite.root || stats.suites++; - }); - - runner.on('test end', function(test){ - stats.tests = stats.tests || 0; - stats.tests++; - }); - - runner.on('pass', function(test){ - stats.passes = stats.passes || 0; - - var medium = test.slow() / 2; - test.speed = test.duration > test.slow() - ? 'slow' - : test.duration > medium - ? 'medium' - : 'fast'; - - stats.passes++; - }); - - runner.on('fail', function(test, err){ - stats.failures = stats.failures || 0; - stats.failures++; - test.err = err; - failures.push(test); - }); - - runner.on('end', function(){ - stats.end = new Date; - stats.duration = new Date - stats.start; - }); - - runner.on('pending', function(){ - stats.pending++; - }); -} - -/** - * Output common epilogue used by many of - * the bundled reporters. - * - * @api public - */ - -Base.prototype.epilogue = function(){ - var stats = this.stats - , fmt - , tests; - - console.log(); - - function pluralize(n) { - return 1 == n ? 'test' : 'tests'; - } + /** + * Setup mocha with `options`. + * + * Options: + * + * - `ui` name "bdd", "tdd", "exports" etc + * - `reporter` reporter instance, defaults to `mocha.reporters.Dot` + * - `globals` array of accepted globals + * - `timeout` timeout in milliseconds + * - `slow` milliseconds to wait before considering a test slow + * - `ignoreLeaks` ignore global leaks + * - `grep` string or regexp to filter tests with + * + * @param {Object} options + * @api public + */ - // failure - if (stats.failures) { - fmt = color('bright fail', ' ' + exports.symbols.err) - + color('fail', ' %d of %d %s failed') - + color('light', ':') + function Mocha(options) { + options = options || {}; + this.files = []; + this.options = options; + this.grep(options.grep); + this.suite = new exports.Suite('', new exports.Context()); + this.ui(options.ui); + this.reporter(options.reporter); + if (options.timeout) this.timeout(options.timeout); + if (options.slow) this.slow(options.slow); + } - console.error(fmt, - stats.failures, - this.runner.total, - pluralize(this.runner.total)); + /** + * Add test `file`. + * + * @param {String} file + * @api public + */ - Base.list(this.failures); - console.error(); - return; - } + Mocha.prototype.addFile = function(file) { + this.files.push(file); + return this; + }; - // pass - fmt = color('bright pass', ' ') - + color('green', ' %d %s complete') - + color('light', ' (%s)'); + /** + * Set reporter to `reporter`, defaults to "dot". + * + * @param {String|Function} reporter name of a reporter or a reporter constructor + * @api public + */ - console.log(fmt, - stats.tests || 0, - pluralize(stats.tests), - ms(stats.duration)); + Mocha.prototype.reporter = function(reporter) { + if ('function' == typeof reporter) { + this._reporter = reporter; + } else { + reporter = reporter || 'dot'; + try { + this._reporter = require('./reporters/' + reporter); + } catch (err) { + this._reporter = require(reporter); + } + if (!this._reporter) throw new Error('invalid reporter "' + reporter + '"'); + } + return this; + }; - // pending - if (stats.pending) { - fmt = color('pending', ' ') - + color('pending', ' %d %s pending'); + /** + * Set test UI `name`, defaults to "bdd". + * + * @param {String} bdd + * @api public + */ - console.log(fmt, stats.pending, pluralize(stats.pending)); - } + Mocha.prototype.ui = function(name) { + name = name || 'bdd'; + this._ui = exports.interfaces[name]; + if (!this._ui) throw new Error('invalid interface "' + name + '"'); + this._ui = this._ui(this.suite); + return this; + }; - console.log(); -}; - -/** - * Pad the given `str` to `len`. - * - * @param {String} str - * @param {String} len - * @return {String} - * @api private - */ - -function pad(str, len) { - str = String(str); - return Array(len - str.length + 1).join(' ') + str; -} - -/** - * Return a character diff for `err`. - * - * @param {Error} err - * @return {String} - * @api private - */ - -function errorDiff(err, type, escape) { - return diff['diff' + type](err.actual, err.expected).map(function(str){ - if (escape) { - str.value = str.value - .replace(/\t/g, '') - .replace(/\r/g, '') - .replace(/\n/g, '\n'); - } - if (str.added) return colorLines('diff added', str.value); - if (str.removed) return colorLines('diff removed', str.value); - return str.value; - }).join(''); -} - -/** - * Color lines for `str`, using the color `name`. - * - * @param {String} name - * @param {String} str - * @return {String} - * @api private - */ - -function colorLines(name, str) { - return str.split('\n').map(function(str){ - return color(name, str); - }).join('\n'); -} - -}); // module: reporters/base.js - -require.register("reporters/doc.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , utils = require('../utils'); - -/** - * Expose `Doc`. - */ - -exports = module.exports = Doc; - -/** - * Initialize a new `Doc` reporter. - * - * @param {Runner} runner - * @api public - */ - -function Doc(runner) { - Base.call(this, runner); - - var self = this - , stats = this.stats - , total = runner.total - , indents = 2; - - function indent() { - return Array(indents).join(' '); - } + /** + * Load registered files. + * + * @api private + */ - runner.on('suite', function(suite){ - if (suite.root) return; - ++indents; - console.log('%s
    ', indent()); - ++indents; - console.log('%s

    %s

    ', indent(), utils.escape(suite.title)); - console.log('%s
    ', indent()); - }); - - runner.on('suite end', function(suite){ - if (suite.root) return; - console.log('%s
    ', indent()); - --indents; - console.log('%s
    ', indent()); - --indents; - }); - - runner.on('pass', function(test){ - console.log('%s
    %s
    ', indent(), utils.escape(test.title)); - var code = utils.escape(utils.clean(test.fn.toString())); - console.log('%s
    %s
    ', indent(), code); - }); -} - -}); // module: reporters/doc.js - -require.register("reporters/dot.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , color = Base.color; - -/** - * Expose `Dot`. - */ - -exports = module.exports = Dot; - -/** - * Initialize a new `Dot` matrix test reporter. - * - * @param {Runner} runner - * @api public - */ - -function Dot(runner) { - Base.call(this, runner); - - var self = this - , stats = this.stats - , width = Base.window.width * .75 | 0 - , n = 0; - - runner.on('start', function(){ - process.stdout.write('\n '); - }); - - runner.on('pending', function(test){ - process.stdout.write(color('pending', Base.symbols.dot)); - }); - - runner.on('pass', function(test){ - if (++n % width == 0) process.stdout.write('\n '); - if ('slow' == test.speed) { - process.stdout.write(color('bright yellow', Base.symbols.dot)); - } else { - process.stdout.write(color(test.speed, Base.symbols.dot)); - } - }); - - runner.on('fail', function(test, err){ - if (++n % width == 0) process.stdout.write('\n '); - process.stdout.write(color('fail', Base.symbols.dot)); - }); - - runner.on('end', function(){ - console.log(); - self.epilogue(); - }); -} - -/** - * Inherit from `Base.prototype`. - */ - -Dot.prototype = new Base; -Dot.prototype.constructor = Dot; - -}); // module: reporters/dot.js - -require.register("reporters/html-cov.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var JSONCov = require('./json-cov') - , fs = require('browser/fs'); - -/** - * Expose `HTMLCov`. - */ - -exports = module.exports = HTMLCov; - -/** - * Initialize a new `JsCoverage` reporter. - * - * @param {Runner} runner - * @api public - */ - -function HTMLCov(runner) { - var jade = require('jade') - , file = __dirname + '/templates/coverage.jade' - , str = fs.readFileSync(file, 'utf8') - , fn = jade.compile(str, { filename: file }) - , self = this; - - JSONCov.call(this, runner, false); - - runner.on('end', function(){ - process.stdout.write(fn({ - cov: self.cov - , coverageClass: coverageClass - })); - }); -} - -/** - * Return coverage class for `n`. - * - * @return {String} - * @api private - */ - -function coverageClass(n) { - if (n >= 75) return 'high'; - if (n >= 50) return 'medium'; - if (n >= 25) return 'low'; - return 'terrible'; -} -}); // module: reporters/html-cov.js - -require.register("reporters/html.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , utils = require('../utils') - , Progress = require('../browser/progress') - , escape = utils.escape; - -/** - * Save timer references to avoid Sinon interfering (see GH-237). - */ - -var Date = global.Date - , setTimeout = global.setTimeout - , setInterval = global.setInterval - , clearTimeout = global.clearTimeout - , clearInterval = global.clearInterval; - -/** - * Expose `Doc`. - */ - -exports = module.exports = HTML; - -/** - * Stats template. - */ - -var statsTemplate = ''; - -/** - * Initialize a new `Doc` reporter. - * - * @param {Runner} runner - * @api public - */ - -function HTML(runner, root) { - Base.call(this, runner); - - var self = this - , stats = this.stats - , total = runner.total - , stat = fragment(statsTemplate) - , items = stat.getElementsByTagName('li') - , passes = items[1].getElementsByTagName('em')[0] - , passesLink = items[1].getElementsByTagName('a')[0] - , failures = items[2].getElementsByTagName('em')[0] - , failuresLink = items[2].getElementsByTagName('a')[0] - , duration = items[3].getElementsByTagName('em')[0] - , canvas = stat.getElementsByTagName('canvas')[0] - , report = fragment('
      ') - , stack = [report] - , progress - , ctx - - root = root || document.getElementById('mocha'); - - if (canvas.getContext) { - var ratio = window.devicePixelRatio || 1; - canvas.style.width = canvas.width; - canvas.style.height = canvas.height; - canvas.width *= ratio; - canvas.height *= ratio; - ctx = canvas.getContext('2d'); - ctx.scale(ratio, ratio); - progress = new Progress; - } + Mocha.prototype.loadFiles = function(fn) { + var self = this; + var suite = this.suite; + var pending = this.files.length; + this.files.forEach(function(file) { + file = path.resolve(file); + suite.emit('pre-require', global, file, self); + suite.emit('require', require(file), file, self); + suite.emit('post-require', global, file, self); + --pending || (fn && fn()); + }); + }; - if (!root) return error('#mocha div missing, add it to your document'); - - // pass toggle - on(passesLink, 'click', function(){ - unhide(); - var name = /pass/.test(report.className) ? '' : ' pass'; - report.className = report.className.replace(/fail|pass/g, '') + name; - if (report.className.trim()) hideSuitesWithout('test pass'); - }); - - // failure toggle - on(failuresLink, 'click', function(){ - unhide(); - var name = /fail/.test(report.className) ? '' : ' fail'; - report.className = report.className.replace(/fail|pass/g, '') + name; - if (report.className.trim()) hideSuitesWithout('test fail'); - }); - - root.appendChild(stat); - root.appendChild(report); - - if (progress) progress.size(40); - - runner.on('suite', function(suite){ - if (suite.root) return; - - // suite - var url = '?grep=' + encodeURIComponent(suite.fullTitle()); - var el = fragment('
    • %s

    • ', url, escape(suite.title)); - - // container - stack[0].appendChild(el); - stack.unshift(document.createElement('ul')); - el.appendChild(stack[0]); - }); - - runner.on('suite end', function(suite){ - if (suite.root) return; - stack.shift(); - }); - - runner.on('fail', function(test, err){ - if ('hook' == test.type) runner.emit('test end', test); - }); - - runner.on('test end', function(test){ - window.scrollTo(0, document.body.scrollHeight); - - // TODO: add to stats - var percent = stats.tests / this.total * 100 | 0; - if (progress) progress.update(percent).draw(ctx); - - // update stats - var ms = new Date - stats.start; - text(passes, stats.passes); - text(failures, stats.failures); - text(duration, (ms / 1000).toFixed(2)); - - // test - if ('passed' == test.state) { - var el = fragment('
    • %e%ems

    • ', test.speed, test.title, test.duration, encodeURIComponent(test.fullTitle())); - } else if (test.pending) { - var el = fragment('
    • %e

    • ', test.title); - } else { - var el = fragment('
    • %e

    • ', test.title, encodeURIComponent(test.fullTitle())); - var str = test.err.stack || test.err.toString(); - - // FF / Opera do not add the message - if (!~str.indexOf(test.err.message)) { - str = test.err.message + '\n' + str; - } + /** + * Enable growl support. + * + * @api private + */ - // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we - // check for the result of the stringifying. - if ('[object Error]' == str) str = test.err.message; + Mocha.prototype._growl = function(runner, reporter) { + var notify = require('growl'); + + runner.on('end', function() { + var stats = reporter.stats; + if (stats.failures) { + var msg = stats.failures + ' of ' + runner.total + ' tests failed'; + notify(msg, { name: 'mocha', title: 'Failed', image: image('error') }); + } else { + notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', { + name: 'mocha', + title: 'Passed', + image: image('ok'), + }); + } + }); + }; - // Safari doesn't give you a stack. Let's at least provide a source line. - if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) { - str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")"; - } + /** + * Add regexp to grep, if `re` is a string it is escaped. + * + * @param {RegExp|String} re + * @return {Mocha} + * @api public + */ - el.appendChild(fragment('
      %e
      ', str)); - } + Mocha.prototype.grep = function(re) { + this.options.grep = 'string' == typeof re ? new RegExp(utils.escapeRegexp(re)) : re; + return this; + }; - // toggle code - // TODO: defer - if (!test.pending) { - var h2 = el.getElementsByTagName('h2')[0]; + /** + * Invert `.grep()` matches. + * + * @return {Mocha} + * @api public + */ - on(h2, 'click', function(){ - pre.style.display = 'none' == pre.style.display - ? 'inline-block' - : 'none'; - }); + Mocha.prototype.invert = function() { + this.options.invert = true; + return this; + }; - var pre = fragment('
      %e
      ', utils.clean(test.fn.toString())); - el.appendChild(pre); - pre.style.display = 'none'; - } + /** + * Ignore global leaks. + * + * @return {Mocha} + * @api public + */ - // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. - if (stack[0]) stack[0].appendChild(el); - }); -} + Mocha.prototype.ignoreLeaks = function() { + this.options.ignoreLeaks = true; + return this; + }; -/** - * Display error `msg`. - */ + /** + * Enable global leak checking. + * + * @return {Mocha} + * @api public + */ -function error(msg) { - document.body.appendChild(fragment('
      %s
      ', msg)); -} + Mocha.prototype.checkLeaks = function() { + this.options.ignoreLeaks = false; + return this; + }; -/** - * Return a DOM fragment from `html`. - */ + /** + * Enable growl support. + * + * @return {Mocha} + * @api public + */ -function fragment(html) { - var args = arguments - , div = document.createElement('div') - , i = 1; + Mocha.prototype.growl = function() { + this.options.growl = true; + return this; + }; - div.innerHTML = html.replace(/%([se])/g, function(_, type){ - switch (type) { - case 's': return String(args[i++]); - case 'e': return escape(args[i++]); - } - }); + /** + * Ignore `globals` array or string. + * + * @param {Array|String} globals + * @return {Mocha} + * @api public + */ - return div.firstChild; -} + Mocha.prototype.globals = function(globals) { + this.options.globals = (this.options.globals || []).concat(globals); + return this; + }; -/** - * Check for suites that do not have elements - * with `classname`, and hide them. - */ + /** + * Set the timeout in milliseconds. + * + * @param {Number} timeout + * @return {Mocha} + * @api public + */ -function hideSuitesWithout(classname) { - var suites = document.getElementsByClassName('suite'); - for (var i = 0; i < suites.length; i++) { - var els = suites[i].getElementsByClassName(classname); - if (0 == els.length) suites[i].className += ' hidden'; - } -} + Mocha.prototype.timeout = function(timeout) { + this.suite.timeout(timeout); + return this; + }; -/** - * Unhide .hidden suites. - */ + /** + * Set slowness threshold in milliseconds. + * + * @param {Number} slow + * @return {Mocha} + * @api public + */ -function unhide() { - var els = document.getElementsByClassName('suite hidden'); - for (var i = 0; i < els.length; ++i) { - els[i].className = els[i].className.replace('suite hidden', 'suite'); - } -} + Mocha.prototype.slow = function(slow) { + this.suite.slow(slow); + return this; + }; -/** - * Set `el` text to `str`. - */ + /** + * Makes all tests async (accepting a callback) + * + * @return {Mocha} + * @api public + */ -function text(el, str) { - if (el.textContent) { - el.textContent = str; - } else { - el.innerText = str; - } -} + Mocha.prototype.asyncOnly = function() { + this.options.asyncOnly = true; + return this; + }; -/** - * Listen on `event` with callback `fn`. - */ + /** + * Run tests and invoke `fn()` when complete. + * + * @param {Function} fn + * @return {Runner} + * @api public + */ -function on(el, event, fn) { - if (el.addEventListener) { - el.addEventListener(event, fn, false); - } else { - el.attachEvent('on' + event, fn); - } -} - -}); // module: reporters/html.js - -require.register("reporters/index.js", function(module, exports, require){ - -exports.Base = require('./base'); -exports.Dot = require('./dot'); -exports.Doc = require('./doc'); -exports.TAP = require('./tap'); -exports.JSON = require('./json'); -exports.HTML = require('./html'); -exports.List = require('./list'); -exports.Min = require('./min'); -exports.Spec = require('./spec'); -exports.Nyan = require('./nyan'); -exports.XUnit = require('./xunit'); -exports.Markdown = require('./markdown'); -exports.Progress = require('./progress'); -exports.Landing = require('./landing'); -exports.JSONCov = require('./json-cov'); -exports.HTMLCov = require('./html-cov'); -exports.JSONStream = require('./json-stream'); -exports.Teamcity = require('./teamcity'); - -}); // module: reporters/index.js - -require.register("reporters/json-cov.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base'); - -/** - * Expose `JSONCov`. - */ - -exports = module.exports = JSONCov; - -/** - * Initialize a new `JsCoverage` reporter. - * - * @param {Runner} runner - * @param {Boolean} output - * @api public - */ - -function JSONCov(runner, output) { - var self = this - , output = 1 == arguments.length ? true : output; - - Base.call(this, runner); - - var tests = [] - , failures = [] - , passes = []; - - runner.on('test end', function(test){ - tests.push(test); - }); - - runner.on('pass', function(test){ - passes.push(test); - }); - - runner.on('fail', function(test){ - failures.push(test); - }); - - runner.on('end', function(){ - var cov = global._$jscoverage || {}; - var result = self.cov = map(cov); - result.stats = self.stats; - result.tests = tests.map(clean); - result.failures = failures.map(clean); - result.passes = passes.map(clean); - if (!output) return; - process.stdout.write(JSON.stringify(result, null, 2 )); - }); -} - -/** - * Map jscoverage data to a JSON structure - * suitable for reporting. - * - * @param {Object} cov - * @return {Object} - * @api private - */ - -function map(cov) { - var ret = { - instrumentation: 'node-jscoverage' - , sloc: 0 - , hits: 0 - , misses: 0 - , coverage: 0 - , files: [] - }; + Mocha.prototype.run = function(fn) { + if (this.files.length) this.loadFiles(); + var suite = this.suite; + var options = this.options; + var runner = new exports.Runner(suite); + var reporter = new this._reporter(runner); + runner.ignoreLeaks = options.ignoreLeaks; + runner.asyncOnly = options.asyncOnly; + if (options.grep) runner.grep(options.grep, options.invert); + if (options.globals) runner.globals(options.globals); + if (options.growl) this._growl(runner, reporter); + return runner.run(fn); + }; + }); // module: mocha.js - for (var filename in cov) { - var data = coverage(filename, cov[filename]); - ret.files.push(data); - ret.hits += data.hits; - ret.misses += data.misses; - ret.sloc += data.sloc; - } + require.register('ms.js', function(module, exports, require) { + /** + * Helpers. + */ - ret.files.sort(function(a, b) { - return a.filename.localeCompare(b.filename); - }); + var s = 1000; + var m = s * 60; + var h = m * 60; + var d = h * 24; - if (ret.sloc > 0) { - ret.coverage = (ret.hits / ret.sloc) * 100; - } + /** + * Parse or format the given `val`. + * + * @param {String|Number} val + * @return {String|Number} + * @api public + */ - return ret; -}; - -/** - * Map jscoverage data for a single source file - * to a JSON structure suitable for reporting. - * - * @param {String} filename name of the source file - * @param {Object} data jscoverage coverage data - * @return {Object} - * @api private - */ - -function coverage(filename, data) { - var ret = { - filename: filename, - coverage: 0, - hits: 0, - misses: 0, - sloc: 0, - source: {} - }; + module.exports = function(val) { + if ('string' == typeof val) return parse(val); + return format(val); + }; - data.source.forEach(function(line, num){ - num++; + /** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ - if (data[num] === 0) { - ret.misses++; - ret.sloc++; - } else if (data[num] !== undefined) { - ret.hits++; - ret.sloc++; + function parse(str) { + var m = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str); + if (!m) return; + var n = parseFloat(m[1]); + var type = (m[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'y': + return n * 31557600000; + case 'days': + case 'day': + case 'd': + return n * 86400000; + case 'hours': + case 'hour': + case 'h': + return n * 3600000; + case 'minutes': + case 'minute': + case 'm': + return n * 60000; + case 'seconds': + case 'second': + case 's': + return n * 1000; + case 'ms': + return n; + } } - ret.source[num] = { - source: line - , coverage: data[num] === undefined - ? '' - : data[num] - }; - }); - - ret.coverage = ret.hits / ret.sloc * 100; - - return ret; -} - -/** - * Return a plain-object representation of `test` - * free of cyclic properties etc. - * - * @param {Object} test - * @return {Object} - * @api private - */ - -function clean(test) { - return { - title: test.title - , fullTitle: test.fullTitle() - , duration: test.duration - } -} - -}); // module: reporters/json-cov.js - -require.register("reporters/json-stream.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , color = Base.color; - -/** - * Expose `List`. - */ - -exports = module.exports = List; - -/** - * Initialize a new `List` test reporter. - * - * @param {Runner} runner - * @api public - */ - -function List(runner) { - Base.call(this, runner); - - var self = this - , stats = this.stats - , total = runner.total; - - runner.on('start', function(){ - console.log(JSON.stringify(['start', { total: total }])); - }); - - runner.on('pass', function(test){ - console.log(JSON.stringify(['pass', clean(test)])); - }); - - runner.on('fail', function(test, err){ - console.log(JSON.stringify(['fail', clean(test)])); - }); - - runner.on('end', function(){ - process.stdout.write(JSON.stringify(['end', self.stats])); - }); -} - -/** - * Return a plain-object representation of `test` - * free of cyclic properties etc. - * - * @param {Object} test - * @return {Object} - * @api private - */ - -function clean(test) { - return { - title: test.title - , fullTitle: test.fullTitle() - , duration: test.duration - } -} -}); // module: reporters/json-stream.js - -require.register("reporters/json.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , cursor = Base.cursor - , color = Base.color; - -/** - * Expose `JSON`. - */ - -exports = module.exports = JSONReporter; - -/** - * Initialize a new `JSON` reporter. - * - * @param {Runner} runner - * @api public - */ - -function JSONReporter(runner) { - var self = this; - Base.call(this, runner); - - var tests = [] - , failures = [] - , passes = []; - - runner.on('test end', function(test){ - tests.push(test); - }); - - runner.on('pass', function(test){ - passes.push(test); - }); - - runner.on('fail', function(test){ - failures.push(test); - }); - - runner.on('end', function(){ - var obj = { - stats: self.stats - , tests: tests.map(clean) - , failures: failures.map(clean) - , passes: passes.map(clean) - }; - - process.stdout.write(JSON.stringify(obj, null, 2)); - }); -} - -/** - * Return a plain-object representation of `test` - * free of cyclic properties etc. - * - * @param {Object} test - * @return {Object} - * @api private - */ - -function clean(test) { - return { - title: test.title - , fullTitle: test.fullTitle() - , duration: test.duration - } -} -}); // module: reporters/json.js + /** + * Format the given `ms`. + * + * @param {Number} ms + * @return {String} + * @api public + */ -require.register("reporters/landing.js", function(module, exports, require){ + function format(ms) { + if (ms == d) return Math.round(ms / d) + ' day'; + if (ms > d) return Math.round(ms / d) + ' days'; + if (ms == h) return Math.round(ms / h) + ' hour'; + if (ms > h) return Math.round(ms / h) + ' hours'; + if (ms == m) return Math.round(ms / m) + ' minute'; + if (ms > m) return Math.round(ms / m) + ' minutes'; + if (ms == s) return Math.round(ms / s) + ' second'; + if (ms > s) return Math.round(ms / s) + ' seconds'; + return ms + ' ms'; + } + }); // module: ms.js -/** - * Module dependencies. - */ + require.register('reporters/base.js', function(module, exports, require) { + /** + * Module dependencies. + */ -var Base = require('./base') - , cursor = Base.cursor - , color = Base.color; + var tty = require('browser/tty'), + diff = require('browser/diff'), + ms = require('../ms'); -/** - * Expose `Landing`. - */ + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ -exports = module.exports = Landing; + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; -/** - * Airplane color. - */ + /** + * Check if both stdio streams are associated with a tty. + */ -Base.colors.plane = 0; + var isatty = tty.isatty(1) && tty.isatty(2); -/** - * Airplane crash color. - */ + /** + * Expose `Base`. + */ -Base.colors['plane crash'] = 31; + exports = module.exports = Base; -/** - * Runway color. - */ + /** + * Enable coloring by default. + */ -Base.colors.runway = 90; + exports.useColors = isatty; -/** - * Initialize a new `Landing` reporter. - * - * @param {Runner} runner - * @api public - */ + /** + * Default color map. + */ -function Landing(runner) { - Base.call(this, runner); + exports.colors = { + pass: 90, + fail: 31, + 'bright pass': 92, + 'bright fail': 91, + 'bright yellow': 93, + pending: 36, + suite: 0, + 'error title': 0, + 'error message': 31, + 'error stack': 90, + checkmark: 32, + fast: 90, + medium: 33, + slow: 31, + green: 32, + light: 90, + 'diff gutter': 90, + 'diff added': 42, + 'diff removed': 41, + }; - var self = this - , stats = this.stats - , width = Base.window.width * .75 | 0 - , total = runner.total - , stream = process.stdout - , plane = color('plane', '✈') - , crashed = -1 - , n = 0; + /** + * Default symbol map. + */ - function runway() { - var buf = Array(width).join('-'); - return ' ' + color('runway', buf); - } + exports.symbols = { + ok: '✓', + err: '✖', + dot: '․', + }; - runner.on('start', function(){ - stream.write('\n '); - cursor.hide(); - }); - - runner.on('test end', function(test){ - // check if the plane crashed - var col = -1 == crashed - ? width * ++n / total | 0 - : crashed; - - // show the crash - if ('failed' == test.state) { - plane = color('plane crash', '✈'); - crashed = col; + // With node.js on Windows: use symbols available in terminal default fonts + if ('win32' == process.platform) { + exports.symbols.ok = '\u221A'; + exports.symbols.err = '\u00D7'; + exports.symbols.dot = '.'; } - // render landing strip - stream.write('\u001b[4F\n\n'); - stream.write(runway()); - stream.write('\n '); - stream.write(color('runway', Array(col).join('⋅'))); - stream.write(plane) - stream.write(color('runway', Array(width - col).join('⋅') + '\n')); - stream.write(runway()); - stream.write('\u001b[0m'); - }); + /** + * Color `str` with the given `type`, + * allowing colors to be disabled, + * as well as user-defined color + * schemes. + * + * @param {String} type + * @param {String} str + * @return {String} + * @api private + */ - runner.on('end', function(){ - cursor.show(); - console.log(); - self.epilogue(); - }); -} + var color = (exports.color = function(type, str) { + if (!exports.useColors) return str; + return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; + }); -/** - * Inherit from `Base.prototype`. - */ + /** + * Expose term window size, with some + * defaults for when stderr is not a tty. + */ -Landing.prototype = new Base; -Landing.prototype.constructor = Landing; - -}); // module: reporters/landing.js - -require.register("reporters/list.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , cursor = Base.cursor - , color = Base.color; - -/** - * Expose `List`. - */ - -exports = module.exports = List; - -/** - * Initialize a new `List` test reporter. - * - * @param {Runner} runner - * @api public - */ - -function List(runner) { - Base.call(this, runner); - - var self = this - , stats = this.stats - , n = 0; - - runner.on('start', function(){ - console.log(); - }); - - runner.on('test', function(test){ - process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); - }); - - runner.on('pending', function(test){ - var fmt = color('checkmark', ' -') - + color('pending', ' %s'); - console.log(fmt, test.fullTitle()); - }); - - runner.on('pass', function(test){ - var fmt = color('checkmark', ' '+Base.symbols.dot) - + color('pass', ' %s: ') - + color(test.speed, '%dms'); - cursor.CR(); - console.log(fmt, test.fullTitle(), test.duration); - }); - - runner.on('fail', function(test, err){ - cursor.CR(); - console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); - }); - - runner.on('end', self.epilogue.bind(self)); -} - -/** - * Inherit from `Base.prototype`. - */ - -List.prototype = new Base; -List.prototype.constructor = List; - - -}); // module: reporters/list.js - -require.register("reporters/markdown.js", function(module, exports, require){ -/** - * Module dependencies. - */ - -var Base = require('./base') - , utils = require('../utils'); - -/** - * Expose `Markdown`. - */ + exports.window = { + width: isatty ? (process.stdout.getWindowSize ? process.stdout.getWindowSize(1)[0] : tty.getWindowSize()[1]) : 75, + }; -exports = module.exports = Markdown; + /** + * Expose some basic cursor interactions + * that are common among reporters. + */ -/** - * Initialize a new `Markdown` reporter. - * - * @param {Runner} runner - * @api public - */ + exports.cursor = { + hide: function() { + process.stdout.write('\u001b[?25l'); + }, -function Markdown(runner) { - Base.call(this, runner); + show: function() { + process.stdout.write('\u001b[?25h'); + }, - var self = this - , stats = this.stats - , total = runner.total - , level = 0 - , buf = ''; + deleteLine: function() { + process.stdout.write('\u001b[2K'); + }, - function title(str) { - return Array(level).join('#') + ' ' + str; - } + beginningOfLine: function() { + process.stdout.write('\u001b[0G'); + }, + + CR: function() { + exports.cursor.deleteLine(); + exports.cursor.beginningOfLine(); + }, + }; + + /** + * Outut the given `failures` as a list. + * + * @param {Array} failures + * @api public + */ + + exports.list = function(failures) { + console.error(); + failures.forEach(function(test, i) { + // format + var fmt = color('error title', ' %s) %s:\n') + color('error message', ' %s') + color('error stack', '\n%s\n'); + + // msg + var err = test.err, + message = err.message || '', + stack = err.stack || message, + index = stack.indexOf(message) + message.length, + msg = stack.slice(0, index), + actual = err.actual, + expected = err.expected, + escape = true; + + // explicitly show diff + if (err.showDiff) { + escape = false; + err.actual = actual = JSON.stringify(actual, null, 2); + err.expected = expected = JSON.stringify(expected, null, 2); + } + + // actual / expected diff + if ('string' == typeof actual && 'string' == typeof expected) { + var len = Math.max(actual.length, expected.length); + + if (len < 20) msg = errorDiff(err, 'Chars', escape); + else msg = errorDiff(err, 'Words', escape); + + // linenos + var lines = msg.split('\n'); + if (lines.length > 4) { + var width = String(lines.length).length; + msg = lines + .map(function(str, i) { + return pad(++i, width) + ' |' + ' ' + str; + }) + .join('\n'); + } + + // legend + msg = '\n' + color('diff removed', 'actual') + ' ' + color('diff added', 'expected') + '\n\n' + msg + '\n'; + + // indent + msg = msg.replace(/^/gm, ' '); + + fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); + } + + // indent stack trace without msg + stack = stack.slice(index ? index + 1 : index).replace(/^/gm, ' '); + + console.error(fmt, i + 1, test.fullTitle(), msg, stack); + }); + }; + + /** + * Initialize a new `Base` reporter. + * + * All other reporters generally + * inherit from this reporter, providing + * stats such as test duration, number + * of tests passed / failed etc. + * + * @param {Runner} runner + * @api public + */ + + function Base(runner) { + var self = this, + stats = (this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 }), + failures = (this.failures = []); + + if (!runner) return; + this.runner = runner; + + runner.stats = stats; + + runner.on('start', function() { + stats.start = new Date(); + }); + + runner.on('suite', function(suite) { + stats.suites = stats.suites || 0; + suite.root || stats.suites++; + }); + + runner.on('test end', function(test) { + stats.tests = stats.tests || 0; + stats.tests++; + }); + + runner.on('pass', function(test) { + stats.passes = stats.passes || 0; + + var medium = test.slow() / 2; + test.speed = test.duration > test.slow() ? 'slow' : test.duration > medium ? 'medium' : 'fast'; + + stats.passes++; + }); + + runner.on('fail', function(test, err) { + stats.failures = stats.failures || 0; + stats.failures++; + test.err = err; + failures.push(test); + }); + + runner.on('end', function() { + stats.end = new Date(); + stats.duration = new Date() - stats.start; + }); + + runner.on('pending', function() { + stats.pending++; + }); + } + + /** + * Output common epilogue used by many of + * the bundled reporters. + * + * @api public + */ + + Base.prototype.epilogue = function() { + var stats = this.stats, + fmt, + tests; + + console.log(); + + function pluralize(n) { + return 1 == n ? 'test' : 'tests'; + } + + // failure + if (stats.failures) { + fmt = color('bright fail', ' ' + exports.symbols.err) + color('fail', ' %d of %d %s failed') + color('light', ':'); + + console.error(fmt, stats.failures, this.runner.total, pluralize(this.runner.total)); + + Base.list(this.failures); + console.error(); + return; + } + + // pass + fmt = color('bright pass', ' ') + color('green', ' %d %s complete') + color('light', ' (%s)'); + + console.log(fmt, stats.tests || 0, pluralize(stats.tests), ms(stats.duration)); + + // pending + if (stats.pending) { + fmt = color('pending', ' ') + color('pending', ' %d %s pending'); + + console.log(fmt, stats.pending, pluralize(stats.pending)); + } + + console.log(); + }; + + /** + * Pad the given `str` to `len`. + * + * @param {String} str + * @param {String} len + * @return {String} + * @api private + */ + + function pad(str, len) { + str = String(str); + return Array(len - str.length + 1).join(' ') + str; + } + + /** + * Return a character diff for `err`. + * + * @param {Error} err + * @return {String} + * @api private + */ + + function errorDiff(err, type, escape) { + return diff['diff' + type](err.actual, err.expected) + .map(function(str) { + if (escape) { + str.value = str.value + .replace(/\t/g, '') + .replace(/\r/g, '') + .replace(/\n/g, '\n'); + } + if (str.added) return colorLines('diff added', str.value); + if (str.removed) return colorLines('diff removed', str.value); + return str.value; + }) + .join(''); + } + + /** + * Color lines for `str`, using the color `name`. + * + * @param {String} name + * @param {String} str + * @return {String} + * @api private + */ + + function colorLines(name, str) { + return str + .split('\n') + .map(function(str) { + return color(name, str); + }) + .join('\n'); + } + }); // module: reporters/base.js + + require.register('reporters/doc.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + utils = require('../utils'); + + /** + * Expose `Doc`. + */ + + exports = module.exports = Doc; + + /** + * Initialize a new `Doc` reporter. + * + * @param {Runner} runner + * @api public + */ + + function Doc(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + total = runner.total, + indents = 2; + + function indent() { + return Array(indents).join(' '); + } + + runner.on('suite', function(suite) { + if (suite.root) return; + ++indents; + console.log('%s
      ', indent()); + ++indents; + console.log('%s

      %s

      ', indent(), utils.escape(suite.title)); + console.log('%s
      ', indent()); + }); + + runner.on('suite end', function(suite) { + if (suite.root) return; + console.log('%s
      ', indent()); + --indents; + console.log('%s
      ', indent()); + --indents; + }); + + runner.on('pass', function(test) { + console.log('%s
      %s
      ', indent(), utils.escape(test.title)); + var code = utils.escape(utils.clean(test.fn.toString())); + console.log('%s
      %s
      ', indent(), code); + }); + } + }); // module: reporters/doc.js + + require.register('reporters/dot.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + color = Base.color; + + /** + * Expose `Dot`. + */ + + exports = module.exports = Dot; + + /** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + + function Dot(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + width = (Base.window.width * 0.75) | 0, + n = 0; + + runner.on('start', function() { + process.stdout.write('\n '); + }); + + runner.on('pending', function(test) { + process.stdout.write(color('pending', Base.symbols.dot)); + }); + + runner.on('pass', function(test) { + if (++n % width == 0) process.stdout.write('\n '); + if ('slow' == test.speed) { + process.stdout.write(color('bright yellow', Base.symbols.dot)); + } else { + process.stdout.write(color(test.speed, Base.symbols.dot)); + } + }); + + runner.on('fail', function(test, err) { + if (++n % width == 0) process.stdout.write('\n '); + process.stdout.write(color('fail', Base.symbols.dot)); + }); + + runner.on('end', function() { + console.log(); + self.epilogue(); + }); + } + + /** + * Inherit from `Base.prototype`. + */ + + Dot.prototype = new Base(); + Dot.prototype.constructor = Dot; + }); // module: reporters/dot.js + + require.register('reporters/html-cov.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var JSONCov = require('./json-cov'), + fs = require('browser/fs'); + + /** + * Expose `HTMLCov`. + */ + + exports = module.exports = HTMLCov; + + /** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @api public + */ + + function HTMLCov(runner) { + var jade = require('jade'), + file = __dirname + '/templates/coverage.jade', + str = fs.readFileSync(file, 'utf8'), + fn = jade.compile(str, { filename: file }), + self = this; + + JSONCov.call(this, runner, false); + + runner.on('end', function() { + process.stdout.write( + fn({ + cov: self.cov, + coverageClass: coverageClass, + }) + ); + }); + } + + /** + * Return coverage class for `n`. + * + * @return {String} + * @api private + */ + + function coverageClass(n) { + if (n >= 75) return 'high'; + if (n >= 50) return 'medium'; + if (n >= 25) return 'low'; + return 'terrible'; + } + }); // module: reporters/html-cov.js + + require.register('reporters/html.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + utils = require('../utils'), + Progress = require('../browser/progress'), + escape = utils.escape; + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; + + /** + * Expose `Doc`. + */ + + exports = module.exports = HTML; + + /** + * Stats template. + */ + + var statsTemplate = + ''; + + /** + * Initialize a new `Doc` reporter. + * + * @param {Runner} runner + * @api public + */ + + function HTML(runner, root) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + total = runner.total, + stat = fragment(statsTemplate), + items = stat.getElementsByTagName('li'), + passes = items[1].getElementsByTagName('em')[0], + passesLink = items[1].getElementsByTagName('a')[0], + failures = items[2].getElementsByTagName('em')[0], + failuresLink = items[2].getElementsByTagName('a')[0], + duration = items[3].getElementsByTagName('em')[0], + canvas = stat.getElementsByTagName('canvas')[0], + report = fragment('
        '), + stack = [report], + progress, + ctx; + + root = root || document.getElementById('mocha'); + + if (canvas.getContext) { + var ratio = window.devicePixelRatio || 1; + canvas.style.width = canvas.width; + canvas.style.height = canvas.height; + canvas.width *= ratio; + canvas.height *= ratio; + ctx = canvas.getContext('2d'); + ctx.scale(ratio, ratio); + progress = new Progress(); + } + + if (!root) return error('#mocha div missing, add it to your document'); + + // pass toggle + on(passesLink, 'click', function() { + unhide(); + var name = /pass/.test(report.className) ? '' : ' pass'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test pass'); + }); + + // failure toggle + on(failuresLink, 'click', function() { + unhide(); + var name = /fail/.test(report.className) ? '' : ' fail'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test fail'); + }); + + root.appendChild(stat); + root.appendChild(report); + + if (progress) progress.size(40); + + runner.on('suite', function(suite) { + if (suite.root) return; + + // suite + var url = '?grep=' + encodeURIComponent(suite.fullTitle()); + var el = fragment('
      • %s

      • ', url, escape(suite.title)); + + // container + stack[0].appendChild(el); + stack.unshift(document.createElement('ul')); + el.appendChild(stack[0]); + }); + + runner.on('suite end', function(suite) { + if (suite.root) return; + stack.shift(); + }); + + runner.on('fail', function(test, err) { + if ('hook' == test.type) runner.emit('test end', test); + }); + + runner.on('test end', function(test) { + window.scrollTo(0, document.body.scrollHeight); + + // TODO: add to stats + var percent = ((stats.tests / this.total) * 100) | 0; + if (progress) progress.update(percent).draw(ctx); + + // update stats + var ms = new Date() - stats.start; + text(passes, stats.passes); + text(failures, stats.failures); + text(duration, (ms / 1000).toFixed(2)); + + // test + if ('passed' == test.state) { + var el = fragment( + '
      • %e%ems

      • ', + test.speed, + test.title, + test.duration, + encodeURIComponent(test.fullTitle()) + ); + } else if (test.pending) { + var el = fragment('
      • %e

      • ', test.title); + } else { + var el = fragment( + '
      • %e

      • ', + test.title, + encodeURIComponent(test.fullTitle()) + ); + var str = test.err.stack || test.err.toString(); + + // FF / Opera do not add the message + if (!~str.indexOf(test.err.message)) { + str = test.err.message + '\n' + str; + } + + // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we + // check for the result of the stringifying. + if ('[object Error]' == str) str = test.err.message; + + // Safari doesn't give you a stack. Let's at least provide a source line. + if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) { + str += '\n(' + test.err.sourceURL + ':' + test.err.line + ')'; + } + + el.appendChild(fragment('
        %e
        ', str)); + } + + // toggle code + // TODO: defer + if (!test.pending) { + var h2 = el.getElementsByTagName('h2')[0]; + + on(h2, 'click', function() { + pre.style.display = 'none' == pre.style.display ? 'inline-block' : 'none'; + }); + + var pre = fragment('
        %e
        ', utils.clean(test.fn.toString())); + el.appendChild(pre); + pre.style.display = 'none'; + } + + // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. + if (stack[0]) stack[0].appendChild(el); + }); + } + + /** + * Display error `msg`. + */ + + function error(msg) { + document.body.appendChild(fragment('
        %s
        ', msg)); + } + + /** + * Return a DOM fragment from `html`. + */ + + function fragment(html) { + var args = arguments, + div = document.createElement('div'), + i = 1; + + div.innerHTML = html.replace(/%([se])/g, function(_, type) { + switch (type) { + case 's': + return String(args[i++]); + case 'e': + return escape(args[i++]); + } + }); + + return div.firstChild; + } + + /** + * Check for suites that do not have elements + * with `classname`, and hide them. + */ + + function hideSuitesWithout(classname) { + var suites = document.getElementsByClassName('suite'); + for (var i = 0; i < suites.length; i++) { + var els = suites[i].getElementsByClassName(classname); + if (0 == els.length) suites[i].className += ' hidden'; + } + } + + /** + * Unhide .hidden suites. + */ + + function unhide() { + var els = document.getElementsByClassName('suite hidden'); + for (var i = 0; i < els.length; ++i) { + els[i].className = els[i].className.replace('suite hidden', 'suite'); + } + } + + /** + * Set `el` text to `str`. + */ + + function text(el, str) { + if (el.textContent) { + el.textContent = str; + } else { + el.innerText = str; + } + } + + /** + * Listen on `event` with callback `fn`. + */ + + function on(el, event, fn) { + if (el.addEventListener) { + el.addEventListener(event, fn, false); + } else { + el.attachEvent('on' + event, fn); + } + } + }); // module: reporters/html.js + + require.register('reporters/index.js', function(module, exports, require) { + exports.Base = require('./base'); + exports.Dot = require('./dot'); + exports.Doc = require('./doc'); + exports.TAP = require('./tap'); + exports.JSON = require('./json'); + exports.HTML = require('./html'); + exports.List = require('./list'); + exports.Min = require('./min'); + exports.Spec = require('./spec'); + exports.Nyan = require('./nyan'); + exports.XUnit = require('./xunit'); + exports.Markdown = require('./markdown'); + exports.Progress = require('./progress'); + exports.Landing = require('./landing'); + exports.JSONCov = require('./json-cov'); + exports.HTMLCov = require('./html-cov'); + exports.JSONStream = require('./json-stream'); + exports.Teamcity = require('./teamcity'); + }); // module: reporters/index.js + + require.register('reporters/json-cov.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'); + + /** + * Expose `JSONCov`. + */ + + exports = module.exports = JSONCov; + + /** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @param {Boolean} output + * @api public + */ + + function JSONCov(runner, output) { + var self = this, + output = 1 == arguments.length ? true : output; + + Base.call(this, runner); + + var tests = [], + failures = [], + passes = []; + + runner.on('test end', function(test) { + tests.push(test); + }); + + runner.on('pass', function(test) { + passes.push(test); + }); + + runner.on('fail', function(test) { + failures.push(test); + }); + + runner.on('end', function() { + var cov = global._$jscoverage || {}; + var result = (self.cov = map(cov)); + result.stats = self.stats; + result.tests = tests.map(clean); + result.failures = failures.map(clean); + result.passes = passes.map(clean); + if (!output) return; + process.stdout.write(JSON.stringify(result, null, 2)); + }); + } + + /** + * Map jscoverage data to a JSON structure + * suitable for reporting. + * + * @param {Object} cov + * @return {Object} + * @api private + */ + + function map(cov) { + var ret = { + instrumentation: 'node-jscoverage', + sloc: 0, + hits: 0, + misses: 0, + coverage: 0, + files: [], + }; + + for (var filename in cov) { + var data = coverage(filename, cov[filename]); + ret.files.push(data); + ret.hits += data.hits; + ret.misses += data.misses; + ret.sloc += data.sloc; + } + + ret.files.sort(function(a, b) { + return a.filename.localeCompare(b.filename); + }); + + if (ret.sloc > 0) { + ret.coverage = (ret.hits / ret.sloc) * 100; + } + + return ret; + } + + /** + * Map jscoverage data for a single source file + * to a JSON structure suitable for reporting. + * + * @param {String} filename name of the source file + * @param {Object} data jscoverage coverage data + * @return {Object} + * @api private + */ + + function coverage(filename, data) { + var ret = { + filename: filename, + coverage: 0, + hits: 0, + misses: 0, + sloc: 0, + source: {}, + }; + + data.source.forEach(function(line, num) { + num++; + + if (data[num] === 0) { + ret.misses++; + ret.sloc++; + } else if (data[num] !== undefined) { + ret.hits++; + ret.sloc++; + } + + ret.source[num] = { + source: line, + coverage: data[num] === undefined ? '' : data[num], + }; + }); + + ret.coverage = (ret.hits / ret.sloc) * 100; + + return ret; + } + + /** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + + function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration, + }; + } + }); // module: reporters/json-cov.js + + require.register('reporters/json-stream.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + color = Base.color; + + /** + * Expose `List`. + */ + + exports = module.exports = List; + + /** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + + function List(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + total = runner.total; + + runner.on('start', function() { + console.log(JSON.stringify(['start', { total: total }])); + }); + + runner.on('pass', function(test) { + console.log(JSON.stringify(['pass', clean(test)])); + }); + + runner.on('fail', function(test, err) { + console.log(JSON.stringify(['fail', clean(test)])); + }); + + runner.on('end', function() { + process.stdout.write(JSON.stringify(['end', self.stats])); + }); + } + + /** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + + function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration, + }; + } + }); // module: reporters/json-stream.js + + require.register('reporters/json.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `JSON`. + */ + + exports = module.exports = JSONReporter; + + /** + * Initialize a new `JSON` reporter. + * + * @param {Runner} runner + * @api public + */ + + function JSONReporter(runner) { + var self = this; + Base.call(this, runner); + + var tests = [], + failures = [], + passes = []; + + runner.on('test end', function(test) { + tests.push(test); + }); + + runner.on('pass', function(test) { + passes.push(test); + }); + + runner.on('fail', function(test) { + failures.push(test); + }); + + runner.on('end', function() { + var obj = { + stats: self.stats, + tests: tests.map(clean), + failures: failures.map(clean), + passes: passes.map(clean), + }; + + process.stdout.write(JSON.stringify(obj, null, 2)); + }); + } + + /** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + + function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration, + }; + } + }); // module: reporters/json.js + + require.register('reporters/landing.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `Landing`. + */ + + exports = module.exports = Landing; + + /** + * Airplane color. + */ + + Base.colors.plane = 0; + + /** + * Airplane crash color. + */ + + Base.colors['plane crash'] = 31; + + /** + * Runway color. + */ + + Base.colors.runway = 90; + + /** + * Initialize a new `Landing` reporter. + * + * @param {Runner} runner + * @api public + */ + + function Landing(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + width = (Base.window.width * 0.75) | 0, + total = runner.total, + stream = process.stdout, + plane = color('plane', '✈'), + crashed = -1, + n = 0; + + function runway() { + var buf = Array(width).join('-'); + return ' ' + color('runway', buf); + } + + runner.on('start', function() { + stream.write('\n '); + cursor.hide(); + }); + + runner.on('test end', function(test) { + // check if the plane crashed + var col = -1 == crashed ? ((width * ++n) / total) | 0 : crashed; + + // show the crash + if ('failed' == test.state) { + plane = color('plane crash', '✈'); + crashed = col; + } + + // render landing strip + stream.write('\u001b[4F\n\n'); + stream.write(runway()); + stream.write('\n '); + stream.write(color('runway', Array(col).join('⋅'))); + stream.write(plane); + stream.write(color('runway', Array(width - col).join('⋅') + '\n')); + stream.write(runway()); + stream.write('\u001b[0m'); + }); + + runner.on('end', function() { + cursor.show(); + console.log(); + self.epilogue(); + }); + } + + /** + * Inherit from `Base.prototype`. + */ + + Landing.prototype = new Base(); + Landing.prototype.constructor = Landing; + }); // module: reporters/landing.js + + require.register('reporters/list.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `List`. + */ + + exports = module.exports = List; + + /** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + + function List(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + n = 0; + + runner.on('start', function() { + console.log(); + }); + + runner.on('test', function(test) { + process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); + }); + + runner.on('pending', function(test) { + var fmt = color('checkmark', ' -') + color('pending', ' %s'); + console.log(fmt, test.fullTitle()); + }); + + runner.on('pass', function(test) { + var fmt = color('checkmark', ' ' + Base.symbols.dot) + color('pass', ' %s: ') + color(test.speed, '%dms'); + cursor.CR(); + console.log(fmt, test.fullTitle(), test.duration); + }); + + runner.on('fail', function(test, err) { + cursor.CR(); + console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); + }); + + runner.on('end', self.epilogue.bind(self)); + } + + /** + * Inherit from `Base.prototype`. + */ + + List.prototype = new Base(); + List.prototype.constructor = List; + }); // module: reporters/list.js + + require.register('reporters/markdown.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + utils = require('../utils'); + + /** + * Expose `Markdown`. + */ + + exports = module.exports = Markdown; + + /** + * Initialize a new `Markdown` reporter. + * + * @param {Runner} runner + * @api public + */ + + function Markdown(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + total = runner.total, + level = 0, + buf = ''; + + function title(str) { + return Array(level).join('#') + ' ' + str; + } + + function indent() { + return Array(level).join(' '); + } + + function mapTOC(suite, obj) { + var ret = obj; + obj = obj[suite.title] = obj[suite.title] || { suite: suite }; + suite.suites.forEach(function(suite) { + mapTOC(suite, obj); + }); + return ret; + } + + function stringifyTOC(obj, level) { + ++level; + var buf = ''; + var link; + for (var key in obj) { + if ('suite' == key) continue; + if (key) link = ' - [' + key + '](#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; + if (key) buf += Array(level).join(' ') + link; + buf += stringifyTOC(obj[key], level); + } + --level; + return buf; + } + + function generateTOC(suite) { + var obj = mapTOC(suite, {}); + return stringifyTOC(obj, 0); + } + + generateTOC(runner.suite); + + runner.on('suite', function(suite) { + ++level; + var slug = utils.slug(suite.fullTitle()); + buf += '' + '\n'; + buf += title(suite.title) + '\n'; + }); + + runner.on('suite end', function(suite) { + --level; + }); + + runner.on('pass', function(test) { + var code = utils.clean(test.fn.toString()); + buf += test.title + '.\n'; + buf += '\n```js\n'; + buf += code + '\n'; + buf += '```\n\n'; + }); + + runner.on('end', function() { + process.stdout.write('# TOC\n'); + process.stdout.write(generateTOC(runner.suite)); + process.stdout.write(buf); + }); + } + }); // module: reporters/markdown.js + + require.register('reporters/min.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'); + + /** + * Expose `Min`. + */ + + exports = module.exports = Min; + + /** + * Initialize a new `Min` minimal test reporter (best used with --watch). + * + * @param {Runner} runner + * @api public + */ + + function Min(runner) { + Base.call(this, runner); + + runner.on('start', function() { + // clear screen + process.stdout.write('\u001b[2J'); + // set cursor position + process.stdout.write('\u001b[1;3H'); + }); + + runner.on('end', this.epilogue.bind(this)); + } + + /** + * Inherit from `Base.prototype`. + */ + + Min.prototype = new Base(); + Min.prototype.constructor = Min; + }); // module: reporters/min.js + + require.register('reporters/nyan.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + color = Base.color; + + /** + * Expose `Dot`. + */ + + exports = module.exports = NyanCat; + + /** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + + function NyanCat(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + width = (Base.window.width * 0.75) | 0, + rainbowColors = (this.rainbowColors = self.generateColors()), + colorIndex = (this.colorIndex = 0), + numerOfLines = (this.numberOfLines = 4), + trajectories = (this.trajectories = [[], [], [], []]), + nyanCatWidth = (this.nyanCatWidth = 11), + trajectoryWidthMax = (this.trajectoryWidthMax = width - nyanCatWidth), + scoreboardWidth = (this.scoreboardWidth = 5), + tick = (this.tick = 0), + n = 0; + + runner.on('start', function() { + Base.cursor.hide(); + self.draw('start'); + }); + + runner.on('pending', function(test) { + self.draw('pending'); + }); + + runner.on('pass', function(test) { + self.draw('pass'); + }); + + runner.on('fail', function(test, err) { + self.draw('fail'); + }); + + runner.on('end', function() { + Base.cursor.show(); + for (var i = 0; i < self.numberOfLines; i++) write('\n'); + self.epilogue(); + }); + } + + /** + * Draw the nyan cat with runner `status`. + * + * @param {String} status + * @api private + */ + + NyanCat.prototype.draw = function(status) { + this.appendRainbow(); + this.drawScoreboard(); + this.drawRainbow(); + this.drawNyanCat(status); + this.tick = !this.tick; + }; + + /** + * Draw the "scoreboard" showing the number + * of passes, failures and pending tests. + * + * @api private + */ + + NyanCat.prototype.drawScoreboard = function() { + var stats = this.stats; + var colors = Base.colors; + + function draw(color, n) { + write(' '); + write('\u001b[' + color + 'm' + n + '\u001b[0m'); + write('\n'); + } + + draw(colors.green, stats.passes); + draw(colors.fail, stats.failures); + draw(colors.pending, stats.pending); + write('\n'); + + this.cursorUp(this.numberOfLines); + }; + + /** + * Append the rainbow. + * + * @api private + */ + + NyanCat.prototype.appendRainbow = function() { + var segment = this.tick ? '_' : '-'; + var rainbowified = this.rainbowify(segment); + + for (var index = 0; index < this.numberOfLines; index++) { + var trajectory = this.trajectories[index]; + if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift(); + trajectory.push(rainbowified); + } + }; + + /** + * Draw the rainbow. + * + * @api private + */ + + NyanCat.prototype.drawRainbow = function() { + var self = this; + + this.trajectories.forEach(function(line, index) { + write('\u001b[' + self.scoreboardWidth + 'C'); + write(line.join('')); + write('\n'); + }); + + this.cursorUp(this.numberOfLines); + }; + + /** + * Draw the nyan cat with `status`. + * + * @param {String} status + * @api private + */ + + NyanCat.prototype.drawNyanCat = function(status) { + var self = this; + var startWidth = this.scoreboardWidth + this.trajectories[0].length; + + [0, 1, 2, 3].forEach(function(index) { + write('\u001b[' + startWidth + 'C'); + + switch (index) { + case 0: + write('_,------,'); + write('\n'); + break; + case 1: + var padding = self.tick ? ' ' : ' '; + write('_|' + padding + '/\\_/\\ '); + write('\n'); + break; + case 2: + var padding = self.tick ? '_' : '__'; + var tail = self.tick ? '~' : '^'; + var face; + switch (status) { + case 'pass': + face = '( ^ .^)'; + break; + case 'fail': + face = '( o .o)'; + break; + default: + face = '( - .-)'; + } + write(tail + '|' + padding + face + ' '); + write('\n'); + break; + case 3: + var padding = self.tick ? ' ' : ' '; + write(padding + '"" "" '); + write('\n'); + break; + } + }); + + this.cursorUp(this.numberOfLines); + }; + + /** + * Move cursor up `n`. + * + * @param {Number} n + * @api private + */ + + NyanCat.prototype.cursorUp = function(n) { + write('\u001b[' + n + 'A'); + }; + + /** + * Move cursor down `n`. + * + * @param {Number} n + * @api private + */ + + NyanCat.prototype.cursorDown = function(n) { + write('\u001b[' + n + 'B'); + }; + + /** + * Generate rainbow colors. + * + * @return {Array} + * @api private + */ + + NyanCat.prototype.generateColors = function() { + var colors = []; + + for (var i = 0; i < 6 * 7; i++) { + var pi3 = Math.floor(Math.PI / 3); + var n = i * (1.0 / 6); + var r = Math.floor(3 * Math.sin(n) + 3); + var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); + var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); + colors.push(36 * r + 6 * g + b + 16); + } + + return colors; + }; + + /** + * Apply rainbow to the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + NyanCat.prototype.rainbowify = function(str) { + var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; + this.colorIndex += 1; + return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; + }; + + /** + * Stdout helper. + */ + + function write(string) { + process.stdout.write(string); + } + + /** + * Inherit from `Base.prototype`. + */ + + NyanCat.prototype = new Base(); + NyanCat.prototype.constructor = NyanCat; + }); // module: reporters/nyan.js + + require.register('reporters/progress.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `Progress`. + */ + + exports = module.exports = Progress; + + /** + * General progress bar color. + */ + + Base.colors.progress = 90; + + /** + * Initialize a new `Progress` bar test reporter. + * + * @param {Runner} runner + * @param {Object} options + * @api public + */ + + function Progress(runner, options) { + Base.call(this, runner); + + var self = this, + options = options || {}, + stats = this.stats, + width = (Base.window.width * 0.5) | 0, + total = runner.total, + complete = 0, + max = Math.max; + + // default chars + options.open = options.open || '['; + options.complete = options.complete || '▬'; + options.incomplete = options.incomplete || Base.symbols.dot; + options.close = options.close || ']'; + options.verbose = false; + + // tests started + runner.on('start', function() { + console.log(); + cursor.hide(); + }); + + // tests complete + runner.on('test end', function() { + complete++; + var incomplete = total - complete, + percent = complete / total, + n = (width * percent) | 0, + i = width - n; + + cursor.CR(); + process.stdout.write('\u001b[J'); + process.stdout.write(color('progress', ' ' + options.open)); + process.stdout.write(Array(n).join(options.complete)); + process.stdout.write(Array(i).join(options.incomplete)); + process.stdout.write(color('progress', options.close)); + if (options.verbose) { + process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); + } + }); + + // tests are complete, output some stats + // and the failures if any + runner.on('end', function() { + cursor.show(); + console.log(); + self.epilogue(); + }); + } + + /** + * Inherit from `Base.prototype`. + */ + + Progress.prototype = new Base(); + Progress.prototype.constructor = Progress; + }); // module: reporters/progress.js + + require.register('reporters/spec.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `Spec`. + */ + + exports = module.exports = Spec; + + /** + * Initialize a new `Spec` test reporter. + * + * @param {Runner} runner + * @api public + */ + + function Spec(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + indents = 0, + n = 0; + + function indent() { + return Array(indents).join(' '); + } + + runner.on('start', function() { + console.log(); + }); + + runner.on('suite', function(suite) { + ++indents; + console.log(color('suite', '%s%s'), indent(), suite.title); + }); + + runner.on('suite end', function(suite) { + --indents; + if (1 == indents) console.log(); + }); + + runner.on('test', function(test) { + process.stdout.write(indent() + color('pass', ' ◦ ' + test.title + ': ')); + }); + + runner.on('pending', function(test) { + var fmt = indent() + color('pending', ' - %s'); + console.log(fmt, test.title); + }); + + runner.on('pass', function(test) { + if ('fast' == test.speed) { + var fmt = indent() + color('checkmark', ' ' + Base.symbols.ok) + color('pass', ' %s '); + cursor.CR(); + console.log(fmt, test.title); + } else { + var fmt = indent() + color('checkmark', ' ' + Base.symbols.ok) + color('pass', ' %s ') + color(test.speed, '(%dms)'); + cursor.CR(); + console.log(fmt, test.title, test.duration); + } + }); + + runner.on('fail', function(test, err) { + cursor.CR(); + console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); + }); + + runner.on('end', self.epilogue.bind(self)); + } + + /** + * Inherit from `Base.prototype`. + */ + + Spec.prototype = new Base(); + Spec.prototype.constructor = Spec; + }); // module: reporters/spec.js + + require.register('reporters/tap.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `TAP`. + */ + + exports = module.exports = TAP; + + /** + * Initialize a new `TAP` reporter. + * + * @param {Runner} runner + * @api public + */ + + function TAP(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + n = 1; + + runner.on('start', function() { + var total = runner.grepTotal(runner.suite); + console.log('%d..%d', 1, total); + }); + + runner.on('test end', function() { + ++n; + }); + + runner.on('pending', function(test) { + console.log('ok %d %s # SKIP -', n, title(test)); + }); + + runner.on('pass', function(test) { + console.log('ok %d %s', n, title(test)); + }); + + runner.on('fail', function(test, err) { + console.log('not ok %d %s', n, title(test)); + console.log(err.stack.replace(/^/gm, ' ')); + }); + } + + /** + * Return a TAP-safe title of `test` + * + * @param {Object} test + * @return {String} + * @api private + */ + + function title(test) { + return test.fullTitle().replace(/#/g, ''); + } + }); // module: reporters/tap.js + + require.register('reporters/teamcity.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'); + + /** + * Expose `Teamcity`. + */ + + exports = module.exports = Teamcity; + + /** + * Initialize a new `Teamcity` reporter. + * + * @param {Runner} runner + * @api public + */ + + function Teamcity(runner) { + Base.call(this, runner); + var stats = this.stats; + + runner.on('start', function() { + console.log("##teamcity[testSuiteStarted name='mocha.suite']"); + }); + + runner.on('test', function(test) { + console.log("##teamcity[testStarted name='" + escape(test.fullTitle()) + "']"); + }); + + runner.on('fail', function(test, err) { + console.log("##teamcity[testFailed name='" + escape(test.fullTitle()) + "' message='" + escape(err.message) + "']"); + }); + + runner.on('pending', function(test) { + console.log("##teamcity[testIgnored name='" + escape(test.fullTitle()) + "' message='pending']"); + }); + + runner.on('test end', function(test) { + console.log("##teamcity[testFinished name='" + escape(test.fullTitle()) + "' duration='" + test.duration + "']"); + }); + + runner.on('end', function() { + console.log("##teamcity[testSuiteFinished name='mocha.suite' duration='" + stats.duration + "']"); + }); + } + + /** + * Escape the given `str`. + */ + + function escape(str) { + return str + .replace(/\|/g, '||') + .replace(/\n/g, '|n') + .replace(/\r/g, '|r') + .replace(/\[/g, '|[') + .replace(/\]/g, '|]') + .replace(/\u0085/g, '|x') + .replace(/\u2028/g, '|l') + .replace(/\u2029/g, '|p') + .replace(/'/g, "|'"); + } + }); // module: reporters/teamcity.js + + require.register('reporters/xunit.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var Base = require('./base'), + utils = require('../utils'), + escape = utils.escape; + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; + + /** + * Expose `XUnit`. + */ + + exports = module.exports = XUnit; + + /** + * Initialize a new `XUnit` reporter. + * + * @param {Runner} runner + * @api public + */ + + function XUnit(runner) { + Base.call(this, runner); + var stats = this.stats, + tests = [], + self = this; + + runner.on('pass', function(test) { + tests.push(test); + }); + + runner.on('fail', function(test) { + tests.push(test); + }); + + runner.on('end', function() { + console.log( + tag( + 'testsuite', + { + name: 'Mocha Tests', + tests: stats.tests, + failures: stats.failures, + errors: stats.failures, + skip: stats.tests - stats.failures - stats.passes, + timestamp: new Date().toUTCString(), + time: stats.duration / 1000, + }, + false + ) + ); + + tests.forEach(test); + console.log(''); + }); + } + + /** + * Inherit from `Base.prototype`. + */ + + XUnit.prototype = new Base(); + XUnit.prototype.constructor = XUnit; + + /** + * Output tag for the given `test.` + */ + + function test(test) { + var attrs = { + classname: test.parent.fullTitle(), + name: test.title, + time: test.duration / 1000, + }; + + if ('failed' == test.state) { + var err = test.err; + attrs.message = escape(err.message); + console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); + } else if (test.pending) { + console.log(tag('testcase', attrs, false, tag('skipped', {}, true))); + } else { + console.log(tag('testcase', attrs, true)); + } + } + + /** + * HTML tag helper. + */ + + function tag(name, attrs, close, content) { + var end = close ? '/>' : '>', + pairs = [], + tag; + + for (var key in attrs) { + pairs.push(key + '="' + escape(attrs[key]) + '"'); + } + + tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; + if (content) tag += content + ''; + } + }); // module: reporters/xunit.js + + require.register('runnable.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var EventEmitter = require('browser/events').EventEmitter, + debug = require('browser/debug')('mocha:runnable'), + milliseconds = require('./ms'); + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; + + /** + * Object#toString(). + */ + + var toString = Object.prototype.toString; + + /** + * Expose `Runnable`. + */ + + module.exports = Runnable; + + /** + * Initialize a new `Runnable` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + + function Runnable(title, fn) { + this.title = title; + this.fn = fn; + this.async = fn && fn.length; + this.sync = !this.async; + this._timeout = 2000; + this._slow = 75; + this.timedOut = false; + } + + /** + * Inherit from `EventEmitter.prototype`. + */ + + Runnable.prototype = new EventEmitter(); + Runnable.prototype.constructor = Runnable; + + /** + * Set & get timeout `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + + Runnable.prototype.timeout = function(ms) { + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = ms; + if (this.timer) this.resetTimeout(); + return this; + }; + + /** + * Set & get slow `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + + Runnable.prototype.slow = function(ms) { + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._slow = ms; + return this; + }; + + /** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + + Runnable.prototype.fullTitle = function() { + return this.parent.fullTitle() + ' ' + this.title; + }; + + /** + * Clear the timeout. + * + * @api private + */ + + Runnable.prototype.clearTimeout = function() { + clearTimeout(this.timer); + }; + + /** + * Inspect the runnable void of private properties. + * + * @return {String} + * @api private + */ + + Runnable.prototype.inspect = function() { + return JSON.stringify( + this, + function(key, val) { + if ('_' == key[0]) return; + if ('parent' == key) return '#'; + if ('ctx' == key) return '#'; + return val; + }, + 2 + ); + }; + + /** + * Reset the timeout. + * + * @api private + */ + + Runnable.prototype.resetTimeout = function() { + var self = this, + ms = this.timeout(); + + this.clearTimeout(); + if (ms) { + this.timer = setTimeout(function() { + self.callback(new Error('timeout of ' + ms + 'ms exceeded')); + self.timedOut = true; + }, ms); + } + }; + + /** + * Run the test and invoke `fn(err)`. + * + * @param {Function} fn + * @api private + */ + + Runnable.prototype.run = function(fn) { + var self = this, + ms = this.timeout(), + start = new Date(), + ctx = this.ctx, + finished, + emitted; + + if (ctx) ctx.runnable(this); + + // timeout + if (this.async) { + if (ms) { + this.timer = setTimeout(function() { + done(new Error('timeout of ' + ms + 'ms exceeded')); + self.timedOut = true; + }, ms); + } + } + + // called multiple times + function multiple(err) { + if (emitted) return; + emitted = true; + self.emit('error', err || new Error('done() called multiple times')); + } + + // finished + function done(err) { + if (self.timedOut) return; + if (finished) return multiple(err); + self.clearTimeout(); + self.duration = new Date() - start; + finished = true; + fn(err); + } + + // for .resetTimeout() + this.callback = done; + + // async + if (this.async) { + try { + this.fn.call(ctx, function(err) { + if (toString.call(err) === '[object Error]') return done(err); + if (null != err) return done(new Error('done() invoked with non-Error: ' + err)); + done(); + }); + } catch (err) { + done(err); + } + return; + } + + if (this.asyncOnly) { + return done(new Error('--async-only option in use without declaring `done()`')); + } + + // sync + try { + if (!this.pending) this.fn.call(ctx); + this.duration = new Date() - start; + fn(); + } catch (err) { + fn(err); + } + }; + }); // module: runnable.js + + require.register('runner.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var EventEmitter = require('browser/events').EventEmitter, + debug = require('browser/debug')('mocha:runner'), + Test = require('./test'), + utils = require('./utils'), + filter = utils.filter, + keys = utils.keys, + noop = function() {}; + + /** + * Non-enumerable globals. + */ + + var globals = ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'XMLHttpRequest', 'Date']; + + /** + * Expose `Runner`. + */ + + module.exports = Runner; + + /** + * Initialize a `Runner` for the given `suite`. + * + * Events: + * + * - `start` execution started + * - `end` execution complete + * - `suite` (suite) test suite execution started + * - `suite end` (suite) all tests (and sub-suites) have finished + * - `test` (test) test execution started + * - `test end` (test) test completed + * - `hook` (hook) hook execution started + * - `hook end` (hook) hook complete + * - `pass` (test) test passed + * - `fail` (test, err) test failed + * + * @api public + */ + + function Runner(suite) { + var self = this; + this._globals = []; + this.suite = suite; + this.total = suite.total(); + this.failures = 0; + this.on('test end', function(test) { + self.checkGlobals(test); + }); + this.on('hook end', function(hook) { + self.checkGlobals(hook); + }); + this.grep(/.*/); + this.globals(this.globalProps().concat(['errno'])); + } + + /** + * Inherit from `EventEmitter.prototype`. + */ + + Runner.prototype = new EventEmitter(); + Runner.prototype.constructor = Runner; + + /** + * Run tests with full titles matching `re`. Updates runner.total + * with number of tests matched. + * + * @param {RegExp} re + * @param {Boolean} invert + * @return {Runner} for chaining + * @api public + */ + + Runner.prototype.grep = function(re, invert) { + debug('grep %s', re); + this._grep = re; + this._invert = invert; + this.total = this.grepTotal(this.suite); + return this; + }; + + /** + * Returns the number of tests matching the grep search for the + * given suite. + * + * @param {Suite} suite + * @return {Number} + * @api public + */ + + Runner.prototype.grepTotal = function(suite) { + var self = this; + var total = 0; + + suite.eachTest(function(test) { + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (match) total++; + }); + + return total; + }; + + /** + * Return a list of global properties. + * + * @return {Array} + * @api private + */ + + Runner.prototype.globalProps = function() { + var props = utils.keys(global); + + // non-enumerables + for (var i = 0; i < globals.length; ++i) { + if (~props.indexOf(globals[i])) continue; + props.push(globals[i]); + } + + return props; + }; + + /** + * Allow the given `arr` of globals. + * + * @param {Array} arr + * @return {Runner} for chaining + * @api public + */ + + Runner.prototype.globals = function(arr) { + if (0 == arguments.length) return this._globals; + debug('globals %j', arr); + utils.forEach( + arr, + function(arr) { + this._globals.push(arr); + }, + this + ); + return this; + }; + + /** + * Check for global variable leaks. + * + * @api private + */ + + Runner.prototype.checkGlobals = function(test) { + if (this.ignoreLeaks) return; + var ok = this._globals; + var globals = this.globalProps(); + var isNode = process.kill; + var leaks; + + // check length - 2 ('errno' and 'location' globals) + if (isNode && 1 == ok.length - globals.length) return; + else if (2 == ok.length - globals.length) return; + + leaks = filterLeaks(ok, globals); + this._globals = this._globals.concat(leaks); + + if (leaks.length > 1) { + this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + '')); + } else if (leaks.length) { + this.fail(test, new Error('global leak detected: ' + leaks[0])); + } + }; + + /** + * Fail the given `test`. + * + * @param {Test} test + * @param {Error} err + * @api private + */ + + Runner.prototype.fail = function(test, err) { + ++this.failures; + test.state = 'failed'; + + if ('string' == typeof err) { + err = new Error('the string "' + err + '" was thrown, throw an Error :)'); + } + + this.emit('fail', test, err); + }; + + /** + * Fail the given `hook` with `err`. + * + * Hook failures (currently) hard-end due + * to that fact that a failing hook will + * surely cause subsequent tests to fail, + * causing jumbled reporting. + * + * @param {Hook} hook + * @param {Error} err + * @api private + */ + + Runner.prototype.failHook = function(hook, err) { + this.fail(hook, err); + this.emit('end'); + }; + + /** + * Run hook `name` callbacks and then invoke `fn()`. + * + * @param {String} name + * @param {Function} function + * @api private + */ + + Runner.prototype.hook = function(name, fn) { + var suite = this.suite, + hooks = suite['_' + name], + self = this, + timer; + + function next(i) { + var hook = hooks[i]; + if (!hook) return fn(); + self.currentRunnable = hook; + + self.emit('hook', hook); + + hook.on('error', function(err) { + self.failHook(hook, err); + }); + + hook.run(function(err) { + hook.removeAllListeners('error'); + var testError = hook.error(); + if (testError) self.fail(self.test, testError); + if (err) return self.failHook(hook, err); + self.emit('hook end', hook); + next(++i); + }); + } + + process.nextTick(function() { + next(0); + }); + }; + + /** + * Run hook `name` for the given array of `suites` + * in order, and callback `fn(err)`. + * + * @param {String} name + * @param {Array} suites + * @param {Function} fn + * @api private + */ + + Runner.prototype.hooks = function(name, suites, fn) { + var self = this, + orig = this.suite; + + function next(suite) { + self.suite = suite; + + if (!suite) { + self.suite = orig; + return fn(); + } + + self.hook(name, function(err) { + if (err) { + self.suite = orig; + return fn(err); + } + + next(suites.pop()); + }); + } + + next(suites.pop()); + }; + + /** + * Run hooks from the top level down. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + + Runner.prototype.hookUp = function(name, fn) { + var suites = [this.suite].concat(this.parents()).reverse(); + this.hooks(name, suites, fn); + }; + + /** + * Run hooks from the bottom up. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + + Runner.prototype.hookDown = function(name, fn) { + var suites = [this.suite].concat(this.parents()); + this.hooks(name, suites, fn); + }; + + /** + * Return an array of parent Suites from + * closest to furthest. + * + * @return {Array} + * @api private + */ + + Runner.prototype.parents = function() { + var suite = this.suite, + suites = []; + while ((suite = suite.parent)) suites.push(suite); + return suites; + }; + + /** + * Run the current test and callback `fn(err)`. + * + * @param {Function} fn + * @api private + */ + + Runner.prototype.runTest = function(fn) { + var test = this.test, + self = this; + + if (this.asyncOnly) test.asyncOnly = true; + + try { + test.on('error', function(err) { + self.fail(test, err); + }); + test.run(fn); + } catch (err) { + fn(err); + } + }; + + /** + * Run tests in the given `suite` and invoke + * the callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + + Runner.prototype.runTests = function(suite, fn) { + var self = this, + tests = suite.tests.slice(), + test; - function indent() { - return Array(level).join(' '); - } + function next(err) { + // if we bail after first err + if (self.failures && suite._bail) return fn(); - function mapTOC(suite, obj) { - var ret = obj; - obj = obj[suite.title] = obj[suite.title] || { suite: suite }; - suite.suites.forEach(function(suite){ - mapTOC(suite, obj); - }); - return ret; - } + // next test + test = tests.shift(); - function stringifyTOC(obj, level) { - ++level; - var buf = ''; - var link; - for (var key in obj) { - if ('suite' == key) continue; - if (key) link = ' - [' + key + '](#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; - if (key) buf += Array(level).join(' ') + link; - buf += stringifyTOC(obj[key], level); - } - --level; - return buf; - } + // all done + if (!test) return fn(); - function generateTOC(suite) { - var obj = mapTOC(suite, {}); - return stringifyTOC(obj, 0); - } + // grep + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (!match) return next(); - generateTOC(runner.suite); - - runner.on('suite', function(suite){ - ++level; - var slug = utils.slug(suite.fullTitle()); - buf += '' + '\n'; - buf += title(suite.title) + '\n'; - }); - - runner.on('suite end', function(suite){ - --level; - }); - - runner.on('pass', function(test){ - var code = utils.clean(test.fn.toString()); - buf += test.title + '.\n'; - buf += '\n```js\n'; - buf += code + '\n'; - buf += '```\n\n'; - }); - - runner.on('end', function(){ - process.stdout.write('# TOC\n'); - process.stdout.write(generateTOC(runner.suite)); - process.stdout.write(buf); - }); -} -}); // module: reporters/markdown.js - -require.register("reporters/min.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base'); - -/** - * Expose `Min`. - */ - -exports = module.exports = Min; - -/** - * Initialize a new `Min` minimal test reporter (best used with --watch). - * - * @param {Runner} runner - * @api public - */ - -function Min(runner) { - Base.call(this, runner); - - runner.on('start', function(){ - // clear screen - process.stdout.write('\u001b[2J'); - // set cursor position - process.stdout.write('\u001b[1;3H'); - }); - - runner.on('end', this.epilogue.bind(this)); -} - -/** - * Inherit from `Base.prototype`. - */ - -Min.prototype = new Base; -Min.prototype.constructor = Min; - -}); // module: reporters/min.js - -require.register("reporters/nyan.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , color = Base.color; - -/** - * Expose `Dot`. - */ - -exports = module.exports = NyanCat; - -/** - * Initialize a new `Dot` matrix test reporter. - * - * @param {Runner} runner - * @api public - */ - -function NyanCat(runner) { - Base.call(this, runner); - - var self = this - , stats = this.stats - , width = Base.window.width * .75 | 0 - , rainbowColors = this.rainbowColors = self.generateColors() - , colorIndex = this.colorIndex = 0 - , numerOfLines = this.numberOfLines = 4 - , trajectories = this.trajectories = [[], [], [], []] - , nyanCatWidth = this.nyanCatWidth = 11 - , trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth) - , scoreboardWidth = this.scoreboardWidth = 5 - , tick = this.tick = 0 - , n = 0; - - runner.on('start', function(){ - Base.cursor.hide(); - self.draw('start'); - }); - - runner.on('pending', function(test){ - self.draw('pending'); - }); - - runner.on('pass', function(test){ - self.draw('pass'); - }); - - runner.on('fail', function(test, err){ - self.draw('fail'); - }); - - runner.on('end', function(){ - Base.cursor.show(); - for (var i = 0; i < self.numberOfLines; i++) write('\n'); - self.epilogue(); - }); -} - -/** - * Draw the nyan cat with runner `status`. - * - * @param {String} status - * @api private - */ - -NyanCat.prototype.draw = function(status){ - this.appendRainbow(); - this.drawScoreboard(); - this.drawRainbow(); - this.drawNyanCat(status); - this.tick = !this.tick; -}; - -/** - * Draw the "scoreboard" showing the number - * of passes, failures and pending tests. - * - * @api private - */ - -NyanCat.prototype.drawScoreboard = function(){ - var stats = this.stats; - var colors = Base.colors; - - function draw(color, n) { - write(' '); - write('\u001b[' + color + 'm' + n + '\u001b[0m'); - write('\n'); - } + // pending + if (test.pending) { + self.emit('pending', test); + self.emit('test end', test); + return next(); + } - draw(colors.green, stats.passes); - draw(colors.fail, stats.failures); - draw(colors.pending, stats.pending); - write('\n'); + // execute test and hook(s) + self.emit('test', (self.test = test)); + self.hookDown('beforeEach', function() { + self.currentRunnable = self.test; + self.runTest(function(err) { + test = self.test; + + if (err) { + self.fail(test, err); + self.emit('test end', test); + return self.hookUp('afterEach', next); + } + + test.state = 'passed'; + self.emit('pass', test); + self.emit('test end', test); + self.hookUp('afterEach', next); + }); + }); + } - this.cursorUp(this.numberOfLines); -}; + this.next = next; + next(); + }; -/** - * Append the rainbow. - * - * @api private - */ + /** + * Run the given `suite` and invoke the + * callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ -NyanCat.prototype.appendRainbow = function(){ - var segment = this.tick ? '_' : '-'; - var rainbowified = this.rainbowify(segment); + Runner.prototype.runSuite = function(suite, fn) { + var total = this.grepTotal(suite), + self = this, + i = 0; - for (var index = 0; index < this.numberOfLines; index++) { - var trajectory = this.trajectories[index]; - if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift(); - trajectory.push(rainbowified); - } -}; - -/** - * Draw the rainbow. - * - * @api private - */ - -NyanCat.prototype.drawRainbow = function(){ - var self = this; - - this.trajectories.forEach(function(line, index) { - write('\u001b[' + self.scoreboardWidth + 'C'); - write(line.join('')); - write('\n'); - }); - - this.cursorUp(this.numberOfLines); -}; - -/** - * Draw the nyan cat with `status`. - * - * @param {String} status - * @api private - */ - -NyanCat.prototype.drawNyanCat = function(status) { - var self = this; - var startWidth = this.scoreboardWidth + this.trajectories[0].length; - - [0, 1, 2, 3].forEach(function(index) { - write('\u001b[' + startWidth + 'C'); - - switch (index) { - case 0: - write('_,------,'); - write('\n'); - break; - case 1: - var padding = self.tick ? ' ' : ' '; - write('_|' + padding + '/\\_/\\ '); - write('\n'); - break; - case 2: - var padding = self.tick ? '_' : '__'; - var tail = self.tick ? '~' : '^'; - var face; - switch (status) { - case 'pass': - face = '( ^ .^)'; - break; - case 'fail': - face = '( o .o)'; - break; - default: - face = '( - .-)'; - } - write(tail + '|' + padding + face + ' '); - write('\n'); - break; - case 3: - var padding = self.tick ? ' ' : ' '; - write(padding + '"" "" '); - write('\n'); - break; - } - }); - - this.cursorUp(this.numberOfLines); -}; - -/** - * Move cursor up `n`. - * - * @param {Number} n - * @api private - */ - -NyanCat.prototype.cursorUp = function(n) { - write('\u001b[' + n + 'A'); -}; - -/** - * Move cursor down `n`. - * - * @param {Number} n - * @api private - */ - -NyanCat.prototype.cursorDown = function(n) { - write('\u001b[' + n + 'B'); -}; - -/** - * Generate rainbow colors. - * - * @return {Array} - * @api private - */ - -NyanCat.prototype.generateColors = function(){ - var colors = []; - - for (var i = 0; i < (6 * 7); i++) { - var pi3 = Math.floor(Math.PI / 3); - var n = (i * (1.0 / 6)); - var r = Math.floor(3 * Math.sin(n) + 3); - var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); - var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); - colors.push(36 * r + 6 * g + b + 16); - } + debug('run suite %s', suite.fullTitle()); - return colors; -}; - -/** - * Apply rainbow to the given `str`. - * - * @param {String} str - * @return {String} - * @api private - */ - -NyanCat.prototype.rainbowify = function(str){ - var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; - this.colorIndex += 1; - return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; -}; - -/** - * Stdout helper. - */ - -function write(string) { - process.stdout.write(string); -} - -/** - * Inherit from `Base.prototype`. - */ - -NyanCat.prototype = new Base; -NyanCat.prototype.constructor = NyanCat; - - -}); // module: reporters/nyan.js - -require.register("reporters/progress.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , cursor = Base.cursor - , color = Base.color; - -/** - * Expose `Progress`. - */ - -exports = module.exports = Progress; - -/** - * General progress bar color. - */ - -Base.colors.progress = 90; - -/** - * Initialize a new `Progress` bar test reporter. - * - * @param {Runner} runner - * @param {Object} options - * @api public - */ - -function Progress(runner, options) { - Base.call(this, runner); - - var self = this - , options = options || {} - , stats = this.stats - , width = Base.window.width * .50 | 0 - , total = runner.total - , complete = 0 - , max = Math.max; - - // default chars - options.open = options.open || '['; - options.complete = options.complete || '▬'; - options.incomplete = options.incomplete || Base.symbols.dot; - options.close = options.close || ']'; - options.verbose = false; - - // tests started - runner.on('start', function(){ - console.log(); - cursor.hide(); - }); - - // tests complete - runner.on('test end', function(){ - complete++; - var incomplete = total - complete - , percent = complete / total - , n = width * percent | 0 - , i = width - n; - - cursor.CR(); - process.stdout.write('\u001b[J'); - process.stdout.write(color('progress', ' ' + options.open)); - process.stdout.write(Array(n).join(options.complete)); - process.stdout.write(Array(i).join(options.incomplete)); - process.stdout.write(color('progress', options.close)); - if (options.verbose) { - process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); - } - }); + if (!total) return fn(); - // tests are complete, output some stats - // and the failures if any - runner.on('end', function(){ - cursor.show(); - console.log(); - self.epilogue(); - }); -} + this.emit('suite', (this.suite = suite)); -/** - * Inherit from `Base.prototype`. - */ + function next() { + var curr = suite.suites[i++]; + if (!curr) return done(); + self.runSuite(curr, next); + } + + function done() { + self.suite = suite; + self.hook('afterAll', function() { + self.emit('suite end', suite); + fn(); + }); + } -Progress.prototype = new Base; -Progress.prototype.constructor = Progress; + this.hook('beforeAll', function() { + self.runTests(suite, next); + }); + }; + /** + * Handle uncaught exceptions. + * + * @param {Error} err + * @api private + */ -}); // module: reporters/progress.js + Runner.prototype.uncaught = function(err) { + debug('uncaught exception %s', err.message); + var runnable = this.currentRunnable; + if (!runnable || 'failed' == runnable.state) return; + runnable.clearTimeout(); + err.uncaught = true; + this.fail(runnable, err); + + // recover from test + if ('test' == runnable.type) { + this.emit('test end', runnable); + this.hookUp('afterEach', this.next); + return; + } -require.register("reporters/spec.js", function(module, exports, require){ + // bail on hooks + this.emit('end'); + }; -/** - * Module dependencies. - */ + /** + * Run the root suite and invoke `fn(failures)` + * on completion. + * + * @param {Function} fn + * @return {Runner} for chaining + * @api public + */ -var Base = require('./base') - , cursor = Base.cursor - , color = Base.color; + Runner.prototype.run = function(fn) { + var self = this, + fn = fn || function() {}; -/** - * Expose `Spec`. - */ + debug('start'); -exports = module.exports = Spec; + // callback + this.on('end', function() { + debug('end'); + process.removeListener('uncaughtException', self.uncaught.bind(self)); + fn(self.failures); + }); -/** - * Initialize a new `Spec` test reporter. - * - * @param {Runner} runner - * @api public - */ + // run suites + this.emit('start'); + this.runSuite(this.suite, function() { + debug('finished running'); + self.emit('end'); + }); -function Spec(runner) { - Base.call(this, runner); + // uncaught exception + process.on('uncaughtException', this.uncaught.bind(this)); - var self = this - , stats = this.stats - , indents = 0 - , n = 0; + return this; + }; - function indent() { - return Array(indents).join(' ') - } + /** + * Filter leaks with the given globals flagged as `ok`. + * + * @param {Array} ok + * @param {Array} globals + * @return {Array} + * @api private + */ - runner.on('start', function(){ - console.log(); - }); - - runner.on('suite', function(suite){ - ++indents; - console.log(color('suite', '%s%s'), indent(), suite.title); - }); - - runner.on('suite end', function(suite){ - --indents; - if (1 == indents) console.log(); - }); - - runner.on('test', function(test){ - process.stdout.write(indent() + color('pass', ' ◦ ' + test.title + ': ')); - }); - - runner.on('pending', function(test){ - var fmt = indent() + color('pending', ' - %s'); - console.log(fmt, test.title); - }); - - runner.on('pass', function(test){ - if ('fast' == test.speed) { - var fmt = indent() - + color('checkmark', ' ' + Base.symbols.ok) - + color('pass', ' %s '); - cursor.CR(); - console.log(fmt, test.title); - } else { - var fmt = indent() - + color('checkmark', ' ' + Base.symbols.ok) - + color('pass', ' %s ') - + color(test.speed, '(%dms)'); - cursor.CR(); - console.log(fmt, test.title, test.duration); + function filterLeaks(ok, globals) { + return filter(globals, function(key) { + var matched = filter(ok, function(ok) { + if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]); + // Opera and IE expose global variables for HTML element IDs (issue #243) + if (/^mocha-/.test(key)) return true; + return key == ok; + }); + return matched.length == 0 && (!global.navigator || 'onerror' !== key); + }); } - }); + }); // module: runner.js + + require.register('suite.js', function(module, exports, require) { + /** + * Module dependencies. + */ + + var EventEmitter = require('browser/events').EventEmitter, + debug = require('browser/debug')('mocha:suite'), + milliseconds = require('./ms'), + utils = require('./utils'), + Hook = require('./hook'); - runner.on('fail', function(test, err){ - cursor.CR(); - console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); - }); + /** + * Expose `Suite`. + */ - runner.on('end', self.epilogue.bind(self)); -} + exports = module.exports = Suite; -/** - * Inherit from `Base.prototype`. - */ + /** + * Create a new `Suite` with the given `title` + * and parent `Suite`. When a suite with the + * same title is already present, that suite + * is returned to provide nicer reporter + * and more flexible meta-testing. + * + * @param {Suite} parent + * @param {String} title + * @return {Suite} + * @api public + */ -Spec.prototype = new Base; -Spec.prototype.constructor = Spec; + exports.create = function(parent, title) { + var suite = new Suite(title, parent.ctx); + suite.parent = parent; + if (parent.pending) suite.pending = true; + title = suite.fullTitle(); + parent.addSuite(suite); + return suite; + }; + /** + * Initialize a new `Suite` with the given + * `title` and `ctx`. + * + * @param {String} title + * @param {Context} ctx + * @api private + */ -}); // module: reporters/spec.js + function Suite(title, ctx) { + this.title = title; + this.ctx = ctx; + this.suites = []; + this.tests = []; + this.pending = false; + this._beforeEach = []; + this._beforeAll = []; + this._afterEach = []; + this._afterAll = []; + this.root = !title; + this._timeout = 2000; + this._slow = 75; + this._bail = false; + } -require.register("reporters/tap.js", function(module, exports, require){ + /** + * Inherit from `EventEmitter.prototype`. + */ -/** - * Module dependencies. - */ + Suite.prototype = new EventEmitter(); + Suite.prototype.constructor = Suite; -var Base = require('./base') - , cursor = Base.cursor - , color = Base.color; + /** + * Return a clone of this `Suite`. + * + * @return {Suite} + * @api private + */ -/** - * Expose `TAP`. - */ + Suite.prototype.clone = function() { + var suite = new Suite(this.title); + debug('clone'); + suite.ctx = this.ctx; + suite.timeout(this.timeout()); + suite.slow(this.slow()); + suite.bail(this.bail()); + return suite; + }; -exports = module.exports = TAP; + /** + * Set timeout `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ -/** - * Initialize a new `TAP` reporter. - * - * @param {Runner} runner - * @api public - */ + Suite.prototype.timeout = function(ms) { + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = parseInt(ms, 10); + return this; + }; -function TAP(runner) { - Base.call(this, runner); + /** + * Set slow `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ - var self = this - , stats = this.stats - , n = 1; + Suite.prototype.slow = function(ms) { + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('slow %d', ms); + this._slow = ms; + return this; + }; - runner.on('start', function(){ - var total = runner.grepTotal(runner.suite); - console.log('%d..%d', 1, total); - }); + /** + * Sets whether to bail after first error. + * + * @parma {Boolean} bail + * @return {Suite|Number} for chaining + * @api private + */ - runner.on('test end', function(){ - ++n; - }); + Suite.prototype.bail = function(bail) { + if (0 == arguments.length) return this._bail; + debug('bail %s', bail); + this._bail = bail; + return this; + }; - runner.on('pending', function(test){ - console.log('ok %d %s # SKIP -', n, title(test)); - }); + /** + * Run `fn(test[, done])` before running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ - runner.on('pass', function(test){ - console.log('ok %d %s', n, title(test)); - }); + Suite.prototype.beforeAll = function(fn) { + if (this.pending) return this; + var hook = new Hook('"before all" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeAll.push(hook); + this.emit('beforeAll', hook); + return this; + }; - runner.on('fail', function(test, err){ - console.log('not ok %d %s', n, title(test)); - console.log(err.stack.replace(/^/gm, ' ')); - }); -} + /** + * Run `fn(test[, done])` after running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ -/** - * Return a TAP-safe title of `test` - * - * @param {Object} test - * @return {String} - * @api private - */ + Suite.prototype.afterAll = function(fn) { + if (this.pending) return this; + var hook = new Hook('"after all" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterAll.push(hook); + this.emit('afterAll', hook); + return this; + }; -function title(test) { - return test.fullTitle().replace(/#/g, ''); -} + /** + * Run `fn(test[, done])` before each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ -}); // module: reporters/tap.js + Suite.prototype.beforeEach = function(fn) { + if (this.pending) return this; + var hook = new Hook('"before each" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeEach.push(hook); + this.emit('beforeEach', hook); + return this; + }; -require.register("reporters/teamcity.js", function(module, exports, require){ + /** + * Run `fn(test[, done])` after each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ -/** - * Module dependencies. - */ - -var Base = require('./base'); + Suite.prototype.afterEach = function(fn) { + if (this.pending) return this; + var hook = new Hook('"after each" hook', fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterEach.push(hook); + this.emit('afterEach', hook); + return this; + }; -/** - * Expose `Teamcity`. - */ - -exports = module.exports = Teamcity; - -/** - * Initialize a new `Teamcity` reporter. - * - * @param {Runner} runner - * @api public - */ - -function Teamcity(runner) { - Base.call(this, runner); - var stats = this.stats; - - runner.on('start', function() { - console.log("##teamcity[testSuiteStarted name='mocha.suite']"); - }); - - runner.on('test', function(test) { - console.log("##teamcity[testStarted name='" + escape(test.fullTitle()) + "']"); - }); - - runner.on('fail', function(test, err) { - console.log("##teamcity[testFailed name='" + escape(test.fullTitle()) + "' message='" + escape(err.message) + "']"); - }); - - runner.on('pending', function(test) { - console.log("##teamcity[testIgnored name='" + escape(test.fullTitle()) + "' message='pending']"); - }); - - runner.on('test end', function(test) { - console.log("##teamcity[testFinished name='" + escape(test.fullTitle()) + "' duration='" + test.duration + "']"); - }); - - runner.on('end', function() { - console.log("##teamcity[testSuiteFinished name='mocha.suite' duration='" + stats.duration + "']"); - }); -} - -/** - * Escape the given `str`. - */ - -function escape(str) { - return str - .replace(/\|/g, "||") - .replace(/\n/g, "|n") - .replace(/\r/g, "|r") - .replace(/\[/g, "|[") - .replace(/\]/g, "|]") - .replace(/\u0085/g, "|x") - .replace(/\u2028/g, "|l") - .replace(/\u2029/g, "|p") - .replace(/'/g, "|'"); -} - -}); // module: reporters/teamcity.js - -require.register("reporters/xunit.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Base = require('./base') - , utils = require('../utils') - , escape = utils.escape; - -/** - * Save timer references to avoid Sinon interfering (see GH-237). - */ - -var Date = global.Date - , setTimeout = global.setTimeout - , setInterval = global.setInterval - , clearTimeout = global.clearTimeout - , clearInterval = global.clearInterval; - -/** - * Expose `XUnit`. - */ - -exports = module.exports = XUnit; - -/** - * Initialize a new `XUnit` reporter. - * - * @param {Runner} runner - * @api public - */ - -function XUnit(runner) { - Base.call(this, runner); - var stats = this.stats - , tests = [] - , self = this; - - runner.on('pass', function(test){ - tests.push(test); - }); - - runner.on('fail', function(test){ - tests.push(test); - }); - - runner.on('end', function(){ - console.log(tag('testsuite', { - name: 'Mocha Tests' - , tests: stats.tests - , failures: stats.failures - , errors: stats.failures - , skip: stats.tests - stats.failures - stats.passes - , timestamp: (new Date).toUTCString() - , time: stats.duration / 1000 - }, false)); - - tests.forEach(test); - console.log(''); - }); -} - -/** - * Inherit from `Base.prototype`. - */ - -XUnit.prototype = new Base; -XUnit.prototype.constructor = XUnit; - - -/** - * Output tag for the given `test.` - */ - -function test(test) { - var attrs = { - classname: test.parent.fullTitle() - , name: test.title - , time: test.duration / 1000 - }; + /** + * Add a test `suite`. + * + * @param {Suite} suite + * @return {Suite} for chaining + * @api private + */ - if ('failed' == test.state) { - var err = test.err; - attrs.message = escape(err.message); - console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); - } else if (test.pending) { - console.log(tag('testcase', attrs, false, tag('skipped', {}, true))); - } else { - console.log(tag('testcase', attrs, true) ); - } -} + Suite.prototype.addSuite = function(suite) { + suite.parent = this; + suite.timeout(this.timeout()); + suite.slow(this.slow()); + suite.bail(this.bail()); + this.suites.push(suite); + this.emit('suite', suite); + return this; + }; -/** - * HTML tag helper. - */ + /** + * Add a `test` to this suite. + * + * @param {Test} test + * @return {Suite} for chaining + * @api private + */ -function tag(name, attrs, close, content) { - var end = close ? '/>' : '>' - , pairs = [] - , tag; + Suite.prototype.addTest = function(test) { + test.parent = this; + test.timeout(this.timeout()); + test.slow(this.slow()); + test.ctx = this.ctx; + this.tests.push(test); + this.emit('test', test); + return this; + }; - for (var key in attrs) { - pairs.push(key + '="' + escape(attrs[key]) + '"'); - } + /** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ - tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; - if (content) tag += content + ''; -} - -}); // module: reporters/xunit.js - -require.register("runnable.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var EventEmitter = require('browser/events').EventEmitter - , debug = require('browser/debug')('mocha:runnable') - , milliseconds = require('./ms'); - -/** - * Save timer references to avoid Sinon interfering (see GH-237). - */ - -var Date = global.Date - , setTimeout = global.setTimeout - , setInterval = global.setInterval - , clearTimeout = global.clearTimeout - , clearInterval = global.clearInterval; - -/** - * Object#toString(). - */ - -var toString = Object.prototype.toString; - -/** - * Expose `Runnable`. - */ - -module.exports = Runnable; - -/** - * Initialize a new `Runnable` with the given `title` and callback `fn`. - * - * @param {String} title - * @param {Function} fn - * @api private - */ - -function Runnable(title, fn) { - this.title = title; - this.fn = fn; - this.async = fn && fn.length; - this.sync = ! this.async; - this._timeout = 2000; - this._slow = 75; - this.timedOut = false; -} - -/** - * Inherit from `EventEmitter.prototype`. - */ - -Runnable.prototype = new EventEmitter; -Runnable.prototype.constructor = Runnable; - - -/** - * Set & get timeout `ms`. - * - * @param {Number|String} ms - * @return {Runnable|Number} ms or self - * @api private - */ - -Runnable.prototype.timeout = function(ms){ - if (0 == arguments.length) return this._timeout; - if ('string' == typeof ms) ms = milliseconds(ms); - debug('timeout %d', ms); - this._timeout = ms; - if (this.timer) this.resetTimeout(); - return this; -}; - -/** - * Set & get slow `ms`. - * - * @param {Number|String} ms - * @return {Runnable|Number} ms or self - * @api private - */ - -Runnable.prototype.slow = function(ms){ - if (0 === arguments.length) return this._slow; - if ('string' == typeof ms) ms = milliseconds(ms); - debug('timeout %d', ms); - this._slow = ms; - return this; -}; - -/** - * Return the full title generated by recursively - * concatenating the parent's full title. - * - * @return {String} - * @api public - */ - -Runnable.prototype.fullTitle = function(){ - return this.parent.fullTitle() + ' ' + this.title; -}; - -/** - * Clear the timeout. - * - * @api private - */ - -Runnable.prototype.clearTimeout = function(){ - clearTimeout(this.timer); -}; - -/** - * Inspect the runnable void of private properties. - * - * @return {String} - * @api private - */ - -Runnable.prototype.inspect = function(){ - return JSON.stringify(this, function(key, val){ - if ('_' == key[0]) return; - if ('parent' == key) return '#'; - if ('ctx' == key) return '#'; - return val; - }, 2); -}; - -/** - * Reset the timeout. - * - * @api private - */ - -Runnable.prototype.resetTimeout = function(){ - var self = this - , ms = this.timeout(); - - this.clearTimeout(); - if (ms) { - this.timer = setTimeout(function(){ - self.callback(new Error('timeout of ' + ms + 'ms exceeded')); - self.timedOut = true; - }, ms); - } -}; - -/** - * Run the test and invoke `fn(err)`. - * - * @param {Function} fn - * @api private - */ - -Runnable.prototype.run = function(fn){ - var self = this - , ms = this.timeout() - , start = new Date - , ctx = this.ctx - , finished - , emitted; - - if (ctx) ctx.runnable(this); - - // timeout - if (this.async) { - if (ms) { - this.timer = setTimeout(function(){ - done(new Error('timeout of ' + ms + 'ms exceeded')); - self.timedOut = true; - }, ms); - } - } + Suite.prototype.fullTitle = function() { + if (this.parent) { + var full = this.parent.fullTitle(); + if (full) return full + ' ' + this.title; + } + return this.title; + }; - // called multiple times - function multiple(err) { - if (emitted) return; - emitted = true; - self.emit('error', err || new Error('done() called multiple times')); - } + /** + * Return the total number of tests. + * + * @return {Number} + * @api public + */ - // finished - function done(err) { - if (self.timedOut) return; - if (finished) return multiple(err); - self.clearTimeout(); - self.duration = new Date - start; - finished = true; - fn(err); - } + Suite.prototype.total = function() { + return ( + utils.reduce( + this.suites, + function(sum, suite) { + return sum + suite.total(); + }, + 0 + ) + this.tests.length + ); + }; - // for .resetTimeout() - this.callback = done; + /** + * Iterates through each suite recursively to find + * all tests. Applies a function in the format + * `fn(test)`. + * + * @param {Function} fn + * @return {Suite} + * @api private + */ - // async - if (this.async) { - try { - this.fn.call(ctx, function(err){ - if (toString.call(err) === "[object Error]") return done(err); - if (null != err) return done(new Error('done() invoked with non-Error: ' + err)); - done(); + Suite.prototype.eachTest = function(fn) { + utils.forEach(this.tests, fn); + utils.forEach(this.suites, function(suite) { + suite.eachTest(fn); }); - } catch (err) { - done(err); - } - return; - } + return this; + }; + }); // module: suite.js - if (this.asyncOnly) { - return done(new Error('--async-only option in use without declaring `done()`')); - } + require.register('test.js', function(module, exports, require) { + /** + * Module dependencies. + */ - // sync - try { - if (!this.pending) this.fn.call(ctx); - this.duration = new Date - start; - fn(); - } catch (err) { - fn(err); - } -}; - -}); // module: runnable.js - -require.register("runner.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var EventEmitter = require('browser/events').EventEmitter - , debug = require('browser/debug')('mocha:runner') - , Test = require('./test') - , utils = require('./utils') - , filter = utils.filter - , keys = utils.keys - , noop = function(){}; - -/** - * Non-enumerable globals. - */ - -var globals = [ - 'setTimeout', - 'clearTimeout', - 'setInterval', - 'clearInterval', - 'XMLHttpRequest', - 'Date' -]; - -/** - * Expose `Runner`. - */ - -module.exports = Runner; - -/** - * Initialize a `Runner` for the given `suite`. - * - * Events: - * - * - `start` execution started - * - `end` execution complete - * - `suite` (suite) test suite execution started - * - `suite end` (suite) all tests (and sub-suites) have finished - * - `test` (test) test execution started - * - `test end` (test) test completed - * - `hook` (hook) hook execution started - * - `hook end` (hook) hook complete - * - `pass` (test) test passed - * - `fail` (test, err) test failed - * - * @api public - */ - -function Runner(suite) { - var self = this; - this._globals = []; - this.suite = suite; - this.total = suite.total(); - this.failures = 0; - this.on('test end', function(test){ self.checkGlobals(test); }); - this.on('hook end', function(hook){ self.checkGlobals(hook); }); - this.grep(/.*/); - this.globals(this.globalProps().concat(['errno'])); -} - -/** - * Inherit from `EventEmitter.prototype`. - */ - -Runner.prototype = new EventEmitter; -Runner.prototype.constructor = Runner; - - -/** - * Run tests with full titles matching `re`. Updates runner.total - * with number of tests matched. - * - * @param {RegExp} re - * @param {Boolean} invert - * @return {Runner} for chaining - * @api public - */ - -Runner.prototype.grep = function(re, invert){ - debug('grep %s', re); - this._grep = re; - this._invert = invert; - this.total = this.grepTotal(this.suite); - return this; -}; - -/** - * Returns the number of tests matching the grep search for the - * given suite. - * - * @param {Suite} suite - * @return {Number} - * @api public - */ - -Runner.prototype.grepTotal = function(suite) { - var self = this; - var total = 0; - - suite.eachTest(function(test){ - var match = self._grep.test(test.fullTitle()); - if (self._invert) match = !match; - if (match) total++; - }); - - return total; -}; - -/** - * Return a list of global properties. - * - * @return {Array} - * @api private - */ - -Runner.prototype.globalProps = function() { - var props = utils.keys(global); - - // non-enumerables - for (var i = 0; i < globals.length; ++i) { - if (~props.indexOf(globals[i])) continue; - props.push(globals[i]); - } + var Runnable = require('./runnable'); - return props; -}; - -/** - * Allow the given `arr` of globals. - * - * @param {Array} arr - * @return {Runner} for chaining - * @api public - */ - -Runner.prototype.globals = function(arr){ - if (0 == arguments.length) return this._globals; - debug('globals %j', arr); - utils.forEach(arr, function(arr){ - this._globals.push(arr); - }, this); - return this; -}; - -/** - * Check for global variable leaks. - * - * @api private - */ - -Runner.prototype.checkGlobals = function(test){ - if (this.ignoreLeaks) return; - var ok = this._globals; - var globals = this.globalProps(); - var isNode = process.kill; - var leaks; - - // check length - 2 ('errno' and 'location' globals) - if (isNode && 1 == ok.length - globals.length) return - else if (2 == ok.length - globals.length) return; - - leaks = filterLeaks(ok, globals); - this._globals = this._globals.concat(leaks); - - if (leaks.length > 1) { - this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + '')); - } else if (leaks.length) { - this.fail(test, new Error('global leak detected: ' + leaks[0])); - } -}; - -/** - * Fail the given `test`. - * - * @param {Test} test - * @param {Error} err - * @api private - */ - -Runner.prototype.fail = function(test, err){ - ++this.failures; - test.state = 'failed'; - - if ('string' == typeof err) { - err = new Error('the string "' + err + '" was thrown, throw an Error :)'); - } - - this.emit('fail', test, err); -}; - -/** - * Fail the given `hook` with `err`. - * - * Hook failures (currently) hard-end due - * to that fact that a failing hook will - * surely cause subsequent tests to fail, - * causing jumbled reporting. - * - * @param {Hook} hook - * @param {Error} err - * @api private - */ - -Runner.prototype.failHook = function(hook, err){ - this.fail(hook, err); - this.emit('end'); -}; - -/** - * Run hook `name` callbacks and then invoke `fn()`. - * - * @param {String} name - * @param {Function} function - * @api private - */ - -Runner.prototype.hook = function(name, fn){ - var suite = this.suite - , hooks = suite['_' + name] - , self = this - , timer; - - function next(i) { - var hook = hooks[i]; - if (!hook) return fn(); - self.currentRunnable = hook; - - self.emit('hook', hook); - - hook.on('error', function(err){ - self.failHook(hook, err); - }); + /** + * Expose `Test`. + */ - hook.run(function(err){ - hook.removeAllListeners('error'); - var testError = hook.error(); - if (testError) self.fail(self.test, testError); - if (err) return self.failHook(hook, err); - self.emit('hook end', hook); - next(++i); - }); - } + module.exports = Test; - process.nextTick(function(){ - next(0); - }); -}; - -/** - * Run hook `name` for the given array of `suites` - * in order, and callback `fn(err)`. - * - * @param {String} name - * @param {Array} suites - * @param {Function} fn - * @api private - */ - -Runner.prototype.hooks = function(name, suites, fn){ - var self = this - , orig = this.suite; - - function next(suite) { - self.suite = suite; - - if (!suite) { - self.suite = orig; - return fn(); + /** + * Initialize a new `Test` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + + function Test(title, fn) { + Runnable.call(this, title, fn); + this.pending = !fn; + this.type = 'test'; } - self.hook(name, function(err){ - if (err) { - self.suite = orig; - return fn(err); - } + /** + * Inherit from `Runnable.prototype`. + */ - next(suites.pop()); - }); - } + Test.prototype = new Runnable(); + Test.prototype.constructor = Test; + }); // module: test.js - next(suites.pop()); -}; - -/** - * Run hooks from the top level down. - * - * @param {String} name - * @param {Function} fn - * @api private - */ - -Runner.prototype.hookUp = function(name, fn){ - var suites = [this.suite].concat(this.parents()).reverse(); - this.hooks(name, suites, fn); -}; - -/** - * Run hooks from the bottom up. - * - * @param {String} name - * @param {Function} fn - * @api private - */ - -Runner.prototype.hookDown = function(name, fn){ - var suites = [this.suite].concat(this.parents()); - this.hooks(name, suites, fn); -}; - -/** - * Return an array of parent Suites from - * closest to furthest. - * - * @return {Array} - * @api private - */ - -Runner.prototype.parents = function(){ - var suite = this.suite - , suites = []; - while (suite = suite.parent) suites.push(suite); - return suites; -}; - -/** - * Run the current test and callback `fn(err)`. - * - * @param {Function} fn - * @api private - */ - -Runner.prototype.runTest = function(fn){ - var test = this.test - , self = this; - - if (this.asyncOnly) test.asyncOnly = true; - - try { - test.on('error', function(err){ - self.fail(test, err); - }); - test.run(fn); - } catch (err) { - fn(err); - } -}; - -/** - * Run tests in the given `suite` and invoke - * the callback `fn()` when complete. - * - * @param {Suite} suite - * @param {Function} fn - * @api private - */ - -Runner.prototype.runTests = function(suite, fn){ - var self = this - , tests = suite.tests.slice() - , test; - - function next(err) { - // if we bail after first err - if (self.failures && suite._bail) return fn(); - - // next test - test = tests.shift(); - - // all done - if (!test) return fn(); - - // grep - var match = self._grep.test(test.fullTitle()); - if (self._invert) match = !match; - if (!match) return next(); - - // pending - if (test.pending) { - self.emit('pending', test); - self.emit('test end', test); - return next(); - } + require.register('utils.js', function(module, exports, require) { + /** + * Module dependencies. + */ - // execute test and hook(s) - self.emit('test', self.test = test); - self.hookDown('beforeEach', function(){ - self.currentRunnable = self.test; - self.runTest(function(err){ - test = self.test; + var fs = require('browser/fs'), + path = require('browser/path'), + join = path.join, + debug = require('browser/debug')('mocha:watch'); - if (err) { - self.fail(test, err); - self.emit('test end', test); - return self.hookUp('afterEach', next); - } + /** + * Ignored directories. + */ - test.state = 'passed'; - self.emit('pass', test); - self.emit('test end', test); - self.hookUp('afterEach', next); - }); - }); - } + var ignore = ['node_modules', '.git']; - this.next = next; - next(); -}; + /** + * Escape special characters in the given string of html. + * + * @param {String} html + * @return {String} + * @api private + */ -/** - * Run the given `suite` and invoke the - * callback `fn()` when complete. - * - * @param {Suite} suite - * @param {Function} fn - * @api private - */ + exports.escape = function(html) { + return String(html) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + }; -Runner.prototype.runSuite = function(suite, fn){ - var total = this.grepTotal(suite) - , self = this - , i = 0; + /** + * Array#forEach (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ - debug('run suite %s', suite.fullTitle()); + exports.forEach = function(arr, fn, scope) { + for (var i = 0, l = arr.length; i < l; i++) fn.call(scope, arr[i], i); + }; - if (!total) return fn(); + /** + * Array#indexOf (<=IE8) + * + * @parma {Array} arr + * @param {Object} obj to find index of + * @param {Number} start + * @api private + */ - this.emit('suite', this.suite = suite); + exports.indexOf = function(arr, obj, start) { + for (var i = start || 0, l = arr.length; i < l; i++) { + if (arr[i] === obj) return i; + } + return -1; + }; - function next() { - var curr = suite.suites[i++]; - if (!curr) return done(); - self.runSuite(curr, next); - } + /** + * Array#reduce (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} initial value + * @api private + */ - function done() { - self.suite = suite; - self.hook('afterAll', function(){ - self.emit('suite end', suite); - fn(); - }); - } + exports.reduce = function(arr, fn, val) { + var rval = val; - this.hook('beforeAll', function(){ - self.runTests(suite, next); - }); -}; - -/** - * Handle uncaught exceptions. - * - * @param {Error} err - * @api private - */ - -Runner.prototype.uncaught = function(err){ - debug('uncaught exception %s', err.message); - var runnable = this.currentRunnable; - if (!runnable || 'failed' == runnable.state) return; - runnable.clearTimeout(); - err.uncaught = true; - this.fail(runnable, err); - - // recover from test - if ('test' == runnable.type) { - this.emit('test end', runnable); - this.hookUp('afterEach', this.next); - return; - } + for (var i = 0, l = arr.length; i < l; i++) { + rval = fn(rval, arr[i], i, arr); + } - // bail on hooks - this.emit('end'); -}; - -/** - * Run the root suite and invoke `fn(failures)` - * on completion. - * - * @param {Function} fn - * @return {Runner} for chaining - * @api public - */ - -Runner.prototype.run = function(fn){ - var self = this - , fn = fn || function(){}; - - debug('start'); - - // callback - this.on('end', function(){ - debug('end'); - process.removeListener('uncaughtException', self.uncaught.bind(self)); - fn(self.failures); - }); - - // run suites - this.emit('start'); - this.runSuite(this.suite, function(){ - debug('finished running'); - self.emit('end'); - }); - - // uncaught exception - process.on('uncaughtException', this.uncaught.bind(this)); - - return this; -}; - -/** - * Filter leaks with the given globals flagged as `ok`. - * - * @param {Array} ok - * @param {Array} globals - * @return {Array} - * @api private - */ - -function filterLeaks(ok, globals) { - return filter(globals, function(key){ - var matched = filter(ok, function(ok){ - if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]); - // Opera and IE expose global variables for HTML element IDs (issue #243) - if (/^mocha-/.test(key)) return true; - return key == ok; - }); - return matched.length == 0 && (!global.navigator || 'onerror' !== key); - }); -} - -}); // module: runner.js - -require.register("suite.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var EventEmitter = require('browser/events').EventEmitter - , debug = require('browser/debug')('mocha:suite') - , milliseconds = require('./ms') - , utils = require('./utils') - , Hook = require('./hook'); - -/** - * Expose `Suite`. - */ - -exports = module.exports = Suite; - -/** - * Create a new `Suite` with the given `title` - * and parent `Suite`. When a suite with the - * same title is already present, that suite - * is returned to provide nicer reporter - * and more flexible meta-testing. - * - * @param {Suite} parent - * @param {String} title - * @return {Suite} - * @api public - */ - -exports.create = function(parent, title){ - var suite = new Suite(title, parent.ctx); - suite.parent = parent; - if (parent.pending) suite.pending = true; - title = suite.fullTitle(); - parent.addSuite(suite); - return suite; -}; - -/** - * Initialize a new `Suite` with the given - * `title` and `ctx`. - * - * @param {String} title - * @param {Context} ctx - * @api private - */ - -function Suite(title, ctx) { - this.title = title; - this.ctx = ctx; - this.suites = []; - this.tests = []; - this.pending = false; - this._beforeEach = []; - this._beforeAll = []; - this._afterEach = []; - this._afterAll = []; - this.root = !title; - this._timeout = 2000; - this._slow = 75; - this._bail = false; -} - -/** - * Inherit from `EventEmitter.prototype`. - */ - -Suite.prototype = new EventEmitter; -Suite.prototype.constructor = Suite; - - -/** - * Return a clone of this `Suite`. - * - * @return {Suite} - * @api private - */ - -Suite.prototype.clone = function(){ - var suite = new Suite(this.title); - debug('clone'); - suite.ctx = this.ctx; - suite.timeout(this.timeout()); - suite.slow(this.slow()); - suite.bail(this.bail()); - return suite; -}; - -/** - * Set timeout `ms` or short-hand such as "2s". - * - * @param {Number|String} ms - * @return {Suite|Number} for chaining - * @api private - */ - -Suite.prototype.timeout = function(ms){ - if (0 == arguments.length) return this._timeout; - if ('string' == typeof ms) ms = milliseconds(ms); - debug('timeout %d', ms); - this._timeout = parseInt(ms, 10); - return this; -}; - -/** - * Set slow `ms` or short-hand such as "2s". - * - * @param {Number|String} ms - * @return {Suite|Number} for chaining - * @api private - */ - -Suite.prototype.slow = function(ms){ - if (0 === arguments.length) return this._slow; - if ('string' == typeof ms) ms = milliseconds(ms); - debug('slow %d', ms); - this._slow = ms; - return this; -}; - -/** - * Sets whether to bail after first error. - * - * @parma {Boolean} bail - * @return {Suite|Number} for chaining - * @api private - */ - -Suite.prototype.bail = function(bail){ - if (0 == arguments.length) return this._bail; - debug('bail %s', bail); - this._bail = bail; - return this; -}; - -/** - * Run `fn(test[, done])` before running tests. - * - * @param {Function} fn - * @return {Suite} for chaining - * @api private - */ - -Suite.prototype.beforeAll = function(fn){ - if (this.pending) return this; - var hook = new Hook('"before all" hook', fn); - hook.parent = this; - hook.timeout(this.timeout()); - hook.slow(this.slow()); - hook.ctx = this.ctx; - this._beforeAll.push(hook); - this.emit('beforeAll', hook); - return this; -}; - -/** - * Run `fn(test[, done])` after running tests. - * - * @param {Function} fn - * @return {Suite} for chaining - * @api private - */ - -Suite.prototype.afterAll = function(fn){ - if (this.pending) return this; - var hook = new Hook('"after all" hook', fn); - hook.parent = this; - hook.timeout(this.timeout()); - hook.slow(this.slow()); - hook.ctx = this.ctx; - this._afterAll.push(hook); - this.emit('afterAll', hook); - return this; -}; - -/** - * Run `fn(test[, done])` before each test case. - * - * @param {Function} fn - * @return {Suite} for chaining - * @api private - */ - -Suite.prototype.beforeEach = function(fn){ - if (this.pending) return this; - var hook = new Hook('"before each" hook', fn); - hook.parent = this; - hook.timeout(this.timeout()); - hook.slow(this.slow()); - hook.ctx = this.ctx; - this._beforeEach.push(hook); - this.emit('beforeEach', hook); - return this; -}; - -/** - * Run `fn(test[, done])` after each test case. - * - * @param {Function} fn - * @return {Suite} for chaining - * @api private - */ - -Suite.prototype.afterEach = function(fn){ - if (this.pending) return this; - var hook = new Hook('"after each" hook', fn); - hook.parent = this; - hook.timeout(this.timeout()); - hook.slow(this.slow()); - hook.ctx = this.ctx; - this._afterEach.push(hook); - this.emit('afterEach', hook); - return this; -}; - -/** - * Add a test `suite`. - * - * @param {Suite} suite - * @return {Suite} for chaining - * @api private - */ - -Suite.prototype.addSuite = function(suite){ - suite.parent = this; - suite.timeout(this.timeout()); - suite.slow(this.slow()); - suite.bail(this.bail()); - this.suites.push(suite); - this.emit('suite', suite); - return this; -}; - -/** - * Add a `test` to this suite. - * - * @param {Test} test - * @return {Suite} for chaining - * @api private - */ - -Suite.prototype.addTest = function(test){ - test.parent = this; - test.timeout(this.timeout()); - test.slow(this.slow()); - test.ctx = this.ctx; - this.tests.push(test); - this.emit('test', test); - return this; -}; - -/** - * Return the full title generated by recursively - * concatenating the parent's full title. - * - * @return {String} - * @api public - */ - -Suite.prototype.fullTitle = function(){ - if (this.parent) { - var full = this.parent.fullTitle(); - if (full) return full + ' ' + this.title; - } - return this.title; -}; - -/** - * Return the total number of tests. - * - * @return {Number} - * @api public - */ - -Suite.prototype.total = function(){ - return utils.reduce(this.suites, function(sum, suite){ - return sum + suite.total(); - }, 0) + this.tests.length; -}; - -/** - * Iterates through each suite recursively to find - * all tests. Applies a function in the format - * `fn(test)`. - * - * @param {Function} fn - * @return {Suite} - * @api private - */ - -Suite.prototype.eachTest = function(fn){ - utils.forEach(this.tests, fn); - utils.forEach(this.suites, function(suite){ - suite.eachTest(fn); - }); - return this; -}; - -}); // module: suite.js - -require.register("test.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var Runnable = require('./runnable'); - -/** - * Expose `Test`. - */ - -module.exports = Test; - -/** - * Initialize a new `Test` with the given `title` and callback `fn`. - * - * @param {String} title - * @param {Function} fn - * @api private - */ - -function Test(title, fn) { - Runnable.call(this, title, fn); - this.pending = !fn; - this.type = 'test'; -} - -/** - * Inherit from `Runnable.prototype`. - */ - -Test.prototype = new Runnable; -Test.prototype.constructor = Test; - - -}); // module: test.js - -require.register("utils.js", function(module, exports, require){ - -/** - * Module dependencies. - */ - -var fs = require('browser/fs') - , path = require('browser/path') - , join = path.join - , debug = require('browser/debug')('mocha:watch'); - -/** - * Ignored directories. - */ - -var ignore = ['node_modules', '.git']; - -/** - * Escape special characters in the given string of html. - * - * @param {String} html - * @return {String} - * @api private - */ - -exports.escape = function(html){ - return String(html) - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(//g, '>'); -}; - -/** - * Array#forEach (<=IE8) - * - * @param {Array} array - * @param {Function} fn - * @param {Object} scope - * @api private - */ - -exports.forEach = function(arr, fn, scope){ - for (var i = 0, l = arr.length; i < l; i++) - fn.call(scope, arr[i], i); -}; - -/** - * Array#indexOf (<=IE8) - * - * @parma {Array} arr - * @param {Object} obj to find index of - * @param {Number} start - * @api private - */ - -exports.indexOf = function(arr, obj, start){ - for (var i = start || 0, l = arr.length; i < l; i++) { - if (arr[i] === obj) - return i; - } - return -1; -}; - -/** - * Array#reduce (<=IE8) - * - * @param {Array} array - * @param {Function} fn - * @param {Object} initial value - * @api private - */ - -exports.reduce = function(arr, fn, val){ - var rval = val; - - for (var i = 0, l = arr.length; i < l; i++) { - rval = fn(rval, arr[i], i, arr); - } + return rval; + }; - return rval; -}; + /** + * Array#filter (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @api private + */ -/** - * Array#filter (<=IE8) - * - * @param {Array} array - * @param {Function} fn - * @api private - */ + exports.filter = function(arr, fn) { + var ret = []; -exports.filter = function(arr, fn){ - var ret = []; + for (var i = 0, l = arr.length; i < l; i++) { + var val = arr[i]; + if (fn(val, i, arr)) ret.push(val); + } - for (var i = 0, l = arr.length; i < l; i++) { - var val = arr[i]; - if (fn(val, i, arr)) ret.push(val); - } + return ret; + }; - return ret; -}; + /** + * Object.keys (<=IE8) + * + * @param {Object} obj + * @return {Array} keys + * @api private + */ -/** - * Object.keys (<=IE8) - * - * @param {Object} obj - * @return {Array} keys - * @api private - */ + exports.keys = + Object.keys || + function(obj) { + var keys = [], + has = Object.prototype.hasOwnProperty; // for `window` on <=IE8 -exports.keys = Object.keys || function(obj) { - var keys = [] - , has = Object.prototype.hasOwnProperty // for `window` on <=IE8 + for (var key in obj) { + if (has.call(obj, key)) { + keys.push(key); + } + } - for (var key in obj) { - if (has.call(obj, key)) { - keys.push(key); - } - } + return keys; + }; - return keys; -}; - -/** - * Watch the given `files` for changes - * and invoke `fn(file)` on modification. - * - * @param {Array} files - * @param {Function} fn - * @api private - */ - -exports.watch = function(files, fn){ - var options = { interval: 100 }; - files.forEach(function(file){ - debug('file %s', file); - fs.watchFile(file, options, function(curr, prev){ - if (prev.mtime < curr.mtime) fn(file); - }); - }); -}; - -/** - * Ignored files. - */ - -function ignored(path){ - return !~ignore.indexOf(path); -} - -/** - * Lookup files in the given `dir`. - * - * @return {Array} - * @api private - */ - -exports.files = function(dir, ret){ - ret = ret || []; - - fs.readdirSync(dir) - .filter(ignored) - .forEach(function(path){ - path = join(dir, path); - if (fs.statSync(path).isDirectory()) { - exports.files(path, ret); - } else if (path.match(/\.(js|coffee)$/)) { - ret.push(path); - } - }); - - return ret; -}; - -/** - * Compute a slug from the given `str`. - * - * @param {String} str - * @return {String} - * @api private - */ - -exports.slug = function(str){ - return str - .toLowerCase() - .replace(/ +/g, '-') - .replace(/[^-\w]/g, ''); -}; - -/** - * Strip the function definition from `str`, - * and re-indent for pre whitespace. - */ - -exports.clean = function(str) { - str = str - .replace(/^function *\(.*\) *{/, '') - .replace(/\s+\}$/, ''); - - var spaces = str.match(/^\n?( *)/)[1].length - , re = new RegExp('^ {' + spaces + '}', 'gm'); - - str = str.replace(re, ''); - - return exports.trim(str); -}; - -/** - * Escape regular expression characters in `str`. - * - * @param {String} str - * @return {String} - * @api private - */ - -exports.escapeRegexp = function(str){ - return str.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); -}; - -/** - * Trim the given `str`. - * - * @param {String} str - * @return {String} - * @api private - */ - -exports.trim = function(str){ - return str.replace(/^\s+|\s+$/g, ''); -}; - -/** - * Parse the given `qs`. - * - * @param {String} qs - * @return {Object} - * @api private - */ - -exports.parseQuery = function(qs){ - return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){ - var i = pair.indexOf('=') - , key = pair.slice(0, i) - , val = pair.slice(++i); - - obj[key] = decodeURIComponent(val); - return obj; - }, {}); -}; - -/** - * Highlight the given string of `js`. - * - * @param {String} js - * @return {String} - * @api private - */ - -function highlight(js) { - return js - .replace(//g, '>') - .replace(/\/\/(.*)/gm, '//$1') - .replace(/('.*?')/gm, '$1') - .replace(/(\d+\.\d+)/gm, '$1') - .replace(/(\d+)/gm, '$1') - .replace(/\bnew *(\w+)/gm, 'new $1') - .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1') -} - -/** - * Highlight the contents of tag `name`. - * - * @param {String} name - * @api private - */ - -exports.highlightTags = function(name) { - var code = document.getElementsByTagName(name); - for (var i = 0, len = code.length; i < len; ++i) { - code[i].innerHTML = highlight(code[i].innerHTML); - } -}; - -}); // module: utils.js -/** - * Node shims. - * - * These are meant only to allow - * mocha.js to run untouched, not - * to allow running node code in - * the browser. - */ - -process = {}; -process.exit = function(status){}; -process.stdout = {}; -global = window; - -/** - * next tick implementation. - */ - -process.nextTick = (function(){ - // postMessage behaves badly on IE8 - if (window.ActiveXObject || !window.postMessage) { - return function(fn){ fn() }; - } + /** + * Watch the given `files` for changes + * and invoke `fn(file)` on modification. + * + * @param {Array} files + * @param {Function} fn + * @api private + */ - // based on setZeroTimeout by David Baron - // - http://dbaron.org/log/20100309-faster-timeouts - var timeouts = [] - , name = 'mocha-zero-timeout' + exports.watch = function(files, fn) { + var options = { interval: 100 }; + files.forEach(function(file) { + debug('file %s', file); + fs.watchFile(file, options, function(curr, prev) { + if (prev.mtime < curr.mtime) fn(file); + }); + }); + }; + + /** + * Ignored files. + */ - window.addEventListener('message', function(e){ - if (e.source == window && e.data == name) { - if (e.stopPropagation) e.stopPropagation(); - if (timeouts.length) timeouts.shift()(); + function ignored(path) { + return !~ignore.indexOf(path); } - }, true); - return function(fn){ - timeouts.push(fn); - window.postMessage(name, '*'); - } -})(); + /** + * Lookup files in the given `dir`. + * + * @return {Array} + * @api private + */ -/** - * Remove uncaughtException listener. - */ + exports.files = function(dir, ret) { + ret = ret || []; + + fs.readdirSync(dir) + .filter(ignored) + .forEach(function(path) { + path = join(dir, path); + if (fs.statSync(path).isDirectory()) { + exports.files(path, ret); + } else if (path.match(/\.(js|coffee)$/)) { + ret.push(path); + } + }); + + return ret; + }; -process.removeListener = function(e){ - if ('uncaughtException' == e) { - window.onerror = null; - } -}; + /** + * Compute a slug from the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + exports.slug = function(str) { + return str + .toLowerCase() + .replace(/ +/g, '-') + .replace(/[^-\w]/g, ''); + }; + + /** + * Strip the function definition from `str`, + * and re-indent for pre whitespace. + */ + + exports.clean = function(str) { + str = str.replace(/^function *\(.*\) *{/, '').replace(/\s+\}$/, ''); + + var spaces = str.match(/^\n?( *)/)[1].length, + re = new RegExp('^ {' + spaces + '}', 'gm'); -/** - * Implements uncaughtException listener. - */ + str = str.replace(re, ''); -process.on = function(e, fn){ - if ('uncaughtException' == e) { - window.onerror = function(err, url, line){ - fn(new Error(err + ' (' + url + ':' + line + ')')); + return exports.trim(str); + }; + + /** + * Escape regular expression characters in `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + exports.escapeRegexp = function(str) { + return str.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'); + }; + + /** + * Trim the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + exports.trim = function(str) { + return str.replace(/^\s+|\s+$/g, ''); + }; + + /** + * Parse the given `qs`. + * + * @param {String} qs + * @return {Object} + * @api private + */ + + exports.parseQuery = function(qs) { + return exports.reduce( + qs.replace('?', '').split('&'), + function(obj, pair) { + var i = pair.indexOf('='), + key = pair.slice(0, i), + val = pair.slice(++i); + + obj[key] = decodeURIComponent(val); + return obj; + }, + {} + ); }; - } -}; -// boot -;(function(){ + /** + * Highlight the given string of `js`. + * + * @param {String} js + * @return {String} + * @api private + */ + + function highlight(js) { + return js + .replace(//g, '>') + .replace(/\/\/(.*)/gm, '//$1') + .replace(/('.*?')/gm, '$1') + .replace(/(\d+\.\d+)/gm, '$1') + .replace(/(\d+)/gm, '$1') + .replace(/\bnew *(\w+)/gm, 'new $1') + .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1'); + } + /** + * Highlight the contents of tag `name`. + * + * @param {String} name + * @api private + */ + + exports.highlightTags = function(name) { + var code = document.getElementsByTagName(name); + for (var i = 0, len = code.length; i < len; ++i) { + code[i].innerHTML = highlight(code[i].innerHTML); + } + }; + }); // module: utils.js /** - * Expose mocha. + * Node shims. + * + * These are meant only to allow + * mocha.js to run untouched, not + * to allow running node code in + * the browser. */ - var Mocha = window.Mocha = require('mocha'), - mocha = window.mocha = new Mocha({ reporter: 'html' }); + process = {}; + process.exit = function(status) {}; + process.stdout = {}; + global = window; /** - * Override ui to ensure that the ui functions are initialized. - * Normally this would happen in Mocha.prototype.loadFiles. + * next tick implementation. */ - mocha.ui = function(ui){ - Mocha.prototype.ui.call(this, ui); - this.suite.emit('pre-require', window, null, this); - return this; - }; + process.nextTick = (function() { + // postMessage behaves badly on IE8 + if (window.ActiveXObject || !window.postMessage) { + return function(fn) { + fn(); + }; + } + + // based on setZeroTimeout by David Baron + // - http://dbaron.org/log/20100309-faster-timeouts + var timeouts = [], + name = 'mocha-zero-timeout'; + + window.addEventListener( + 'message', + function(e) { + if (e.source == window && e.data == name) { + if (e.stopPropagation) e.stopPropagation(); + if (timeouts.length) timeouts.shift()(); + } + }, + true + ); + + return function(fn) { + timeouts.push(fn); + window.postMessage(name, '*'); + }; + })(); /** - * Setup mocha with the given setting options. + * Remove uncaughtException listener. */ - mocha.setup = function(opts){ - if ('string' == typeof opts) opts = { ui: opts }; - for (var opt in opts) this[opt](opts[opt]); - return this; + process.removeListener = function(e) { + if ('uncaughtException' == e) { + window.onerror = null; + } }; /** - * Run mocha, returning the Runner. + * Implements uncaughtException listener. */ - mocha.run = function(fn){ - var options = mocha.options; - mocha.globals('location'); + process.on = function(e, fn) { + if ('uncaughtException' == e) { + window.onerror = function(err, url, line) { + fn(new Error(err + ' (' + url + ':' + line + ')')); + }; + } + }; + + // boot + (function() { + /** + * Expose mocha. + */ - var query = Mocha.utils.parseQuery(window.location.search || ''); - if (query.grep) mocha.grep(query.grep); - if (query.invert) mocha.invert(); + var Mocha = (window.Mocha = require('mocha')), + mocha = (window.mocha = new Mocha({ reporter: 'html' })); - return Mocha.prototype.run.call(mocha, function(){ - Mocha.utils.highlightTags('code'); - if (fn) fn(); - }); - }; + /** + * Override ui to ensure that the ui functions are initialized. + * Normally this would happen in Mocha.prototype.loadFiles. + */ + + mocha.ui = function(ui) { + Mocha.prototype.ui.call(this, ui); + this.suite.emit('pre-require', window, null, this); + return this; + }; + + /** + * Setup mocha with the given setting options. + */ + + mocha.setup = function(opts) { + if ('string' == typeof opts) opts = { ui: opts }; + for (var opt in opts) this[opt](opts[opt]); + return this; + }; + + /** + * Run mocha, returning the Runner. + */ + + mocha.run = function(fn) { + var options = mocha.options; + mocha.globals('location'); + + var query = Mocha.utils.parseQuery(window.location.search || ''); + if (query.grep) mocha.grep(query.grep); + if (query.invert) mocha.invert(); + + return Mocha.prototype.run.call(mocha, function() { + Mocha.utils.highlightTags('code'); + if (fn) fn(); + }); + }; + })(); })(); -})(); \ No newline at end of file diff --git a/packages/amplify-velocity-template/tests/runner/spec.js b/packages/amplify-velocity-template/tests/runner/spec.js index 64eb34908f..fdb12eb69a 100644 --- a/packages/amplify-velocity-template/tests/runner/spec.js +++ b/packages/amplify-velocity-template/tests/runner/spec.js @@ -1,1009 +1,964 @@ -describe('Compile', function(){ - +describe('Compile', function() { var render = Velocity.render; - function getContext(str, context, macros){ - var compile = new Compile(Parser.parse(str)) - compile.render(context, macros) - return compile.context + function getContext(str, context, macros) { + var compile = new Compile(Parser.parse(str)); + compile.render(context, macros); + return compile.context; } - describe('References', function(){ - - it('get/is method', function(){ - var vm = '$customer.getAddress()' - var vm1 = '$customer.get("Address") $customer.isAddress()' - - assert.equal('bar' , render(vm, {customer: {Address: "bar"}})) - assert.equal('bar bar', render(vm1, {customer: {Address: "bar"}})) - }) - - it('method with attribute', function(){ - var vm = '$foo().bar\n${foo().bar}' - assert.equal('hello\nhello', render(vm, { - foo: function(){ - return { bar: 'hello' } - } - })) - - assert.equal('foo', render('${foo()}', { - foo: function(){ - return 'foo' - } - })) - }) - - it('index notation', function(){ - var vm = '$foo[0] $foo[$i] $foo.get(1)' - assert.equal('bar haha haha', render(vm, {foo: ["bar", "haha"], i: 1})) - }) - - it('set method', function(){ - var vm = '$page.setTitle( "My Home Page" ).setname("haha")$page.Title $page.name' - assert.equal('My Home Page haha', render(vm, {page: {}})) - }) - - it('size method', function(){ - var vm = '$foo.bar.size()' - assert.equal('2', render(vm, {foo: {bar: [1, 2]}})) - assert.equal('2', render(vm, {foo: {bar: {a: 1, b: 3}}})) - - var vm2 = '#if($foo.bar.size()) ok #{else} nosize #end' - assert.equal(' nosize ', render(vm2, {foo: {bar: 123}})) - assert.equal(' nosize ', render(vm2, {foo: {}})) - }) - - it('quiet reference', function(){ - var vm = 'my email is $email' - var vmquiet = 'my email is $!email' - assert.equal(vm, render(vm)) - assert.equal('my email is ', render(vmquiet)) - }) - - it('silence all reference', function(){ - var vm = 'my email is $email' - - var compile = new Compile(Parser.parse(vm)) - assert.equal('my email is ', compile.render(null, null, true)) - }) - - it('this context keep correct, see #16', function(){ - var data = 'a = $a.get()' - Compile.Parser = Parser + describe('References', function() { + it('get/is method', function() { + var vm = '$customer.getAddress()'; + var vm1 = '$customer.get("Address") $customer.isAddress()'; + + assert.equal('bar', render(vm, { customer: { Address: 'bar' } })); + assert.equal('bar bar', render(vm1, { customer: { Address: 'bar' } })); + }); + + it('method with attribute', function() { + var vm = '$foo().bar\n${foo().bar}'; + assert.equal( + 'hello\nhello', + render(vm, { + foo: function() { + return { bar: 'hello' }; + }, + }) + ); + + assert.equal( + 'foo', + render('${foo()}', { + foo: function() { + return 'foo'; + }, + }) + ); + }); + + it('index notation', function() { + var vm = '$foo[0] $foo[$i] $foo.get(1)'; + assert.equal('bar haha haha', render(vm, { foo: ['bar', 'haha'], i: 1 })); + }); + + it('set method', function() { + var vm = '$page.setTitle( "My Home Page" ).setname("haha")$page.Title $page.name'; + assert.equal('My Home Page haha', render(vm, { page: {} })); + }); + + it('size method', function() { + var vm = '$foo.bar.size()'; + assert.equal('2', render(vm, { foo: { bar: [1, 2] } })); + assert.equal('2', render(vm, { foo: { bar: { a: 1, b: 3 } } })); + + var vm2 = '#if($foo.bar.size()) ok #{else} nosize #end'; + assert.equal(' nosize ', render(vm2, { foo: { bar: 123 } })); + assert.equal(' nosize ', render(vm2, { foo: {} })); + }); + + it('quiet reference', function() { + var vm = 'my email is $email'; + var vmquiet = 'my email is $!email'; + assert.equal(vm, render(vm)); + assert.equal('my email is ', render(vmquiet)); + }); + + it('silence all reference', function() { + var vm = 'my email is $email'; + + var compile = new Compile(Parser.parse(vm)); + assert.equal('my email is ', compile.render(null, null, true)); + }); + + it('this context keep correct, see #16', function() { + var data = 'a = $a.get()'; + Compile.Parser = Parser; function b(c) { - this.c = c + this.c = c; } b.prototype.get = function() { - var t = this.eval(" hello $name", {name: 'hanwen'}) - return this.c + t - } + var t = this.eval(' hello $name', { name: 'hanwen' }); + return this.c + t; + }; + + assert.equal('a = 1 hello hanwen', render(data, { a: new b(1) })); + }); - assert.equal('a = 1 hello hanwen', render(data, {a: new b(1)})) - }) + it('get variable form text', function() { + var vm = 'hello $user.getName().getFullName("hanwen")'; + var data = { '$user.getName().getFullName("hanwen")': 'world' }; - it('get variable form text', function(){ - var vm = 'hello $user.getName().getFullName("hanwen")' - var data = { '$user.getName().getFullName("hanwen")': 'world' } - - assert.equal('hello world', render(vm, data)) - }) + assert.equal('hello world', render(vm, data)); + }); - it('escape default', function(){ - var vm = '$name $name2 $cn $cn1' + it('escape default', function() { + var vm = '$name $name2 $cn $cn1'; var data = { name: 'hello world', name2: '&a', cn: '中文', - cn1: '中文' - } + cn1: '中文', + }; - var ret = 'hello world <i>&a 中文 <i>中文' - assert.equal(ret, render(vm, data)) - }) + var ret = 'hello world <i>&a 中文 <i>中文'; + assert.equal(ret, render(vm, data)); + }); - it('add custom ignore escape function', function(){ - var vm = '$noIgnore($name), $ignore($name)' - var expected = '<i>, ' + it('add custom ignore escape function', function() { + var vm = '$noIgnore($name), $ignore($name)'; + var expected = '<i>, '; - var compile = new Compile(Parser.parse(vm)) - compile.addIgnoreEscpape('ignore') + var compile = new Compile(Parser.parse(vm)); + compile.addIgnoreEscpape('ignore'); var context = { name: '', - noIgnore: function(name){ - return name + noIgnore: function(name) { + return name; }, - ignore: function(name){ + ignore: function(name) { return name; - } - } + }, + }; - var ret = compile.render(context) - assert.equal(expected, ret) - }) + var ret = compile.render(context); + assert.equal(expected, ret); + }); - it('config support', function(){ - var vm = '$foo($name)' - var expected = '' + it('config support', function() { + var vm = '$foo($name)'; + var expected = ''; - var compile = new Compile(Parser.parse(vm), { escape: false }) + var compile = new Compile(Parser.parse(vm), { escape: false }); var context = { name: '', - foo: function(name){ + foo: function(name) { return name; - } - } + }, + }; - var ret = compile.render(context) - assert.equal(expected, ret) - - compile = new Compile(Parser.parse(vm)) - ret = compile.render(context) - assert.equal('<i>', ret) - }) - }) - - describe('Set && Expression', function(){ - - it('set equal to reference', function(){ - var vm = '#set( $monkey = $bill ) ## variable reference' - assert.equal("hello", getContext(vm, {bill: 'hello'}).monkey) - }) - - it('empty map', function(){ - var vm = '#set($foo = {})' - assert.deepEqual({}, getContext(vm).foo) - }) - - it('#set array', function(){ - var vm = '#set($foo = []) #set($foo[0] = 12)' - assert.equal(12, getContext(vm).foo[0]) - }) - - it('set equal to literal', function(){ - var vm = "#set( $monkey.Friend = 'monica' ) ## string literal\n" + - '#set( $monkey.Number = 123 ) ##number literal' - assert.equal("monica", getContext(vm).monkey.Friend) - assert.equal("123" , getContext(vm).monkey.Number) - }) - - it('equal to method/property reference', function(){ - var vm = "#set($monkey.Blame = $spindoctor.Leak) ## property \n" + - '#set( $monkey.Plan = $spindoctor.weave($web) ) ## method' + var ret = compile.render(context); + assert.equal(expected, ret); + + compile = new Compile(Parser.parse(vm)); + ret = compile.render(context); + assert.equal('<i>', ret); + }); + }); + + describe('Set && Expression', function() { + it('set equal to reference', function() { + var vm = '#set( $monkey = $bill ) ## variable reference'; + assert.equal('hello', getContext(vm, { bill: 'hello' }).monkey); + }); + + it('empty map', function() { + var vm = '#set($foo = {})'; + assert.deepEqual({}, getContext(vm).foo); + }); + + it('#set array', function() { + var vm = '#set($foo = []) #set($foo[0] = 12)'; + assert.equal(12, getContext(vm).foo[0]); + }); + + it('set equal to literal', function() { + var vm = "#set( $monkey.Friend = 'monica' ) ## string literal\n" + '#set( $monkey.Number = 123 ) ##number literal'; + assert.equal('monica', getContext(vm).monkey.Friend); + assert.equal('123', getContext(vm).monkey.Number); + }); + + it('equal to method/property reference', function() { + var vm = '#set($monkey.Blame = $spindoctor.Leak) ## property \n' + '#set( $monkey.Plan = $spindoctor.weave($web) ) ## method'; var obj = { spindoctor: { - weave: function(name){ - return name + weave: function(name) { + return name; }, - Leak: "hello world" + Leak: 'hello world', }, - web: "name" - } - - assert.equal("hello world" , getContext(vm, obj).monkey.Blame) - assert.equal("name" , getContext(vm, obj).monkey.Plan) - }) + web: 'name', + }; + assert.equal('hello world', getContext(vm, obj).monkey.Blame); + assert.equal('name', getContext(vm, obj).monkey.Plan); + }); - it('equal to map/list', function(){ + it('equal to map/list', function() { var vms = [ '#set( $monkey.Say = ["Not", $my, "fault"] ) ## ArrayList', - '#set( $monkey.Map = {"banana" : "good", "roast beef" : "bad"}) ## Map' - ] - - var list = ["Not", "my", "fault"] - var map = {"banana" : "good", "roast beef" : "bad"} - assert.deepEqual(list , getContext(vms[0], {my: "my"}).monkey.Say) - assert.deepEqual(map , getContext(vms[1]).monkey.Map) - }) - - it('expression simple math', function(){ - assert.equal(10 , getContext('#set($foo = 2 * 5)').foo) - assert.equal(2 , getContext('#set($foo = 4 / 2)').foo) - assert.equal(-3 , getContext('#set($foo = 2 - 5)').foo) - assert.equal(1 , getContext('#set($foo = 5 % 2)').foo) - assert.equal(7 , getContext('#set($foo = 7)').foo) - }) - - it('math with decimal', function(){ - assert.equal(10.5 , getContext('#set($foo = 2.1 * 5)').foo) - assert.equal(2.1 , getContext('#set($foo = 4.2 / 2)').foo) - assert.equal(-7.5 , getContext('#set($foo = - 2.5 - 5)').foo) - }) - - it('expression complex math', function(){ - assert.equal(20 , getContext('#set($foo = (7 + 3) * (10 - 8))').foo) - assert.equal(-20 , getContext('#set($foo = -(7 + 3) * (10 - 8))').foo) - assert.equal(-1 , getContext('#set($foo = -7 + 3 * (10 - 8))').foo) - }) - - it('expression compare', function(){ - assert.equal(false , getContext('#set($foo = 10 > 11)').foo) - assert.equal(true , getContext('#set($foo = 10 < 11)').foo) - assert.equal(true , getContext('#set($foo = 10 != 11)').foo) - assert.equal(true , getContext('#set($foo = 10 <= 11)').foo) - assert.equal(true , getContext('#set($foo = 11 <= 11)').foo) - assert.equal(false , getContext('#set($foo = 12 <= 11)').foo) - assert.equal(true , getContext('#set($foo = 12 >= 11)').foo) - assert.equal(false , getContext('#set($foo = 10 == 11)').foo) - }) - - it('expression logic', function(){ - assert.equal(false , getContext('#set($foo = 10 == 11 && 3 > 1)').foo) - assert.equal(true , getContext('#set($foo = 10 < 11 && 3 > 1)').foo) - assert.equal(true , getContext('#set($foo = 10 > 11 || 3 > 1)').foo) - assert.equal(true , getContext('#set($foo = !(10 > 11) && 3 > 1)').foo) - assert.equal(false , getContext('#set($foo = $a > $b)', {a: 1, b: 2}).foo) - assert.equal(false , getContext('#set($foo = $a && $b)', {a: 1, b: 0}).foo) - assert.equal(true , getContext('#set($foo = $a || $b)', {a: 1, b: 0}).foo) - }) - - it('#set context should be global, #25', function(){ - var vm = '#macro(local) #set($val =1) $val #end #local() $val' - var ret = render(vm).replace(/\s+/g, '') - assert.equal('11', ret) - }) - }) - - describe('Literals', function(){ - - it("eval string value", function(){ - var vm = '#set( $directoryRoot = "www" )' + - '#set( $templateName = "index.vm")' + - '#set( $template = "$directoryRoot/$templateName" )' + - '$template' - - assert.equal('www/index.vm', render(vm)) - }) - - it('not eval string', function(){ - var vm = "#set( $blargh = '$foo' )$blargh" - assert.equal('$foo', render(vm)) - }) - - it('not parse #[[ ]]#', function(){ - var vm = '#foreach ($woogie in $boogie) nothing to $woogie #end' - assert.equal(vm, render('#[[' + vm + ']]#')) - }) - - it('Range Operator', function(){ - var vm1 = '#set($foo = [-1..2])' - var vm2 = '#set($foo = [-1..$bar])' - var vm3 = '#set($foo = [$bar..2])' - assert.deepEqual([-1, 0, 1, 2], getContext(vm1).foo) - assert.deepEqual([-1, 0, 1, 2], getContext(vm2, {bar: 2}).foo) - assert.deepEqual([-1, 0, 1, 2], getContext(vm3, {bar: -1}).foo) - assert.deepEqual([], getContext('#set($foo = [$bar..1])').foo) - }) - - it('map and array nest', function(){ - var vm1 = '' + - '#set($a = [\n' + - ' {"name": 1},\n' + - ' {"name": 2}\n' + - '])\n' + - ' ' - - var vm2 = '' + + '#set( $monkey.Map = {"banana" : "good", "roast beef" : "bad"}) ## Map', + ]; + + var list = ['Not', 'my', 'fault']; + var map = { banana: 'good', 'roast beef': 'bad' }; + assert.deepEqual(list, getContext(vms[0], { my: 'my' }).monkey.Say); + assert.deepEqual(map, getContext(vms[1]).monkey.Map); + }); + + it('expression simple math', function() { + assert.equal(10, getContext('#set($foo = 2 * 5)').foo); + assert.equal(2, getContext('#set($foo = 4 / 2)').foo); + assert.equal(-3, getContext('#set($foo = 2 - 5)').foo); + assert.equal(1, getContext('#set($foo = 5 % 2)').foo); + assert.equal(7, getContext('#set($foo = 7)').foo); + }); + + it('math with decimal', function() { + assert.equal(10.5, getContext('#set($foo = 2.1 * 5)').foo); + assert.equal(2.1, getContext('#set($foo = 4.2 / 2)').foo); + assert.equal(-7.5, getContext('#set($foo = - 2.5 - 5)').foo); + }); + + it('expression complex math', function() { + assert.equal(20, getContext('#set($foo = (7 + 3) * (10 - 8))').foo); + assert.equal(-20, getContext('#set($foo = -(7 + 3) * (10 - 8))').foo); + assert.equal(-1, getContext('#set($foo = -7 + 3 * (10 - 8))').foo); + }); + + it('expression compare', function() { + assert.equal(false, getContext('#set($foo = 10 > 11)').foo); + assert.equal(true, getContext('#set($foo = 10 < 11)').foo); + assert.equal(true, getContext('#set($foo = 10 != 11)').foo); + assert.equal(true, getContext('#set($foo = 10 <= 11)').foo); + assert.equal(true, getContext('#set($foo = 11 <= 11)').foo); + assert.equal(false, getContext('#set($foo = 12 <= 11)').foo); + assert.equal(true, getContext('#set($foo = 12 >= 11)').foo); + assert.equal(false, getContext('#set($foo = 10 == 11)').foo); + }); + + it('expression logic', function() { + assert.equal(false, getContext('#set($foo = 10 == 11 && 3 > 1)').foo); + assert.equal(true, getContext('#set($foo = 10 < 11 && 3 > 1)').foo); + assert.equal(true, getContext('#set($foo = 10 > 11 || 3 > 1)').foo); + assert.equal(true, getContext('#set($foo = !(10 > 11) && 3 > 1)').foo); + assert.equal(false, getContext('#set($foo = $a > $b)', { a: 1, b: 2 }).foo); + assert.equal(false, getContext('#set($foo = $a && $b)', { a: 1, b: 0 }).foo); + assert.equal(true, getContext('#set($foo = $a || $b)', { a: 1, b: 0 }).foo); + }); + + it('#set context should be global, #25', function() { + var vm = '#macro(local) #set($val =1) $val #end #local() $val'; + var ret = render(vm).replace(/\s+/g, ''); + assert.equal('11', ret); + }); + }); + + describe('Literals', function() { + it('eval string value', function() { + var vm = + '#set( $directoryRoot = "www" )' + + '#set( $templateName = "index.vm")' + + '#set( $template = "$directoryRoot/$templateName" )' + + '$template'; + + assert.equal('www/index.vm', render(vm)); + }); + + it('not eval string', function() { + var vm = "#set( $blargh = '$foo' )$blargh"; + assert.equal('$foo', render(vm)); + }); + + it('not parse #[[ ]]#', function() { + var vm = '#foreach ($woogie in $boogie) nothing to $woogie #end'; + assert.equal(vm, render('#[[' + vm + ']]#')); + }); + + it('Range Operator', function() { + var vm1 = '#set($foo = [-1..2])'; + var vm2 = '#set($foo = [-1..$bar])'; + var vm3 = '#set($foo = [$bar..2])'; + assert.deepEqual([-1, 0, 1, 2], getContext(vm1).foo); + assert.deepEqual([-1, 0, 1, 2], getContext(vm2, { bar: 2 }).foo); + assert.deepEqual([-1, 0, 1, 2], getContext(vm3, { bar: -1 }).foo); + assert.deepEqual([], getContext('#set($foo = [$bar..1])').foo); + }); + + it('map and array nest', function() { + var vm1 = '' + '#set($a = [\n' + ' {"name": 1},\n' + ' {"name": 2}\n' + '])\n' + ' '; + + var vm2 = + '' + '#set($a = {\n' + ' "a": [1, 2, ["1", "a"], {"a": 1}],\n' + ' "b": "12",\n' + ' "d": null,\n' + ' "c": false\n' + '})\n' + - '' - - assert.deepEqual([{name: 1}, { name: 2 }], getContext(vm1).a) - assert.deepEqual({a: [1, 2, ["1", "a"], {a: 1}], b: "12", d: null, c: false}, getContext(vm2).a) - }) - }) - - describe('Conditionals', function(){ - - it('#if', function(){ - var vm = '#if($foo)Velocity#end' - assert.equal('Velocity', render(vm, {foo: 1})) - }) - - it('#if not work', function(){ - var vm = '#if($!css_pureui)hello world#end' - assert.equal('', render(vm)) - }) - - it('#elseif & #else', function(){ - var vm = '#if($foo < 5)Go North#elseif($foo == 8)Go East#{else}Go South#end' - assert.equal('Go North' , render(vm, {foo: 4})) - assert.equal('Go East' , render(vm, {foo: 8})) - assert.equal('Go South' , render(vm, {foo: 9})) - }) - - it('#if with arguments', function(){ - var vm = '#if($foo.isTrue(true))true#end' + ''; + + assert.deepEqual([{ name: 1 }, { name: 2 }], getContext(vm1).a); + assert.deepEqual({ a: [1, 2, ['1', 'a'], { a: 1 }], b: '12', d: null, c: false }, getContext(vm2).a); + }); + }); + + describe('Conditionals', function() { + it('#if', function() { + var vm = '#if($foo)Velocity#end'; + assert.equal('Velocity', render(vm, { foo: 1 })); + }); + + it('#if not work', function() { + var vm = '#if($!css_pureui)hello world#end'; + assert.equal('', render(vm)); + }); + + it('#elseif & #else', function() { + var vm = '#if($foo < 5)Go North#elseif($foo == 8)Go East#{else}Go South#end'; + assert.equal('Go North', render(vm, { foo: 4 })); + assert.equal('Go East', render(vm, { foo: 8 })); + assert.equal('Go South', render(vm, { foo: 9 })); + }); + + it('#if with arguments', function() { + var vm = '#if($foo.isTrue(true))true#end'; var foo = { isTrue: function(str) { - return !!str - } - } + return !!str; + }, + }; + + assert.equal('true', render(vm, { foo: foo })); + }); + }); + + describe('Loops', function() { + it('#foreach', function() { + var vm = '#foreach( $product in $allProducts )
      • $product
      • #end'; + var data = { allProducts: ['book', 'phone'] }; + assert.equal('
      • book
      • phone
      • ', render(vm, data)); + }); + + it('#foreach with map', function() { + var vm = '#foreach($key in $products) name => $products.name #end'; + var data = { products: { name: 'hanwen' } }; + assert.equal(' name => hanwen ', render(vm, data)); + }); + + it('#foreach with map keySet', function() { + var vm = '#foreach($key in $products.keySet()) $key => $products.get($key) #end'; + var data = { products: { name: 'hanwen' } }; + assert.equal(' name => hanwen ', render(vm, data)); + }); + + it('#foreach with nest foreach', function() { + var vm = '#foreach($i in [1..2])${velocityCount}#foreach($j in [2..3])${velocityCount}#end#end'; + assert.equal('112212', render(vm)); + var vm = '#foreach($i in [5..2])$i#end'; + assert.equal('5432', render(vm)); + }); + + it('#foreach with map entrySet', function() { + var vm = + '' + + '#set($js_file = {\n' + + ' "js_arale":"build/js/arale.js?t=20110608",\n' + + ' "js_ma_template":"build/js/ma/template.js?t=20110608",\n' + + ' "js_pa_pa":"build/js/pa/pa.js?t=20110608",\n' + + ' "js_swiff":"build/js/app/swiff.js?t=20110608",\n' + + ' "js_alieditControl":"build/js/pa/alieditcontrol-update.js?t=20110608"\n' + + '})\n' + + '#foreach($_item in $js_file.entrySet())' + + '$_item.key = $staticServer.getURI("/${_item.value}")\n' + + '#end'; - assert.equal('true', render(vm, {foo: foo})) - }) - - }) - - describe('Loops', function(){ - - it('#foreach', function(){ - var vm = '#foreach( $product in $allProducts )
      • $product
      • #end' - var data = {allProducts: ["book", "phone"]} - assert.equal('
      • book
      • phone
      • ', render(vm, data)) - }) - - it('#foreach with map', function(){ - var vm = '#foreach($key in $products) name => $products.name #end' - var data = {products: {name: "hanwen"}} - assert.equal(' name => hanwen ', render(vm, data)) - }) - - it('#foreach with map keySet', function(){ - var vm = '#foreach($key in $products.keySet()) $key => $products.get($key) #end' - var data = {products: {name: "hanwen"}} - assert.equal(' name => hanwen ', render(vm, data)) - }) - - it('#foreach with nest foreach', function(){ - var vm = '#foreach($i in [1..2])${velocityCount}#foreach($j in [2..3])${velocityCount}#end#end' - assert.equal('112212', render(vm)) - var vm = '#foreach($i in [5..2])$i#end' - assert.equal('5432', render(vm)) - }) - - it('#foreach with map entrySet', function(){ - var vm = '' + - '#set($js_file = {\n' + - ' "js_arale":"build/js/arale.js?t=20110608",\n'+ - ' "js_ma_template":"build/js/ma/template.js?t=20110608",\n'+ - ' "js_pa_pa":"build/js/pa/pa.js?t=20110608",\n'+ - ' "js_swiff":"build/js/app/swiff.js?t=20110608",\n' + - ' "js_alieditControl":"build/js/pa/alieditcontrol-update.js?t=20110608"\n' + - '})\n' + - '#foreach($_item in $js_file.entrySet())'+ - '$_item.key = $staticServer.getURI("/${_item.value}")\n'+ - '#end' - - var ret = 'js_arale = /path/build/js/arale.js?t=20110608\n' + - 'js_ma_template = /path/build/js/ma/template.js?t=20110608\n' + - 'js_pa_pa = /path/build/js/pa/pa.js?t=20110608\n' + - 'js_swiff = /path/build/js/app/swiff.js?t=20110608\n' + - 'js_alieditControl = /path/build/js/pa/alieditcontrol-update.js?t=20110608\n' + var ret = + 'js_arale = /path/build/js/arale.js?t=20110608\n' + + 'js_ma_template = /path/build/js/ma/template.js?t=20110608\n' + + 'js_pa_pa = /path/build/js/pa/pa.js?t=20110608\n' + + 'js_swiff = /path/build/js/app/swiff.js?t=20110608\n' + + 'js_alieditControl = /path/build/js/pa/alieditcontrol-update.js?t=20110608\n'; var data = { staticServer: { - getURI: function(url){ - return '/path' + url - } - } - } + getURI: function(url) { + return '/path' + url; + }, + }, + }; - assert.equal(ret.trim(), render(vm, data).trim()) - - }) - - it('#foreach with #macro, $velocityCount should work find, #25', function(){ - var vm = '#macro(local) #end #foreach ($one in [1,2,4]) #local() $velocityCount #end' - var ret = render(vm).replace(/\s+/g, '') - assert.equal('123', ret) - }) - - it('#break', function(){ - var vm = '#foreach($num in [1..6]) #if($foreach.count > 3) #break #end $num #end' - assert.equal(' 1 2 3 4 ', render(vm)) - }) - - }) - - describe('Velocimacros', function(){ - - it('simple #macro', function(){ - var vm = '#macro( d )#end #d()' - assert.equal(' ', render(vm)) - }) - - it('compex #macro', function(){ - var vm = '#macro( d $name)$!name#end #d($foo)' - var vm1 = '#macro( d $a $b)#if($b)$a#end#end #d ( $foo $bar )' - - assert.equal(' hanwen', render(vm, {foo: 'hanwen'})) - assert.equal(' ' , render(vm)) - assert.equal(' ' , render(vm1, {foo: "haha"})) - assert.equal(' ' , render(vm1, {foo: "haha", bar: false})) - assert.equal(' haha' , render(vm1, {foo: "haha", bar: true})) - }) - - it('#macro call arguments', function(){ - var vm = '#macro( d $a $b $d)$a$b$!d#end #d ( $foo , $bar, $dd )' - assert.equal(' ab', render(vm, {foo: 'a', bar: 'b'})) - assert.equal(' abd', render(vm, {foo: 'a', bar: 'b', dd: 'd'})) - }) - - it('#macro map argument', function(){ - var vm = '#macro( d $a)#foreach($_item in $a.entrySet())$_item.key = $_item.value #end#end #d ( {"foo": $foo,"bar":$bar} )' - assert.equal(' foo = a bar = b ', render(vm, {foo: 'a', bar: 'b'})) - }) - - it('#noescape', function(){ - var vm = '#noescape()$hello#end' - assert.equal('hello world', render(vm, {hello: 'hello world'})) - }) - - }) - - describe('Escaping', function(){ - it('escape slash', function(){ - var vm = '#set( $email = "foo" )$email \\$email' - assert.equal('foo $email', render(vm)) - }) - - it('double slash', function(){ - var vm = '#set( $email = "foo" )\\\\$email \\\\\\$email' - assert.equal("\\foo \\$email", render(vm)) - }) - - }) - - describe('Error condiction', function(){ - - it('css color render', function(){ - var vm = 'color: #666 height: 39px' - assert.equal(vm, render(vm)) - }) - - it('jquery parse', function(){ - var vm = '$(function(){ $("a").click() $.post() })' - assert.equal(vm, render(vm)) - }) - - it('issue #7: $ meet with #', function(){ - var vm = '$bar.foo()#if(1>0)...#end' - assert.equal('$bar.foo()...', render(vm)) - }) - - it('issue #15', function(){ - var vm = '#macro(a $b $list)#foreach($a in $list)${a}#end $b #end #a("hello", [1, 2])' - assert.equal(' 12 hello ', render(vm)) - }) - - it('issue #18', function(){ - var vm = '$!tradeDetailModel.goodsInfoModel.goodsTitle[商品页面]' - var ret = '[商品页面]' - assert.equal(ret, render(vm)) - }) - - it('issue #18, condiction 2', function(){ - var vm = '$!a(**** **** **** $stringUtil.right($!b,4))' - var ret = '(**** **** **** $stringUtil.right($!b,4))' - assert.equal(ret, render(vm)) - }) - - it('# meet with css property', function(){ - var vm = '#margin-top:2px' - assert.equal(vm, render(vm)) - }) - - it('var end must in condiction var begin', function(){ - var vm = 'stepFareNo:{$!result.getStepFareNo()}' - assert.equal('stepFareNo:{}', render(vm)) - }) - - it('empty string condiction', function(){ - assert.equal('', render('')) - assert.equal('', render('##hello')) - assert.equal('hello', render('hello')) - }) - - }) + assert.equal(ret.trim(), render(vm, data).trim()); + }); + + it('#foreach with #macro, $velocityCount should work find, #25', function() { + var vm = '#macro(local) #end #foreach ($one in [1,2,4]) #local() $velocityCount #end'; + var ret = render(vm).replace(/\s+/g, ''); + assert.equal('123', ret); + }); + + it('#break', function() { + var vm = '#foreach($num in [1..6]) #if($foreach.count > 3) #break #end $num #end'; + assert.equal(' 1 2 3 4 ', render(vm)); + }); + }); + + describe('Velocimacros', function() { + it('simple #macro', function() { + var vm = '#macro( d )#end #d()'; + assert.equal(' ', render(vm)); + }); + + it('compex #macro', function() { + var vm = '#macro( d $name)$!name#end #d($foo)'; + var vm1 = '#macro( d $a $b)#if($b)$a#end#end #d ( $foo $bar )'; + + assert.equal(' hanwen', render(vm, { foo: 'hanwen' })); + assert.equal(' ', render(vm)); + assert.equal(' ', render(vm1, { foo: 'haha' })); + assert.equal(' ', render(vm1, { foo: 'haha', bar: false })); + assert.equal(' haha', render(vm1, { foo: 'haha', bar: true })); + }); + + it('#macro call arguments', function() { + var vm = '#macro( d $a $b $d)$a$b$!d#end #d ( $foo , $bar, $dd )'; + assert.equal(' ab', render(vm, { foo: 'a', bar: 'b' })); + assert.equal(' abd', render(vm, { foo: 'a', bar: 'b', dd: 'd' })); + }); + + it('#macro map argument', function() { + var vm = '#macro( d $a)#foreach($_item in $a.entrySet())$_item.key = $_item.value #end#end #d ( {"foo": $foo,"bar":$bar} )'; + assert.equal(' foo = a bar = b ', render(vm, { foo: 'a', bar: 'b' })); + }); + + it('#noescape', function() { + var vm = '#noescape()$hello#end'; + assert.equal('hello world', render(vm, { hello: 'hello world' })); + }); + }); + + describe('Escaping', function() { + it('escape slash', function() { + var vm = '#set( $email = "foo" )$email \\$email'; + assert.equal('foo $email', render(vm)); + }); + + it('double slash', function() { + var vm = '#set( $email = "foo" )\\\\$email \\\\\\$email'; + assert.equal('\\foo \\$email', render(vm)); + }); + }); + + describe('Error condiction', function() { + it('css color render', function() { + var vm = 'color: #666 height: 39px'; + assert.equal(vm, render(vm)); + }); + + it('jquery parse', function() { + var vm = '$(function(){ $("a").click() $.post() })'; + assert.equal(vm, render(vm)); + }); + + it('issue #7: $ meet with #', function() { + var vm = '$bar.foo()#if(1>0)...#end'; + assert.equal('$bar.foo()...', render(vm)); + }); + + it('issue #15', function() { + var vm = '#macro(a $b $list)#foreach($a in $list)${a}#end $b #end #a("hello", [1, 2])'; + assert.equal(' 12 hello ', render(vm)); + }); + + it('issue #18', function() { + var vm = + '$!tradeDetailModel.goodsInfoModel.goodsTitle[商品页面]'; + var ret = '[商品页面]'; + assert.equal(ret, render(vm)); + }); + + it('issue #18, condiction 2', function() { + var vm = '$!a(**** **** **** $stringUtil.right($!b,4))'; + var ret = '(**** **** **** $stringUtil.right($!b,4))'; + assert.equal(ret, render(vm)); + }); + + it('# meet with css property', function() { + var vm = '#margin-top:2px'; + assert.equal(vm, render(vm)); + }); + + it('var end must in condiction var begin', function() { + var vm = 'stepFareNo:{$!result.getStepFareNo()}'; + assert.equal('stepFareNo:{}', render(vm)); + }); + + it('empty string condiction', function() { + assert.equal('', render('')); + assert.equal('', render('##hello')); + assert.equal('hello', render('hello')); + }); + }); describe('throw friendly error message', function() { - it('print right posiont when error throw', function(){ - var vm = '111\nsdfs\n$foo($name)' - var expected = '' + it('print right posiont when error throw', function() { + var vm = '111\nsdfs\n$foo($name)'; + var expected = ''; - var compile = new Compile(Parser.parse(vm), { escape: false }) + var compile = new Compile(Parser.parse(vm), { escape: false }); var context = { name: '', - foo: function(name){ - throw new Error('Run error') - } - } - assert.throws(function(){ - compile.render(context) - }, /\$foo\(\$name\)/) - - assert.throws(function(){ - compile.render(context) - }, /L\/N 3:0/) - }) - - it('print error stack of user-defined macro', function(){ - var vm = '111\n\n#foo($name)' - var vm1 = '\nhello#parse("vm.vm")' + foo: function(name) { + throw new Error('Run error'); + }, + }; + assert.throws(function() { + compile.render(context); + }, /\$foo\(\$name\)/); + + assert.throws(function() { + compile.render(context); + }, /L\/N 3:0/); + }); + + it('print error stack of user-defined macro', function() { + var vm = '111\n\n#foo($name)'; + var vm1 = '\nhello#parse("vm.vm")'; var files = { 'vm.vm': vm, 'vm1.vm': vm1 }; - var compile = new Compile(Parser.parse('\n\n#parse("vm1.vm")')) + var compile = new Compile(Parser.parse('\n\n#parse("vm1.vm")')); var macros = { - foo: function(name){ - throw new Error('Run error') + foo: function(name) { + throw new Error('Run error'); }, - parse: function(name){ + parse: function(name) { return this.eval(files[name]); - } - } + }, + }; - var expected = '' + - 'Run error\n' + - ' at #foo($name) L/N 3:0\n' + - ' at #parse("vm.vm") L/N 2:5\n' + - ' at #parse("vm1.vm") L/N 3:0'; + var expected = + '' + 'Run error\n' + ' at #foo($name) L/N 3:0\n' + ' at #parse("vm.vm") L/N 2:5\n' + ' at #parse("vm1.vm") L/N 3:0'; try { - compile.render({}, macros) - } catch(e) { + compile.render({}, macros); + } catch (e) { assert.equal(expected, e.message); } - }) - }) + }); + }); - describe('user-defined macro, such as #include, #parse', function(){ - - it('basic', function(){ + describe('user-defined macro, such as #include, #parse', function() { + it('basic', function() { var macros = { - haha: function(a, b){ - a = a || '' - b = b || '' - return a + " hello to " + b - } - } + haha: function(a, b) { + a = a || ''; + b = b || ''; + return a + ' hello to ' + b; + }, + }; - var vm = '#haha($a, $b)' - assert.equal(' hello to ', render(vm, {}, macros)) - assert.equal('Lily hello to ', render(vm, {a: "Lily"}, macros)) - }) + var vm = '#haha($a, $b)'; + assert.equal(' hello to ', render(vm, {}, macros)); + assert.equal('Lily hello to ', render(vm, { a: 'Lily' }, macros)); + }); - it('use eval', function(){ + it('use eval', function() { //这一句非常重要,在node端无需处理,web端必须如此声明 - Compile.Parser = Parser + Compile.Parser = Parser; var macros = { - cmsparse: function(str){ - return this.eval(str) + cmsparse: function(str) { + return this.eval(str); }, - d: function(){ - return 'I am d!' - } - } + d: function() { + return 'I am d!'; + }, + }; - var vm = '#cmsparse($str)' + var vm = '#cmsparse($str)'; var o = { str: 'hello $foo.bar, #d()', - foo: {bar: 'world'} - } + foo: { bar: 'world' }, + }; - assert.equal('hello world, I am d!', render(vm, o, macros)) - }) + assert.equal('hello world, I am d!', render(vm, o, macros)); + }); - it('use eval with local variable', function(){ + it('use eval with local variable', function() { //这一句非常重要,在node端无需处理,web端必须如此声明 - Compile.Parser = Parser + Compile.Parser = Parser; var macros = { - - cmsparse: function(str){ - return macros.include.apply(this, arguments) + cmsparse: function(str) { + return macros.include.apply(this, arguments); }, - include: function(str){ - return this.eval(str, {name: "hanwen"}) + include: function(str) { + return this.eval(str, { name: 'hanwen' }); }, - parse: function(file){ - return file + parse: function(file) { + return file; }, - d: function($name){ - return this.eval('I am $name!') - } - } + d: function($name) { + return this.eval('I am $name!'); + }, + }; - var vm = '#cmsparse($str)' - var vm1 = '#parse("a.vm")' + var vm = '#cmsparse($str)'; + var vm1 = '#parse("a.vm")'; var o = { str: 'hello $foo.bar, #d()', - foo: {bar: 'world'} - } + foo: { bar: 'world' }, + }; - assert.equal('hello world, I am hanwen!', render(vm, o, macros)) - assert.equal(undefined, getContext(vm, o, macros).name) - assert.equal('a.vm', render(vm1, o, macros)) - }) + assert.equal('hello world, I am hanwen!', render(vm, o, macros)); + assert.equal(undefined, getContext(vm, o, macros).name); + assert.equal('a.vm', render(vm1, o, macros)); + }); - it('eval work with #set', function(){ + it('eval work with #set', function() { //这一句非常重要,在node端无需处理,web端必须如此声明 - Compile.Parser = Parser + Compile.Parser = Parser; var macros = { - cmsparse: function(str){ - return this.eval(str) - } - } + cmsparse: function(str) { + return this.eval(str); + }, + }; - var vm = '#cmsparse($str)' + var vm = '#cmsparse($str)'; var o = { - str: '#set($foo = ["hello"," ", "world"])#foreach($word in $foo)$word#end' - } - - assert.equal('hello world', render(vm, o, macros)) - }) - }) + str: '#set($foo = ["hello"," ", "world"])#foreach($word in $foo)$word#end', + }; + assert.equal('hello world', render(vm, o, macros)); + }); + }); describe('self defined function', function() { - - it('$control.setTemplate', function(){ - + it('$control.setTemplate', function() { var control = { - - setTemplate: function(vm){ - + setTemplate: function(vm) { this.vm = vm; return this; - }, - toString: function(){ + toString: function() { return this.eval(this.vm, this.__temp); }, __temp: {}, - setParameter: function(key, value){ + setParameter: function(key, value) { this.__temp[key] = value; return this; - } + }, }; - var str = 'hello $who, welcome to $where' - - var vm = '$control.setTemplate($str).setParameter("who", "Blob").setParameter("where", "China")' - assert.equal(render(vm, {str: str, control: control}), 'hello Blob, welcome to China') + var str = 'hello $who, welcome to $where'; - }) - - }) + var vm = '$control.setTemplate($str).setParameter("who", "Blob").setParameter("where", "China")'; + assert.equal(render(vm, { str: str, control: control }), 'hello Blob, welcome to China'); + }); + }); describe('issues', function() { it('#29', function() { - var vm = '#set($total = 0) #foreach($i in [1,2,3]) #set($total = $total + $i) #end $total' - assert.equal(render(vm).trim(), "6") - }) + var vm = '#set($total = 0) #foreach($i in [1,2,3]) #set($total = $total + $i) #end $total'; + assert.equal(render(vm).trim(), '6'); + }); it('#30', function() { - var vm = '$foo.setName' - assert.equal(render(vm, { foo: { setName: "foo" }}).trim(), "foo") - }) - }) - - describe('multiline', function(){ - it('#set multiline', function(){ - var vm = "$bar.foo()\n#set($foo=$bar)\n..." - assert.equal("$bar.foo()\n...", render(vm)) - }) - - it('#if multiline', function(){ - var vm = "$bar.foo()\n#if(1>0)\n...#end" - assert.equal("$bar.foo()\n...", render(vm)) - }) - - it('#set #set', function(){ - var vm = "$bar.foo()\n...\n#set($foo=$bar)\n#set($foo=$bar)" - assert.equal("$bar.foo()\n...\n", render(vm)) - }) - - it('#if multiline #set', function(){ - var vm = "$bar.foo()\n#if(1>0)\n#set($foo=$bar)\n...#end" - assert.equal("$bar.foo()\n...", render(vm)) - }) - - it('#if multiline #set #end', function(){ - var vm = "$bar.foo()\n#if(1>0)...\n#set($foo=$bar)\n#end" - assert.equal("$bar.foo()\n...\n", render(vm)) - }) - - it('with references', function(){ - var vm = [ 'a', - '#foreach($b in $nums)', - '#if($b) ', - 'b', - 'e $b.alm', - '#end', - '#end', - 'c'].join("\n"); - var expected = [ 'a', 'b', 'e 1', 'b', 'e 2', 'b', 'e 3', 'c'].join("\n") - - assert.equal(expected, render(vm, {nums:[{alm:1},{alm:2},{alm:3}],bar:""})) - }) - - it('multiple newlines after statement', function(){ - var vm = '#if(1>0)\n\nb#end' - assert.equal('\nb', render(vm)) - }) - }) - - describe('define support', function(){ - it('basic', function(){ - var vm = '#define( $block )\nHello $who#end\n#set( $who = "World!" )\n$block' - assert.equal('Hello World!', render(vm)) - }) - }) -}) - -describe('Helper', function(){ - var getRefText = Velocity.Helper.getRefText - var parse = Velocity.Parser.parse + var vm = '$foo.setName'; + assert.equal(render(vm, { foo: { setName: 'foo' } }).trim(), 'foo'); + }); + }); + + describe('multiline', function() { + it('#set multiline', function() { + var vm = '$bar.foo()\n#set($foo=$bar)\n...'; + assert.equal('$bar.foo()\n...', render(vm)); + }); + + it('#if multiline', function() { + var vm = '$bar.foo()\n#if(1>0)\n...#end'; + assert.equal('$bar.foo()\n...', render(vm)); + }); + + it('#set #set', function() { + var vm = '$bar.foo()\n...\n#set($foo=$bar)\n#set($foo=$bar)'; + assert.equal('$bar.foo()\n...\n', render(vm)); + }); + + it('#if multiline #set', function() { + var vm = '$bar.foo()\n#if(1>0)\n#set($foo=$bar)\n...#end'; + assert.equal('$bar.foo()\n...', render(vm)); + }); + + it('#if multiline #set #end', function() { + var vm = '$bar.foo()\n#if(1>0)...\n#set($foo=$bar)\n#end'; + assert.equal('$bar.foo()\n...\n', render(vm)); + }); + + it('with references', function() { + var vm = ['a', '#foreach($b in $nums)', '#if($b) ', 'b', 'e $b.alm', '#end', '#end', 'c'].join('\n'); + var expected = ['a', 'b', 'e 1', 'b', 'e 2', 'b', 'e 3', 'c'].join('\n'); + + assert.equal(expected, render(vm, { nums: [{ alm: 1 }, { alm: 2 }, { alm: 3 }], bar: '' })); + }); + + it('multiple newlines after statement', function() { + var vm = '#if(1>0)\n\nb#end'; + assert.equal('\nb', render(vm)); + }); + }); + + describe('define support', function() { + it('basic', function() { + var vm = '#define( $block )\nHello $who#end\n#set( $who = "World!" )\n$block'; + assert.equal('Hello World!', render(vm)); + }); + }); +}); + +describe('Helper', function() { + var getRefText = Velocity.Helper.getRefText; + var parse = Velocity.Parser.parse; describe('getRefText', function() { it('simple reference', function() { - var foo = '$a.b' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + var foo = '$a.b'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference method', function() { - var foo = '$a.b()' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + var foo = '$a.b()'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference method with arguments', function() { - var foo = '$a.b("hello")' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) + var foo = '$a.b("hello")'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); - foo = '$a.b(\'hello\')' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) + foo = "$a.b('hello')"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); - foo = '$a.b(\'hello\',10)' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + foo = "$a.b('hello',10)"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference method with arguments array', function() { - var foo = '$a.b(["hello"])' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) + var foo = '$a.b(["hello"])'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); - foo = '$a.b([\'hello\', 2])' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) + foo = "$a.b(['hello', 2])"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); it('reference index', function() { - var foo = '$a.b[1]' - var ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - - foo = '$a.b["cc"]' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - - foo = '$a.b[\'cc\']' - ast = parse(foo)[0] - assert.equal(getRefText(ast), foo) - }) - }) -}) -describe('Parser', function(){ - - describe('simple references', function(){ - - it('simple references', function(){ - var vm = 'hello world: $foo' - var ret = Parser.parse(vm) - - assert.ok(ret instanceof Array) - assert.equal(2, ret.length) - assert.equal('hello world: ', ret[0]) - assert.equal('foo', ret[1].id) - - }) - - it('valid variable references', function(){ - var vm = '$mud-Slinger_1' - assert.equal('mud-Slinger_1', Parser.parse(vm)[0].id) - }) - - it('wraped references', function(){ - var vm = '${mudSlinger}' - var ast = Parser.parse(vm)[0] - assert.equal(true, ast.isWraped) - assert.equal('mudSlinger', ast.id) - }) - - it('function call references', function(){ - var ast = Parser.parse('$foo()')[0] - assert.equal(false, ast.args) - assert.equal('references', ast.type) - }) - - }) - - describe('Properties', function(){ - - it('simple property', function(){ - var vm = '$customer.Address' - var asts = Parser.parse(vm) + var foo = '$a.b[1]'; + var ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + + foo = '$a.b["cc"]'; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + + foo = "$a.b['cc']"; + ast = parse(foo)[0]; + assert.equal(getRefText(ast), foo); + }); + }); +}); +describe('Parser', function() { + describe('simple references', function() { + it('simple references', function() { + var vm = 'hello world: $foo'; + var ret = Parser.parse(vm); + + assert.ok(ret instanceof Array); + assert.equal(2, ret.length); + assert.equal('hello world: ', ret[0]); + assert.equal('foo', ret[1].id); + }); + + it('valid variable references', function() { + var vm = '$mud-Slinger_1'; + assert.equal('mud-Slinger_1', Parser.parse(vm)[0].id); + }); + + it('wraped references', function() { + var vm = '${mudSlinger}'; + var ast = Parser.parse(vm)[0]; + assert.equal(true, ast.isWraped); + assert.equal('mudSlinger', ast.id); + }); + + it('function call references', function() { + var ast = Parser.parse('$foo()')[0]; + assert.equal(false, ast.args); + assert.equal('references', ast.type); + }); + }); + + describe('Properties', function() { + it('simple property', function() { + var vm = '$customer.Address'; + var asts = Parser.parse(vm); assert.deepEqual(asts[0], { - id : "customer", - prue : true, - type : "references", - path : [{"type": "property","id": "Address"}], - leader : '$', - pos: { first_line: 1, last_line: 1, first_column: 0, last_column: 17 } - }) - }) - - }) - - describe('Methods ', function(){ - - it('with no arguments', function(){ - var vm = '$foo.bar()' - var ast = Parser.parse(vm)[0] - - assert.deepEqual(ast['path'], [{ - type : "method", - id : "bar", - args : false - }]) - }) - - it('with arguments integer', function(){ - var vm = '$foo.bar(10)' - var ast = Parser.parse(vm)[0] - - assert.deepEqual(ast['path'], [{ - type : "method", - id : "bar", - args : [{ - type : "integer", - value : "10" - }] - }]) - }) - - it('with arguments references', function(){ - var vm = '$foo.bar($bar)' - var ast = Parser.parse(vm)[0] - - assert.equal(ast.prue, true) - - assert.deepEqual(ast.path[0].args, [{ - type : "references", - leader : "$", - id : "bar" - }]) - }) - - }) - - describe('Index', function(){ - - it('all kind of indexs', function(){ - var vm = '$foo[0] $foo[$i] $foo["bar"]' - var asts = Parser.parse(vm) - - assert.equal(5, asts.length) - - //asts[0].path[0] => $foo[0] - //{type: 'index', id: {type: 'integer', value: '0'}} - assert.equal('index', asts[0].path[0].type) - assert.equal('integer', asts[0].path[0].id.type) - assert.equal('0', asts[0].path[0].id.value) - - //asts[2].path[0] => $foo[$i] - //{type: 'references', id: {type:'references', id: 'i', leader: '$'}} - assert.equal('index', asts[2].path[0].type) - assert.equal('references', asts[2].path[0].id.type) - assert.equal('i', asts[2].path[0].id.id) - - //asts[4].path[0] => $foo["bar"] - //{type: 'index', id: {type: 'string', value: 'bar', isEval: true}} - assert.equal('index', asts[4].path[0].type) - assert.equal('string', asts[4].path[0].id.type) - assert.equal('bar', asts[4].path[0].id.value) - - }) - - }) - - describe('complex references', function(){ - - it('property + index + property', function(){ - var vm = '$foo.bar[1].junk' - var ast = Parser.parse(vm)[0] - - assert.equal('foo', ast.id) - assert.equal(3, ast.path.length) - - var paths = ast.path - - assert.equal('property', paths[0].type) - assert.equal('index', paths[1].type) - assert.equal('property', paths[2].type) - - }) - - - it('method + index', function(){ - var vm = '$foo.callMethod()[1]' - var ast = Parser.parse(vm)[0] - - assert.equal(2, ast.path.length) - - assert.equal('method', ast.path[0].type) - assert.equal('callMethod', ast.path[0].id) - - assert.equal('index', ast.path[1].type) - assert.equal('1', ast.path[1].id.value) - assert.equal('integer', ast.path[1].id.type) - - }) - - it('property should not start with alphabet', function(){ - var asts = Parser.parse('$foo.124') - var ast2 = Parser.parse('$foo.-24')[0] - - assert.equal(3, asts.length) - assert.equal('foo', asts[0].id) - assert.equal(undefined, asts[0].path) - - assert.equal(undefined, ast2.path) - - }) - - it('index should end with close bracket', function(){ - assert.throws(function(){ - Parser.parse("$foo.bar['a'12]") - }, /Parse error/) - }) - - }) - - describe('Directives', function(){ - - it('#macro', function(){ - var vm = '#macro( d $a $b)#if($b)$a#end#end #d($foo $bar)' - var asts = Parser.parse(vm) - - var ifAst = asts[0][1] - - assert.equal(ifAst[0].condition.type, 'references') - assert.equal(ifAst[0].condition.id, 'b') - assert.equal(ifAst[0].condition.prue, undefined) + id: 'customer', + prue: true, + type: 'references', + path: [{ type: 'property', id: 'Address' }], + leader: '$', + pos: { first_line: 1, last_line: 1, first_column: 0, last_column: 17 }, + }); + }); + }); + + describe('Methods ', function() { + it('with no arguments', function() { + var vm = '$foo.bar()'; + var ast = Parser.parse(vm)[0]; + + assert.deepEqual(ast['path'], [ + { + type: 'method', + id: 'bar', + args: false, + }, + ]); + }); + + it('with arguments integer', function() { + var vm = '$foo.bar(10)'; + var ast = Parser.parse(vm)[0]; + + assert.deepEqual(ast['path'], [ + { + type: 'method', + id: 'bar', + args: [ + { + type: 'integer', + value: '10', + }, + ], + }, + ]); + }); - assert.equal(ifAst[1].type, 'references') - assert.equal(ifAst[1].id, 'a') - assert.equal(ifAst[1].prue, true) + it('with arguments references', function() { + var vm = '$foo.bar($bar)'; + var ast = Parser.parse(vm)[0]; - assert.equal(asts.length, 3) - assert.equal(ifAst.length, 2) - }) + assert.equal(ast.prue, true); - }) + assert.deepEqual(ast.path[0].args, [ + { + type: 'references', + leader: '$', + id: 'bar', + }, + ]); + }); + }); - describe('comment identify', function(){ + describe('Index', function() { + it('all kind of indexs', function() { + var vm = '$foo[0] $foo[$i] $foo["bar"]'; + var asts = Parser.parse(vm); - it('one line comment', function(){ - var asts = Parser.parse('#set( $monkey.Number = 123)##number literal') + assert.equal(5, asts.length); - assert.equal(2, asts.length) - assert.equal('comment', asts[1].type) - }) + //asts[0].path[0] => $foo[0] + //{type: 'index', id: {type: 'integer', value: '0'}} + assert.equal('index', asts[0].path[0].type); + assert.equal('integer', asts[0].path[0].id.type); + assert.equal('0', asts[0].path[0].id.value); - }) + //asts[2].path[0] => $foo[$i] + //{type: 'references', id: {type:'references', id: 'i', leader: '$'}} + assert.equal('index', asts[2].path[0].type); + assert.equal('references', asts[2].path[0].id.type); + assert.equal('i', asts[2].path[0].id.id); -}) + //asts[4].path[0] => $foo["bar"] + //{type: 'index', id: {type: 'string', value: 'bar', isEval: true}} + assert.equal('index', asts[4].path[0].type); + assert.equal('string', asts[4].path[0].id.type); + assert.equal('bar', asts[4].path[0].id.value); + }); + }); + + describe('complex references', function() { + it('property + index + property', function() { + var vm = '$foo.bar[1].junk'; + var ast = Parser.parse(vm)[0]; + + assert.equal('foo', ast.id); + assert.equal(3, ast.path.length); + + var paths = ast.path; + + assert.equal('property', paths[0].type); + assert.equal('index', paths[1].type); + assert.equal('property', paths[2].type); + }); + + it('method + index', function() { + var vm = '$foo.callMethod()[1]'; + var ast = Parser.parse(vm)[0]; + + assert.equal(2, ast.path.length); + + assert.equal('method', ast.path[0].type); + assert.equal('callMethod', ast.path[0].id); + + assert.equal('index', ast.path[1].type); + assert.equal('1', ast.path[1].id.value); + assert.equal('integer', ast.path[1].id.type); + }); + + it('property should not start with alphabet', function() { + var asts = Parser.parse('$foo.124'); + var ast2 = Parser.parse('$foo.-24')[0]; + + assert.equal(3, asts.length); + assert.equal('foo', asts[0].id); + assert.equal(undefined, asts[0].path); + + assert.equal(undefined, ast2.path); + }); + + it('index should end with close bracket', function() { + assert.throws(function() { + Parser.parse("$foo.bar['a'12]"); + }, /Parse error/); + }); + }); + + describe('Directives', function() { + it('#macro', function() { + var vm = '#macro( d $a $b)#if($b)$a#end#end #d($foo $bar)'; + var asts = Parser.parse(vm); + + var ifAst = asts[0][1]; + + assert.equal(ifAst[0].condition.type, 'references'); + assert.equal(ifAst[0].condition.id, 'b'); + assert.equal(ifAst[0].condition.prue, undefined); + + assert.equal(ifAst[1].type, 'references'); + assert.equal(ifAst[1].id, 'a'); + assert.equal(ifAst[1].prue, true); + + assert.equal(asts.length, 3); + assert.equal(ifAst.length, 2); + }); + }); + + describe('comment identify', function() { + it('one line comment', function() { + var asts = Parser.parse('#set( $monkey.Number = 123)##number literal'); + + assert.equal(2, asts.length); + assert.equal('comment', asts[1].type); + }); + }); +}); diff --git a/packages/amplify-velocity-template/tests/runner/standalone-debug.js b/packages/amplify-velocity-template/tests/runner/standalone-debug.js index 08e5823778..34d3bcf094 100644 --- a/packages/amplify-velocity-template/tests/runner/standalone-debug.js +++ b/packages/amplify-velocity-template/tests/runner/standalone-debug.js @@ -1,78 +1,73 @@ -var define -var require -(function(global, undefined) { - - function isType(type) { - return function(obj) { - return {}.toString.call(obj) == "[object " + type + "]" - } - } - - var isFunction = isType("Function") - - var cachedMods = {} - - function Module() { - } - - Module.prototype.exec = function () { - var mod = this - - if (this.execed) { - return mod.exports - } - this.execed = true - - function require(id) { - return Module.get(id).exec() - } - - var factory = mod.factory - - var exports = isFunction(factory) ? - factory(require, mod.exports = {}, mod) : - factory - - if (exports === undefined) { - exports = mod.exports - } - - // Reduce memory leak - delete mod.factory - - mod.exports = exports - - return exports - } - - define = function (id, deps, factory) { - var meta = { - id: id, - deps: deps, - factory: factory - } - - Module.save(meta) - } - - Module.save = function(meta) { - var mod = Module.get(meta.id) - - mod.id = meta.id - mod.dependencies = meta.deps - mod.factory = meta.factory - } - - Module.get = function(id) { - return cachedMods[id] || (cachedMods[id] = new Module()) - } - - require = function(id) { - var mod = Module.get(id) - if(!mod.execed) { - mod.exec() - } - return mod.exports - } - -})(this) +var define; +var require; +(function(global, undefined) { + function isType(type) { + return function(obj) { + return {}.toString.call(obj) == '[object ' + type + ']'; + }; + } + + var isFunction = isType('Function'); + + var cachedMods = {}; + + function Module() {} + + Module.prototype.exec = function() { + var mod = this; + + if (this.execed) { + return mod.exports; + } + this.execed = true; + + function require(id) { + return Module.get(id).exec(); + } + + var factory = mod.factory; + + var exports = isFunction(factory) ? factory(require, (mod.exports = {}), mod) : factory; + + if (exports === undefined) { + exports = mod.exports; + } + + // Reduce memory leak + delete mod.factory; + + mod.exports = exports; + + return exports; + }; + + define = function(id, deps, factory) { + var meta = { + id: id, + deps: deps, + factory: factory, + }; + + Module.save(meta); + }; + + Module.save = function(meta) { + var mod = Module.get(meta.id); + + mod.id = meta.id; + mod.dependencies = meta.deps; + mod.factory = meta.factory; + }; + + Module.get = function(id) { + return cachedMods[id] || (cachedMods[id] = new Module()); + }; + + require = function(id) { + var mod = Module.get(id); + if (!mod.execed) { + mod.exec(); + } + return mod.exports; + }; +})(this); diff --git a/packages/amplify-velocity-template/tests/script/syncRunner.js b/packages/amplify-velocity-template/tests/script/syncRunner.js index 267e30bc66..d3bddd7f53 100644 --- a/packages/amplify-velocity-template/tests/script/syncRunner.js +++ b/packages/amplify-velocity-template/tests/script/syncRunner.js @@ -6,24 +6,23 @@ var files = fs.readdirSync(path.join(__dirname, '../')); var ret = ''; files.forEach(function(file) { - if (path.extname(file) !== '.js') { + if (path.extname(file) !== '.js') { return; } - var str = fs.readFileSync(path.join(__dirname, "../", file)); + var str = fs.readFileSync(path.join(__dirname, '../', file)); ret += str.toString().replace(/^[\S\s]*?describe/, 'describe'); }); -fs.writeFileSync(path.join(__dirname, "../runner/spec.js"), ret); +fs.writeFileSync(path.join(__dirname, '../runner/spec.js'), ret); var json = require('../../package.json'); synsVersion(); -function synsVersion(){ +function synsVersion() { var pathname = path.join(__dirname, '../runner/tests.html'); var html = fs.readFileSync(pathname); - html = html.toString() - .replace(/(velocityjs\/)\d+\.\d+\.\d+/, '$1' + json.version); + html = html.toString().replace(/(velocityjs\/)\d+\.\d+\.\d+/, '$1' + json.version); fs.writeFileSync(pathname, html); console.log('sync version to %s', json.version); diff --git a/packages/amplify-velocity-template/tests/set.test.js b/packages/amplify-velocity-template/tests/set.test.js index 1152ffdd9f..c12e827a74 100644 --- a/packages/amplify-velocity-template/tests/set.test.js +++ b/packages/amplify-velocity-template/tests/set.test.js @@ -1,130 +1,123 @@ -var Velocity = require('../src/velocity') -var assert = require("assert") -var parse = Velocity.parse -var Compile = Velocity.Compile +var Velocity = require('../src/velocity'); +var assert = require('assert'); +var parse = Velocity.parse; +var Compile = Velocity.Compile; describe('Set && Expression', function() { var render = Velocity.render; function getContext(str, context, macros) { - var compile = new Compile(parse(str)) - compile.render(context, macros) - return compile.context + var compile = new Compile(parse(str)); + compile.render(context, macros); + return compile.context; } it('set equal to reference', function() { - var vm = '#set( $monkey = $bill ) ## variable reference' - assert.equal("hello", getContext(vm, {bill: 'hello'}).monkey) - }) + var vm = '#set( $monkey = $bill ) ## variable reference'; + assert.equal('hello', getContext(vm, { bill: 'hello' }).monkey); + }); it('empty map', function() { - var vm = '#set($foo = {})' - assert.deepEqual({}, getContext(vm).foo) - }) + var vm = '#set($foo = {})'; + assert.deepEqual({}, getContext(vm).foo); + }); it('#set array', function() { - var vm = '#set($foo = []) #set($foo[0] = 12)' - assert.equal(12, getContext(vm).foo[0]) - }) + var vm = '#set($foo = []) #set($foo[0] = 12)'; + assert.equal(12, getContext(vm).foo[0]); + }); it('set equal to literal', function() { - var vm = "#set( $monkey.Friend = 'monica' ) ## string literal\n" + - '#set( $monkey.Number = 123 ) ##number literal' - assert.equal("monica", getContext(vm).monkey.Friend) - assert.equal("123", getContext(vm).monkey.Number) - }) - - it('set equal to result of method ', function () { - var vm = "#set( $monkey = 'monica' ) ## string literal\n" + - '#set( $result = $monkey.substring(1) ) ##calling method' - assert.equal("monica", getContext(vm).monkey) - assert.equal("onica", getContext(vm).result) - }) - - it('set equal to result of method ', function () { - var vm = "#set( $monkey = 1234 ) ## number literal\n" + - '#set( $result = $monkey.toString() ) ##calling method' - assert.equal("1234", getContext(vm).monkey) - assert.equal("1234", getContext(vm).result) - }) + var vm = "#set( $monkey.Friend = 'monica' ) ## string literal\n" + '#set( $monkey.Number = 123 ) ##number literal'; + assert.equal('monica', getContext(vm).monkey.Friend); + assert.equal('123', getContext(vm).monkey.Number); + }); + + it('set equal to result of method ', function() { + var vm = "#set( $monkey = 'monica' ) ## string literal\n" + '#set( $result = $monkey.substring(1) ) ##calling method'; + assert.equal('monica', getContext(vm).monkey); + assert.equal('onica', getContext(vm).result); + }); + + it('set equal to result of method ', function() { + var vm = '#set( $monkey = 1234 ) ## number literal\n' + '#set( $result = $monkey.toString() ) ##calling method'; + assert.equal('1234', getContext(vm).monkey); + assert.equal('1234', getContext(vm).result); + }); it('equal to method/property reference', function() { - var vm = "#set($monkey.Blame = $spindoctor.Leak) ## property \n" + - '#set( $monkey.Plan = $spindoctor.weave($web) ) ## method' + var vm = '#set($monkey.Blame = $spindoctor.Leak) ## property \n' + '#set( $monkey.Plan = $spindoctor.weave($web) ) ## method'; var obj = { spindoctor: { weave: function(name) { - return name + return name; }, - Leak: "hello world" + Leak: 'hello world', }, - web: "name" - } - - assert.equal("hello world", getContext(vm, obj).monkey.Blame) - assert.equal("name", getContext(vm, obj).monkey.Plan) - }) + web: 'name', + }; + assert.equal('hello world', getContext(vm, obj).monkey.Blame); + assert.equal('name', getContext(vm, obj).monkey.Plan); + }); it('equal to map/list', function() { var vms = [ '#set( $monkey.Say = ["Not", $my, "fault"] ) ## ArrayList', - '#set( $monkey.Map = {"banana" : "good", "roast beef" : "bad"}) ## Map' - ] + '#set( $monkey.Map = {"banana" : "good", "roast beef" : "bad"}) ## Map', + ]; - var list = ["Not", "my", "fault"] - var map = {banana: "good", 'roast beef': "bad"} - assert.deepEqual(list, getContext(vms[0], {my: "my"}).monkey.Say) - assert.deepEqual(map, getContext(vms[1]).monkey.Map) - }) + var list = ['Not', 'my', 'fault']; + var map = { banana: 'good', 'roast beef': 'bad' }; + assert.deepEqual(list, getContext(vms[0], { my: 'my' }).monkey.Say); + assert.deepEqual(map, getContext(vms[1]).monkey.Map); + }); it('expression simple math', function() { - assert.equal(10, getContext('#set($foo = 2 * 5)').foo) - assert.equal(2, getContext('#set($foo = 4 / 2)').foo) - assert.equal(-3, getContext('#set($foo = 2 - 5)').foo) - assert.equal(1, getContext('#set($foo = 5 % 2)').foo) - assert.equal(7, getContext('#set($foo = 7)').foo) - }) + assert.equal(10, getContext('#set($foo = 2 * 5)').foo); + assert.equal(2, getContext('#set($foo = 4 / 2)').foo); + assert.equal(-3, getContext('#set($foo = 2 - 5)').foo); + assert.equal(1, getContext('#set($foo = 5 % 2)').foo); + assert.equal(7, getContext('#set($foo = 7)').foo); + }); it('math with decimal', function() { - assert.equal(10.5, getContext('#set($foo = 2.1 * 5)').foo) - assert.equal(2.1, getContext('#set($foo = 4.2 / 2)').foo) - assert.equal(-7.5, getContext('#set($foo = - 2.5 - 5)').foo) - }) + assert.equal(10.5, getContext('#set($foo = 2.1 * 5)').foo); + assert.equal(2.1, getContext('#set($foo = 4.2 / 2)').foo); + assert.equal(-7.5, getContext('#set($foo = - 2.5 - 5)').foo); + }); it('expression complex math', function() { - assert.equal(20, getContext('#set($foo = (7 + 3) * (10 - 8))').foo) - assert.equal(-20, getContext('#set($foo = -(7 + 3) * (10 - 8))').foo) - assert.equal(-1, getContext('#set($foo = -7 + 3 * (10 - 8))').foo) - }) + assert.equal(20, getContext('#set($foo = (7 + 3) * (10 - 8))').foo); + assert.equal(-20, getContext('#set($foo = -(7 + 3) * (10 - 8))').foo); + assert.equal(-1, getContext('#set($foo = -7 + 3 * (10 - 8))').foo); + }); it('expression compare', function() { - assert.equal(false, getContext('#set($foo = 10 > 11)').foo) - assert.equal(true, getContext('#set($foo = 10 < 11)').foo) - assert.equal(true, getContext('#set($foo = 10 != 11)').foo) - assert.equal(true, getContext('#set($foo = 10 <= 11)').foo) - assert.equal(true, getContext('#set($foo = 11 <= 11)').foo) - assert.equal(false, getContext('#set($foo = 12 <= 11)').foo) - assert.equal(true, getContext('#set($foo = 12 >= 11)').foo) - assert.equal(false, getContext('#set($foo = 10 == 11)').foo) - }) - + assert.equal(false, getContext('#set($foo = 10 > 11)').foo); + assert.equal(true, getContext('#set($foo = 10 < 11)').foo); + assert.equal(true, getContext('#set($foo = 10 != 11)').foo); + assert.equal(true, getContext('#set($foo = 10 <= 11)').foo); + assert.equal(true, getContext('#set($foo = 11 <= 11)').foo); + assert.equal(false, getContext('#set($foo = 12 <= 11)').foo); + assert.equal(true, getContext('#set($foo = 12 >= 11)').foo); + assert.equal(false, getContext('#set($foo = 10 == 11)').foo); + }); it('expression logic', function() { - assert.equal(false, getContext('#set($foo = 10 == 11 && 3 > 1)').foo) - assert.equal(true, getContext('#set($foo = 10 < 11 && 3 > 1)').foo) - assert.equal(true, getContext('#set($foo = 10 > 11 || 3 > 1)').foo) - assert.equal(true, getContext('#set($foo = !(10 > 11) && 3 > 1)').foo) - assert.equal(false, getContext('#set($foo = $a > $b)', {a: 1, b: 2}).foo) - assert.equal(false, getContext('#set($foo = $a && $b)', {a: 1, b: 0}).foo) - assert.equal(true, getContext('#set($foo = $a || $b)', {a: 1, b: 0}).foo) - }) - + assert.equal(false, getContext('#set($foo = 10 == 11 && 3 > 1)').foo); + assert.equal(true, getContext('#set($foo = 10 < 11 && 3 > 1)').foo); + assert.equal(true, getContext('#set($foo = 10 > 11 || 3 > 1)').foo); + assert.equal(true, getContext('#set($foo = !(10 > 11) && 3 > 1)').foo); + assert.equal(false, getContext('#set($foo = $a > $b)', { a: 1, b: 2 }).foo); + assert.equal(false, getContext('#set($foo = $a && $b)', { a: 1, b: 0 }).foo); + assert.equal(true, getContext('#set($foo = $a || $b)', { a: 1, b: 0 }).foo); + }); it('var in key', function() { - var vm = '#set($o = {}) #set($key = "k") #set($o[$key] = "c") #set($o.f = "d") $o $o[$key]' - var ret = render(vm).replace(/\s+/g, '') - assert.equal('{k=c,f=d}c', ret) + var vm = '#set($o = {}) #set($key = "k") #set($o[$key] = "c") #set($o.f = "d") $o $o[$key]'; + var ret = render(vm).replace(/\s+/g, ''); + assert.equal('{k=c,f=d}c', ret); var vm2 = ` #set($obj = {}) @@ -137,16 +130,16 @@ describe('Set && Expression', function() { #set($obj[$item.k] = $item) #end $obj - ` - var ret2 = render(vm2).replace(/\s+/g, '') + `; + var ret2 = render(vm2).replace(/\s+/g, ''); assert.equal('{a={k=a},b={k=b},c={k=c}}', ret2); - }) + }); it('#set context should be global, #25', function() { - var vm = '#macro(local) #set($val =1) $val #end #local() $val' - var ret = render(vm).replace(/\s+/g, '') - assert.equal('11', ret) - }) + var vm = '#macro(local) #set($val =1) $val #end #local() $val'; + var ret = render(vm).replace(/\s+/g, ''); + assert.equal('11', ret); + }); it('#set should support settinng string place holders', function() { var vm = ` @@ -154,10 +147,10 @@ describe('Set && Expression', function() { #set ($sortKeyValue = "Moo") #set($res = "$sortKeyValue#$val") $res - ` - var ret = render(vm).replace(/\s+/g, '') - assert.equal('Moo#Foo', ret) - }) + `; + var ret = render(vm).replace(/\s+/g, ''); + assert.equal('Moo#Foo', ret); + }); describe('set mult level var', function() { it('normal, fix #63', function() { @@ -165,10 +158,10 @@ describe('Set && Expression', function() { #set($a = { "b": {} }) #set($a.b.c1 = 1) #set($a.b.c2 = 2) - ` - var context = getContext(tpl) - context.a.b.should.have.properties('c1', 'c2') - }) + `; + var context = getContext(tpl); + context.a.b.should.have.properties('c1', 'c2'); + }); it('set fail', function() { var tpl = ` @@ -176,11 +169,11 @@ describe('Set && Expression', function() { #set($a.b.c1 = 1) #set($a.b.c2 = 2) #set($a.d.c2 = 2) - ` - var context = getContext(tpl) - context.a.should.not.have.property('d') - }) - }) + `; + var context = getContext(tpl); + context.a.should.not.have.property('d'); + }); + }); it('set with foreach', function() { var tpl = ` @@ -188,11 +181,11 @@ describe('Set && Expression', function() { #set($bTest = false) #if($item > 1) #set($bTest = true) #end

        $bTest

        -#end` - const html = render(tpl) - html.should.containEql('true') - html.should.containEql('false') - }) +#end`; + const html = render(tpl); + html.should.containEql('true'); + html.should.containEql('false'); + }); it('set error #78', function() { var tpl = ` @@ -204,9 +197,10 @@ describe('Set && Expression', function() { #set($bTest = "$!{bTest} test3")

        $bTest

        #end - ` - const html = render(tpl).replace(/\n\s.|\s{2}/g, '').trim(); + `; + const html = render(tpl) + .replace(/\n\s.|\s{2}/g, '') + .trim(); html.should.eql('

        test1 test3

        test1 test2 test3

        test1 test3

        test1 test2 test3

        '); - }) - -}) + }); +}); diff --git a/packages/amplify-velocity-template/tests/stop.js b/packages/amplify-velocity-template/tests/stop.js index cffda0913f..364d9b650f 100644 --- a/packages/amplify-velocity-template/tests/stop.js +++ b/packages/amplify-velocity-template/tests/stop.js @@ -6,7 +6,9 @@ var render = Velocity.render; describe('stop', function() { it('should support #stop', function() { var str = `hello #stop('hello') world`; - render(str).trim().should.eql('hello'); + render(str) + .trim() + .should.eql('hello'); }); it('should support #stop in loop', function() { @@ -18,7 +20,7 @@ describe('stop', function() { #end `; - var ret = render(str, { items: ['hello', 'world']}).trim(); + var ret = render(str, { items: ['hello', 'world'] }).trim(); ret.should.containEql('
      • hello
      • '); ret.should.not.containEql('
      • world
      • '); }); diff --git a/packages/graphql-auth-transformer/src/AuthRule.ts b/packages/graphql-auth-transformer/src/AuthRule.ts index 217c8a233b..b31a9757bd 100644 --- a/packages/graphql-auth-transformer/src/AuthRule.ts +++ b/packages/graphql-auth-transformer/src/AuthRule.ts @@ -4,15 +4,15 @@ export type ModelQuery = 'get' | 'list'; export type ModelMutation = 'create' | 'update' | 'delete'; export type ModelOperation = 'create' | 'update' | 'delete' | 'read'; export interface AuthRule { - allow: AuthStrategy; - provider?: AuthProvider; - ownerField?: string; - identityField?: string; - identityClaim?: string; - groupsField?: string; - groupClaim?: string; - groups?: string[]; - operations?: ModelOperation[]; - queries?: ModelQuery[]; - mutations?: ModelMutation[]; + allow: AuthStrategy; + provider?: AuthProvider; + ownerField?: string; + identityField?: string; + identityClaim?: string; + groupsField?: string; + groupClaim?: string; + groups?: string[]; + operations?: ModelOperation[]; + queries?: ModelQuery[]; + mutations?: ModelMutation[]; } diff --git a/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts b/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts index cb6a477c3a..6ae159590b 100644 --- a/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts +++ b/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts @@ -1,28 +1,44 @@ -import { Transformer, TransformerContext, InvalidDirectiveError, gql, getDirectiveArguments, getFieldArguments } from 'graphql-transformer-core' -import GraphQLAPI from 'cloudform-types/types/appSync/graphQlApi' +import { + Transformer, + TransformerContext, + InvalidDirectiveError, + gql, + getDirectiveArguments, + getFieldArguments, +} from 'graphql-transformer-core'; +import GraphQLAPI from 'cloudform-types/types/appSync/graphQlApi'; import Resolver from 'cloudform-types/types/appSync/resolver'; import { StringParameter } from 'cloudform-types'; -import { ResourceFactory } from './resources' -import { AuthRule, ModelQuery, ModelMutation, ModelOperation, AuthProvider } from './AuthRule' +import { ResourceFactory } from './resources'; +import { AuthRule, ModelQuery, ModelMutation, ModelOperation, AuthProvider } from './AuthRule'; import { - ObjectTypeDefinitionNode, DirectiveNode, ArgumentNode, Kind, - FieldDefinitionNode, InterfaceTypeDefinitionNode, valueFromASTUntyped, NamedTypeNode -} from 'graphql' -import { ResourceConstants, ResolverResourceIDs, isListType, - getBaseType, makeDirective, makeNamedType, makeInputValueDefinition, - blankObjectExtension, extensionWithDirectives, extendFieldWithDirectives, - makeNonNullType, makeField } from 'graphql-transformer-common' + ObjectTypeDefinitionNode, + DirectiveNode, + ArgumentNode, + Kind, + FieldDefinitionNode, + InterfaceTypeDefinitionNode, + valueFromASTUntyped, + NamedTypeNode, +} from 'graphql'; import { - Expression, print, raw, iff, forEach, set, ref, list, compoundExpression, or, newline, - comment -} from 'graphql-mapping-template'; + ResourceConstants, + ResolverResourceIDs, + isListType, + getBaseType, + makeDirective, + makeNamedType, + makeInputValueDefinition, + blankObjectExtension, + extensionWithDirectives, + extendFieldWithDirectives, + makeNonNullType, + makeField, +} from 'graphql-transformer-common'; +import { Expression, print, raw, iff, forEach, set, ref, list, compoundExpression, or, newline, comment } from 'graphql-mapping-template'; import { ModelDirectiveConfiguration, ModelDirectiveOperationType, ModelSubscriptionLevel } from './ModelDirectiveConfiguration'; -import { - OWNER_AUTH_STRATEGY, - GROUPS_AUTH_STRATEGY, - DEFAULT_OWNER_FIELD, -} from './constants'; +import { OWNER_AUTH_STRATEGY, GROUPS_AUTH_STRATEGY, DEFAULT_OWNER_FIELD } from './constants'; /** * Implements the ModelAuthTransformer. @@ -67,1998 +83,1998 @@ import { */ export type AppSyncAuthMode = 'API_KEY' | 'AMAZON_COGNITO_USER_POOLS' | 'AWS_IAM' | 'OPENID_CONNECT'; export type AppSyncAuthConfiguration = { - defaultAuthentication: AppSyncAuthConfigurationEntry - additionalAuthenticationProviders: Array + defaultAuthentication: AppSyncAuthConfigurationEntry; + additionalAuthenticationProviders: Array; }; export type AppSyncAuthConfigurationEntry = { - authenticationType: AppSyncAuthMode - apiKeyConfig?: ApiKeyConfig - userPoolConfig?: UserPoolConfig - openIDConnectConfig?: OpenIDConnectConfig -} + authenticationType: AppSyncAuthMode; + apiKeyConfig?: ApiKeyConfig; + userPoolConfig?: UserPoolConfig; + openIDConnectConfig?: OpenIDConnectConfig; +}; export type ApiKeyConfig = { - description?: string - apiKeyExpirationDays: number + description?: string; + apiKeyExpirationDays: number; }; export type UserPoolConfig = { - userPoolId: string + userPoolId: string; }; export type OpenIDConnectConfig = { - name: string - issuerUrl: string - clientId?: string - iatTTL?: number - authTTL?: number + name: string; + issuerUrl: string; + clientId?: string; + iatTTL?: number; + authTTL?: number; }; const validateAuthModes = (authConfig: AppSyncAuthConfiguration) => { - let additionalAuthModes = []; + let additionalAuthModes = []; - if (authConfig.additionalAuthenticationProviders) { - additionalAuthModes = authConfig.additionalAuthenticationProviders.map(p => p.authenticationType).filter(t => !!t); - } + if (authConfig.additionalAuthenticationProviders) { + additionalAuthModes = authConfig.additionalAuthenticationProviders.map(p => p.authenticationType).filter(t => !!t); + } - const authModes: AppSyncAuthMode[] = [...additionalAuthModes, authConfig.defaultAuthentication.authenticationType]; + const authModes: AppSyncAuthMode[] = [...additionalAuthModes, authConfig.defaultAuthentication.authenticationType]; - for (let i = 0; i < authModes.length; i++) { - const mode = authModes[i]; + for (let i = 0; i < authModes.length; i++) { + const mode = authModes[i]; - if ( - mode !== 'API_KEY' && - mode !== 'AMAZON_COGNITO_USER_POOLS' && - mode !== 'AWS_IAM' && - mode !== 'OPENID_CONNECT' - ) { - throw new Error(`Invalid auth mode ${mode}`); - } + if (mode !== 'API_KEY' && mode !== 'AMAZON_COGNITO_USER_POOLS' && mode !== 'AWS_IAM' && mode !== 'OPENID_CONNECT') { + throw new Error(`Invalid auth mode ${mode}`); } -} + } +}; export type ModelAuthTransformerConfig = { - authConfig?: AppSyncAuthConfiguration + authConfig?: AppSyncAuthConfiguration; }; export type ConfiguredAuthProviders = { - default: AuthProvider, - onlyDefaultAuthProviderConfigured: boolean, - hasApiKey: boolean, - hasUserPools: boolean, - hasOIDC: boolean, - hasIAM: boolean + default: AuthProvider; + onlyDefaultAuthProviderConfigured: boolean; + hasApiKey: boolean; + hasUserPools: boolean; + hasOIDC: boolean; + hasIAM: boolean; }; export class ModelAuthTransformer extends Transformer { - resources: ResourceFactory; - config: ModelAuthTransformerConfig; - configuredAuthProviders: ConfiguredAuthProviders; - generateIAMPolicyforUnauthRole: boolean; - generateIAMPolicyforAuthRole: boolean; - authPolicyResources: Set; - unauthPolicyResources: Set; - - constructor(config?: ModelAuthTransformerConfig) { - super( - 'ModelAuthTransformer', - gql` - directive @auth(rules: [AuthRule!]!) on OBJECT | FIELD_DEFINITION - input AuthRule { - # Specifies the auth rule's strategy. Allowed values are 'owner', 'groups', 'public', 'private'. - allow: AuthStrategy! - - # Legacy name for identityClaim - identityField: String - @deprecated(reason: "The 'identityField' argument is replaced by the 'identityClaim'.") - - # Specifies the name of the provider to use for the rule. This overrides the default provider - # when 'public' and 'private' AuthStrategy is used. Specifying a provider for 'owner' or 'groups' - # are not allowed. - provider: AuthProvider - - # Specifies the name of the claim to look for on the request's JWT token - # from Cognito User Pools (and in the future OIDC) that contains the identity - # of the user. If 'allow' is 'groups', this value should point to a list of groups - # in the claims. If 'allow' is 'owner', this value should point to the logged in user identity string. - # Defaults to "cognito:username" for Cognito User Pools auth. - identityClaim: String - - # Allows for custom config of 'groups' which is validated against the JWT - # Specifies a static list of groups that should have access to the object - groupClaim: String - - # Allowed when the 'allow' argument is 'owner'. - # Specifies the field of type String or [String] that contains owner(s) that can access the object. - ownerField: String # defaults to "owner" - - # Allowed when the 'allow' argument is 'groups'. - # Specifies the field of type String or [String] that contains group(s) that can access the object. - groupsField: String - - # Allowed when the 'allow' argument is 'groups'. - # Specifies a static list of groups that should have access to the object. - groups: [String] - - # Specifies operations to which this auth rule should be applied. - operations: [ModelOperation] - - # Deprecated. It is recommended to use the 'operations' arguments. - queries: [ModelQuery] - @deprecated(reason: "The 'queries' argument will be replaced by the 'operations' argument in a future release.") - - # Deprecated. It is recommended to use the 'operations' arguments. - mutations: [ModelMutation] - @deprecated(reason: "The 'mutations' argument will be replaced by the 'operations' argument in a future release.") - } - enum AuthStrategy { owner groups private public } - enum AuthProvider { apiKey iam oidc userPools } - enum ModelOperation { create update delete read } - enum ModelQuery - @deprecated(reason: "ModelQuery will be replaced by the 'ModelOperation' in a future release.") - { - get - list - } - enum ModelMutation - @deprecated(reason: "ModelMutation will be replaced by the 'ModelOperation' in a future release.") - { - create - update - delete - } - ` - ) - - if (config && config.authConfig) { - this.config = config; - if (!this.config.authConfig.additionalAuthenticationProviders) { - this.config.authConfig.additionalAuthenticationProviders = []; - } - } else { - this.config = { authConfig: { defaultAuthentication: { authenticationType: 'API_KEY' }, additionalAuthenticationProviders: [] } }; - } - validateAuthModes(this.config.authConfig); - this.resources = new ResourceFactory(); - this.configuredAuthProviders = this.getConfiguredAuthProviders(); - this.generateIAMPolicyforUnauthRole = false; - this.generateIAMPolicyforAuthRole = false; - this.authPolicyResources = new Set(); - this.unauthPolicyResources = new Set(); + resources: ResourceFactory; + config: ModelAuthTransformerConfig; + configuredAuthProviders: ConfiguredAuthProviders; + generateIAMPolicyforUnauthRole: boolean; + generateIAMPolicyforAuthRole: boolean; + authPolicyResources: Set; + unauthPolicyResources: Set; + + constructor(config?: ModelAuthTransformerConfig) { + super( + 'ModelAuthTransformer', + gql` + directive @auth(rules: [AuthRule!]!) on OBJECT | FIELD_DEFINITION + input AuthRule { + # Specifies the auth rule's strategy. Allowed values are 'owner', 'groups', 'public', 'private'. + allow: AuthStrategy! + + # Legacy name for identityClaim + identityField: String @deprecated(reason: "The 'identityField' argument is replaced by the 'identityClaim'.") + + # Specifies the name of the provider to use for the rule. This overrides the default provider + # when 'public' and 'private' AuthStrategy is used. Specifying a provider for 'owner' or 'groups' + # are not allowed. + provider: AuthProvider + + # Specifies the name of the claim to look for on the request's JWT token + # from Cognito User Pools (and in the future OIDC) that contains the identity + # of the user. If 'allow' is 'groups', this value should point to a list of groups + # in the claims. If 'allow' is 'owner', this value should point to the logged in user identity string. + # Defaults to "cognito:username" for Cognito User Pools auth. + identityClaim: String + + # Allows for custom config of 'groups' which is validated against the JWT + # Specifies a static list of groups that should have access to the object + groupClaim: String + + # Allowed when the 'allow' argument is 'owner'. + # Specifies the field of type String or [String] that contains owner(s) that can access the object. + ownerField: String # defaults to "owner" + # Allowed when the 'allow' argument is 'groups'. + # Specifies the field of type String or [String] that contains group(s) that can access the object. + groupsField: String + + # Allowed when the 'allow' argument is 'groups'. + # Specifies a static list of groups that should have access to the object. + groups: [String] + + # Specifies operations to which this auth rule should be applied. + operations: [ModelOperation] + + # Deprecated. It is recommended to use the 'operations' arguments. + queries: [ModelQuery] + @deprecated(reason: "The 'queries' argument will be replaced by the 'operations' argument in a future release.") + + # Deprecated. It is recommended to use the 'operations' arguments. + mutations: [ModelMutation] + @deprecated(reason: "The 'mutations' argument will be replaced by the 'operations' argument in a future release.") + } + enum AuthStrategy { + owner + groups + private + public + } + enum AuthProvider { + apiKey + iam + oidc + userPools + } + enum ModelOperation { + create + update + delete + read + } + enum ModelQuery @deprecated(reason: "ModelQuery will be replaced by the 'ModelOperation' in a future release.") { + get + list + } + enum ModelMutation @deprecated(reason: "ModelMutation will be replaced by the 'ModelOperation' in a future release.") { + create + update + delete + } + ` + ); + + if (config && config.authConfig) { + this.config = config; + if (!this.config.authConfig.additionalAuthenticationProviders) { + this.config.authConfig.additionalAuthenticationProviders = []; + } + } else { + this.config = { authConfig: { defaultAuthentication: { authenticationType: 'API_KEY' }, additionalAuthenticationProviders: [] } }; + } + validateAuthModes(this.config.authConfig); + this.resources = new ResourceFactory(); + this.configuredAuthProviders = this.getConfiguredAuthProviders(); + this.generateIAMPolicyforUnauthRole = false; + this.generateIAMPolicyforAuthRole = false; + this.authPolicyResources = new Set(); + this.unauthPolicyResources = new Set(); + } + + /** + * Updates the GraphQL API record with configured authentication providers + */ + private updateAPIAuthentication = (ctx: TransformerContext): void => { + const apiRecord = ctx.getResource(ResourceConstants.RESOURCES.GraphQLAPILogicalID) as GraphQLAPI; + const updated = this.resources.updateGraphQLAPIWithAuth(apiRecord, this.config.authConfig); + ctx.setResource(ResourceConstants.RESOURCES.GraphQLAPILogicalID, updated); + + // Check if we need to create an API key resource or not. + }; + + public before = (ctx: TransformerContext): void => { + const template = this.resources.initTemplate(this.getApiKeyConfig()); + ctx.mergeResources(template.Resources); + ctx.mergeParameters(template.Parameters); + ctx.mergeOutputs(template.Outputs); + ctx.mergeConditions(template.Conditions); + this.updateAPIAuthentication(ctx); + }; + + public after = (ctx: TransformerContext): void => { + if (this.generateIAMPolicyforAuthRole === true) { + // Sanity check to make sure we're not generating invalid policies, where no resources are defined. + if (this.authPolicyResources.size === 0) { + throw new Error('AuthRole policies should be generated, but no resources were added'); + } + + ctx.mergeParameters({ + [ResourceConstants.PARAMETERS.AuthRoleName]: new StringParameter({ + Description: 'Reference to the name of the Auth Role created for the project.', + }), + }); + + ctx.mergeResources({ + [ResourceConstants.RESOURCES.AuthRolePolicy]: this.resources.makeIAMPolicyForRole(true, this.authPolicyResources), + }); } - /** - * Updates the GraphQL API record with configured authentication providers - */ - private updateAPIAuthentication = (ctx: TransformerContext): void => { - const apiRecord = ctx.getResource(ResourceConstants.RESOURCES.GraphQLAPILogicalID) as GraphQLAPI; - const updated = this.resources.updateGraphQLAPIWithAuth(apiRecord, this.config.authConfig); - ctx.setResource(ResourceConstants.RESOURCES.GraphQLAPILogicalID, updated); - - // Check if we need to create an API key resource or not. + if (this.generateIAMPolicyforUnauthRole === true) { + // Sanity check to make sure we're not generating invalid policies, where no resources are defined. + if (this.unauthPolicyResources.size === 0) { + throw new Error('UnauthRole policies should be generated, but no resources were added'); + } + + ctx.mergeParameters({ + [ResourceConstants.PARAMETERS.UnauthRoleName]: new StringParameter({ + Description: 'Reference to the name of the Unauth Role created for the project.', + }), + }); + + ctx.mergeResources({ + [ResourceConstants.RESOURCES.UnauthRolePolicy]: this.resources.makeIAMPolicyForRole(false, this.unauthPolicyResources), + }); } + }; + + private getApiKeyConfig(): ApiKeyConfig { + const authProviders = []; - public before = (ctx: TransformerContext): void => { - const template = this.resources.initTemplate(this.getApiKeyConfig()); - ctx.mergeResources(template.Resources) - ctx.mergeParameters(template.Parameters) - ctx.mergeOutputs(template.Outputs) - ctx.mergeConditions(template.Conditions) - this.updateAPIAuthentication(ctx) + if (this.config.authConfig.additionalAuthenticationProviders) { + authProviders.concat(this.config.authConfig.additionalAuthenticationProviders.filter(p => !!p.authenticationType)); } - public after = (ctx: TransformerContext): void => { - if (this.generateIAMPolicyforAuthRole === true) { - // Sanity check to make sure we're not generating invalid policies, where no resources are defined. - if (this.authPolicyResources.size === 0) { - throw new Error('AuthRole policies should be generated, but no resources were added'); - } + authProviders.push(this.config.authConfig.defaultAuthentication); - ctx.mergeParameters({ - [ResourceConstants.PARAMETERS.AuthRoleName]: new StringParameter({ - Description: 'Reference to the name of the Auth Role created for the project.' - }), - }); - - ctx.mergeResources( - { - [ResourceConstants.RESOURCES.AuthRolePolicy]: - this.resources.makeIAMPolicyForRole(true, this.authPolicyResources) - } - ); - } + const apiKeyAuthProvider = authProviders.find(p => p.authenticationType === 'API_KEY'); - if (this.generateIAMPolicyforUnauthRole === true) { - // Sanity check to make sure we're not generating invalid policies, where no resources are defined. - if (this.unauthPolicyResources.size === 0) { - throw new Error('UnauthRole policies should be generated, but no resources were added'); - } + // Return the found instance or a default instance with 7 days of API key expiration + return apiKeyAuthProvider ? apiKeyAuthProvider.apiKeyConfig : { apiKeyExpirationDays: 7 }; + } - ctx.mergeParameters({ - [ResourceConstants.PARAMETERS.UnauthRoleName]: new StringParameter({ - Description: 'Reference to the name of the Unauth Role created for the project.' - }), - }); - - ctx.mergeResources( - { - [ResourceConstants.RESOURCES.UnauthRolePolicy]: - this.resources.makeIAMPolicyForRole(false, this.unauthPolicyResources) - } - ); - } + /** + * Implement the transform for an object type. Depending on which operations are to be protected + */ + public object = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext): void => { + const modelDirective = def.directives.find(dir => dir.name.value === 'model'); + if (!modelDirective) { + throw new InvalidDirectiveError('Types annotated with @auth must also be annotated with @model.'); } - private getApiKeyConfig(): ApiKeyConfig { - const authProviders = []; + // check if searchable is enabled on the type + const searchableDirective = def.directives.find(dir => dir.name.value === 'searchable'); - if (this.config.authConfig.additionalAuthenticationProviders) { - authProviders.concat (this.config.authConfig.additionalAuthenticationProviders.filter(p => !!p.authenticationType)); - } + // Get and validate the auth rules. + const rules = this.getAuthRulesFromDirective(directive); + // Assign default providers to rules where no provider was explicitly defined + this.ensureDefaultAuthProviderAssigned(rules); + this.validateRules(rules); + // Check the rules if we've to generate IAM policies for Unauth role or not + this.setAuthPolicyFlag(rules); + this.setUnauthPolicyFlag(rules); - authProviders.push(this.config.authConfig.defaultAuthentication); + const { operationRules, queryRules } = this.splitRules(rules); - const apiKeyAuthProvider = authProviders.find(p => p.authenticationType === 'API_KEY'); + // Retrieve the configuration options for the related @model directive + const modelConfiguration = new ModelDirectiveConfiguration(modelDirective, def); + // Get the directives we need to add to the GraphQL nodes + const directives = this.getDirectivesForRules(rules, false); - // Return the found instance or a default instance with 7 days of API key expiration - return apiKeyAuthProvider ? apiKeyAuthProvider.apiKeyConfig : { apiKeyExpirationDays: 7 }; + // Add the directives to the Type node itself + if (directives.length > 0) { + this.extendTypeWithDirectives(ctx, def.name.value, directives); } - /** - * Implement the transform for an object type. Depending on which operations are to be protected - */ - public object = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext): void => { - const modelDirective = def.directives.find((dir) => dir.name.value === 'model') - if (!modelDirective) { - throw new InvalidDirectiveError('Types annotated with @auth must also be annotated with @model.') - } + this.addTypeToResourceReferences(def.name.value, rules); + + // For each operation evaluate the rules and apply the changes to the relevant resolver. + this.protectCreateMutation( + ctx, + ResolverResourceIDs.DynamoDBCreateResolverResourceID(def.name.value), + operationRules.create, + def, + modelConfiguration + ); + this.protectUpdateMutation( + ctx, + ResolverResourceIDs.DynamoDBUpdateResolverResourceID(def.name.value), + operationRules.update, + def, + modelConfiguration + ); + this.protectDeleteMutation( + ctx, + ResolverResourceIDs.DynamoDBDeleteResolverResourceID(def.name.value), + operationRules.delete, + def, + modelConfiguration + ); + this.protectGetQuery(ctx, ResolverResourceIDs.DynamoDBGetResolverResourceID(def.name.value), queryRules.get, def, modelConfiguration); + this.protectListQuery( + ctx, + ResolverResourceIDs.DynamoDBListResolverResourceID(def.name.value), + queryRules.list, + def, + modelConfiguration + ); + this.protectConnections(ctx, def, operationRules.read, modelConfiguration); + this.protectQueries(ctx, def, operationRules.read, modelConfiguration); + + // protect search query if @searchable is enabled + if (searchableDirective) { + this.protectSearchQuery(ctx, def, ResolverResourceIDs.ElasticsearchSearchResolverResourceID(def.name.value), operationRules.read); + } - // check if searchable is enabled on the type - const searchableDirective = def.directives.find((dir) => dir.name.value === 'searchable') + // Check if subscriptions is enabled + if (modelConfiguration.getName('level') !== 'off') { + this.protectOnCreateSubscription(ctx, operationRules.create, def, modelConfiguration); + this.protectOnUpdateSubscription(ctx, operationRules.update, def, modelConfiguration); + this.protectOnDeleteSubscription(ctx, operationRules.delete, def, modelConfiguration); + } + }; + + public field = ( + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + definition: FieldDefinitionNode, + directive: DirectiveNode, + ctx: TransformerContext + ) => { + if (parent.kind === Kind.INTERFACE_TYPE_DEFINITION) { + throw new InvalidDirectiveError( + `The @auth directive cannot be placed on an interface's field. See ${parent.name.value}${definition.name.value}` + ); + } + const modelDirective = parent.directives.find(dir => dir.name.value === 'model'); + if (!modelDirective) { + throw new InvalidDirectiveError('Types annotated with @auth must also be annotated with @model.'); + } + + // Retrieve the configuration options for the related @model directive + const modelConfiguration = new ModelDirectiveConfiguration(modelDirective, parent); + if ( + parent.name.value === ctx.getQueryTypeName() || + parent.name.value === ctx.getMutationTypeName() || + parent.name.value === ctx.getSubscriptionTypeName() + ) { + console.warn( + `Be careful when using @auth directives on a field in a root type. @auth directives on field definitions use the source \ +object to perform authorization logic and the source will be an empty object for fields on root types. \ +Static group authorization should perform as expected.` + ); + } - // Get and validate the auth rules. - const rules = this.getAuthRulesFromDirective(directive); - // Assign default providers to rules where no provider was explicitly defined - this.ensureDefaultAuthProviderAssigned(rules); - this.validateRules(rules); - // Check the rules if we've to generate IAM policies for Unauth role or not - this.setAuthPolicyFlag(rules); - this.setUnauthPolicyFlag(rules); + // Get and validate the auth rules. + const rules = this.getAuthRulesFromDirective(directive); + // Assign default providers to rules where no provider was explicitly defined + this.ensureDefaultAuthProviderAssigned(rules); + this.validateFieldRules(rules); + // Check the rules if we've to generate IAM policies for Unauth role or not + this.setAuthPolicyFlag(rules); + this.setUnauthPolicyFlag(rules); + + this.addFieldToResourceReferences(parent.name.value, definition.name.value, rules); + + // Add the directives to the parent type as well, we've to add the default provider if + // - The type has no @auth directives, so there are NO restrictions on the type + // or + // - The type has @auth rules for the default provider + const includeDefault = this.isTypeNeedsDefaultProviderAccess(parent); + const typeDirectives = this.getDirectivesForRules(rules, includeDefault); + + if (typeDirectives.length > 0) { + this.extendTypeWithDirectives(ctx, parent.name.value, typeDirectives); + } - const { operationRules, queryRules } = this.splitRules(rules); + const isOpRule = (op: ModelOperation) => (rule: AuthRule) => { + if (rule.operations) { + const matchesOp = rule.operations.find(o => o === op); + return Boolean(matchesOp); + } else if (rule.operations === null) { + return false; + } + return true; + }; + const isReadRule = isOpRule('read'); + const isCreateRule = isOpRule('create'); + const isUpdateRule = isOpRule('update'); + const isDeleteRule = isOpRule('delete'); + // The field handler adds the read rule on the object + const readRules = rules.filter((rule: AuthRule) => isReadRule(rule)); + this.protectReadForField(ctx, parent, definition, readRules, modelConfiguration); + + // Protect mutations when objects including this field are trying to be created. + const createRules = rules.filter((rule: AuthRule) => isCreateRule(rule)); + this.protectCreateForField(ctx, parent, definition, createRules, modelConfiguration); + + // Protect update mutations when objects inluding this field are trying to be updated. + const updateRules = rules.filter((rule: AuthRule) => isUpdateRule(rule)); + this.protectUpdateForField(ctx, parent, definition, updateRules, modelConfiguration); + + // Delete operations are only protected by @auth directives on objects. + const deleteRules = rules.filter((rule: AuthRule) => isDeleteRule(rule)); + this.protectDeleteForField(ctx, parent, definition, deleteRules, modelConfiguration); + }; + + private protectReadForField( + ctx: TransformerContext, + parent: ObjectTypeDefinitionNode, + field: FieldDefinitionNode, + rules: AuthRule[], + modelConfiguration: ModelDirectiveConfiguration + ) { + if (rules && rules.length) { + // Get the directives we need to add to the GraphQL nodes + const directives = this.getDirectivesForRules(rules, false); + + if (directives.length > 0) { + this.addDirectivesToField(ctx, parent.name.value, field.name.value, directives); + + const addDirectivesForOperation = (operationType: ModelDirectiveOperationType) => { + if (modelConfiguration.shouldHave(operationType)) { + const operationName = modelConfiguration.getName(operationType); + // If the parent type has any rules for this operation AND + // the default provider we've to get directives including the default + // as well. + const includeDefault = this.isTypeHasRulesForOperation(parent, operationType); + const operationDirectives = this.getDirectivesForRules(rules, includeDefault); + + this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); + } + }; - // Retrieve the configuration options for the related @model directive - const modelConfiguration = new ModelDirectiveConfiguration (modelDirective, def); - // Get the directives we need to add to the GraphQL nodes - const directives = this.getDirectivesForRules(rules, false); + addDirectivesForOperation('get'); + addDirectivesForOperation('list'); + } - // Add the directives to the Type node itself - if (directives.length > 0) { - this.extendTypeWithDirectives(ctx, def.name.value, directives); + const addResourceReference = (operationType: ModelDirectiveOperationType) => { + if (modelConfiguration.shouldHave(operationType)) { + const operationName = modelConfiguration.getName(operationType); + this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); } + }; - this.addTypeToResourceReferences(def.name.value, rules); - - // For each operation evaluate the rules and apply the changes to the relevant resolver. - this.protectCreateMutation(ctx, ResolverResourceIDs.DynamoDBCreateResolverResourceID(def.name.value), operationRules.create, def, - modelConfiguration); - this.protectUpdateMutation(ctx, ResolverResourceIDs.DynamoDBUpdateResolverResourceID(def.name.value), operationRules.update, def, - modelConfiguration); - this.protectDeleteMutation(ctx, ResolverResourceIDs.DynamoDBDeleteResolverResourceID(def.name.value), operationRules.delete, def, - modelConfiguration); - this.protectGetQuery(ctx, ResolverResourceIDs.DynamoDBGetResolverResourceID(def.name.value), queryRules.get, def, modelConfiguration); - this.protectListQuery(ctx, ResolverResourceIDs.DynamoDBListResolverResourceID(def.name.value), queryRules.list, def, modelConfiguration); - this.protectConnections(ctx, def, operationRules.read, modelConfiguration); - this.protectQueries(ctx, def, operationRules.read, modelConfiguration); - - // protect search query if @searchable is enabled - if (searchableDirective) { - this.protectSearchQuery(ctx, def, - ResolverResourceIDs.ElasticsearchSearchResolverResourceID(def.name.value), operationRules.read) - } + addResourceReference('get'); + addResourceReference('list'); - // Check if subscriptions is enabled - if (modelConfiguration.getName('level') !== "off") { - this.protectOnCreateSubscription(ctx, operationRules.create, def, - modelConfiguration); - this.protectOnUpdateSubscription(ctx, operationRules.update, def, - modelConfiguration); - this.protectOnDeleteSubscription(ctx, operationRules.delete, def, - modelConfiguration); + const resolverResourceId = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value); + // If the resolver exists (e.g. @connection use it else make a blank one against None) + let resolver = ctx.getResource(resolverResourceId); + if (!resolver) { + // If we need a none data source for the blank resolver, add it. + const noneDS = ctx.getResource(ResourceConstants.RESOURCES.NoneDataSource); + if (!noneDS) { + ctx.setResource(ResourceConstants.RESOURCES.NoneDataSource, this.resources.noneDataSource()); } + // We also need to add a stack mapping so that this resolver is added to the model stack. + ctx.mapResourceToStack(parent.name.value, resolverResourceId); + resolver = this.resources.blankResolver(parent.name.value, field.name.value); + } + const authExpression = this.authorizationExpressionOnSingleObject(rules, 'ctx.source'); + // if subscriptions auth is enabled protect this field by checking for the operation + // if the operation is a mutation then we deny the a read operation on the field + if (modelConfiguration.getName('level') === 'on') { + if (field.type.kind === Kind.NON_NULL_TYPE) { + throw new InvalidDirectiveError(`\nPer-field auth on the required field ${field.name.value} is not supported with subscriptions. +Either make the field optional, set auth on the object and not the field, or disable subscriptions for the object (setting level to off or public)\n`); + } + // operation check in the protected field + resolver.Properties.ResponseMappingTemplate = print( + this.resources.operationCheckExpression(ctx.getMutationTypeName(), field.name.value) + ); + } + // If a resolver exists, a @connection for example. Prepend it to the req. + const templateParts = [print(authExpression), resolver.Properties.RequestMappingTemplate]; + resolver.Properties.RequestMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, resolver); } + } + + private protectUpdateForField( + ctx: TransformerContext, + parent: ObjectTypeDefinitionNode, + field: FieldDefinitionNode, + rules: AuthRule[], + modelConfiguration: ModelDirectiveConfiguration + ) { + const resolverResourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(parent.name.value); + const subscriptionOperation: ModelDirectiveOperationType = 'onUpdate'; + this.protectUpdateMutation(ctx, resolverResourceId, rules, parent, modelConfiguration, field, subscriptionOperation); + } + + private protectDeleteForField( + ctx: TransformerContext, + parent: ObjectTypeDefinitionNode, + field: FieldDefinitionNode, + rules: AuthRule[], + modelConfiguration: ModelDirectiveConfiguration + ) { + const resolverResourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(parent.name.value); + const subscriptionOperation: ModelDirectiveOperationType = 'onDelete'; + this.protectDeleteMutation(ctx, resolverResourceId, rules, parent, modelConfiguration, field, subscriptionOperation); + } + + /** + * Protects a create mutation based on an @auth rule specified on a @model field. + * @param ctx The context. + * @param typeName The parent type name. + * @param fieldName The name of the field with the @auth directive. + * @param rules The set of rules that should be applied to create operations. + */ + private protectCreateForField( + ctx: TransformerContext, + parent: ObjectTypeDefinitionNode, + field: FieldDefinitionNode, + rules: AuthRule[], + modelConfiguration: ModelDirectiveConfiguration + ) { + const typeName = parent.name.value; + const resolverResourceId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName); + const createResolverResource = ctx.getResource(resolverResourceId); + const mutationTypeName = ctx.getMutationTypeName(); + if (rules && rules.length && createResolverResource) { + // Get the directives we need to add to the GraphQL nodes + const directives = this.getDirectivesForRules(rules, false); + let operationName: string = undefined; + + if (directives.length > 0) { + this.addDirectivesToField(ctx, typeName, field.name.value, directives); + + if (modelConfiguration.shouldHave('create')) { + // If the parent type has any rules for this operation AND + // the default provider we've to get directives including the default + // as well. + const includeDefault = this.isTypeHasRulesForOperation(parent, 'create'); + const operationDirectives = this.getDirectivesForRules(rules, includeDefault); + + operationName = modelConfiguration.getName('create'); + + this.addDirectivesToOperation(ctx, mutationTypeName, operationName, operationDirectives); + } + } + + if (operationName) { + this.addFieldToResourceReferences(mutationTypeName, operationName, rules); + } + + // Break the rules out by strategy. + const staticGroupAuthorizationRules = this.getStaticGroupRules(rules); + const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules); + const ownerAuthorizationRules = this.getOwnerRules(rules); + + if (staticGroupAuthorizationRules.length > 0 || dynamicGroupAuthorizationRules.length > 0 || ownerAuthorizationRules.length > 0) { + // Generate the expressions to validate each strategy. + const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules, field); + + // In create mutations, the dynamic group and ownership authorization checks + // are done before calling PutItem. + const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForCreateOperationsByField( + dynamicGroupAuthorizationRules, + field.name.value + ); + const fieldIsList = (fieldName: string) => { + const field = parent.fields.find(field => field.name.value === fieldName); + if (field) { + return isListType(field.type); + } + return false; + }; + const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForCreateOperationsByField( + ownerAuthorizationRules, + field.name.value, + fieldIsList + ); - public field = ( - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - definition: FieldDefinitionNode, - directive: DirectiveNode, - ctx: TransformerContext - ) => { - if (parent.kind === Kind.INTERFACE_TYPE_DEFINITION) { - throw new InvalidDirectiveError( - `The @auth directive cannot be placed on an interface's field. See ${parent.name.value}${definition.name.value}` - ); - } - const modelDirective = parent.directives.find((dir) => dir.name.value === 'model') - if (!modelDirective) { - throw new InvalidDirectiveError('Types annotated with @auth must also be annotated with @model.') - } + const throwIfUnauthorizedExpression = this.resources.throwIfUnauthorized(field); + + // Populate a list of configured authentication providers based on the rules + const authModesToCheck = new Set(); + const expressions: Array = new Array(); - // Retrieve the configuration options for the related @model directive - const modelConfiguration = new ModelDirectiveConfiguration (modelDirective, parent); if ( - parent.name.value === ctx.getQueryTypeName() || - parent.name.value === ctx.getMutationTypeName() || - parent.name.value === ctx.getSubscriptionTypeName() + ownerAuthorizationRules.find(r => r.provider === 'userPools') || + staticGroupAuthorizationRules.length > 0 || + dynamicGroupAuthorizationRules.length > 0 ) { - console.warn( - `Be careful when using @auth directives on a field in a root type. @auth directives on field definitions use the source \ -object to perform authorization logic and the source will be an empty object for fields on root types. \ -Static group authorization should perform as expected.` - ) + authModesToCheck.add('userPools'); } - - // Get and validate the auth rules. - const rules = this.getAuthRulesFromDirective(directive); - // Assign default providers to rules where no provider was explicitly defined - this.ensureDefaultAuthProviderAssigned(rules); - this.validateFieldRules(rules); - // Check the rules if we've to generate IAM policies for Unauth role or not - this.setAuthPolicyFlag(rules); - this.setUnauthPolicyFlag(rules); - - this.addFieldToResourceReferences(parent.name.value, definition.name.value, rules); - - // Add the directives to the parent type as well, we've to add the default provider if - // - The type has no @auth directives, so there are NO restrictions on the type - // or - // - The type has @auth rules for the default provider - const includeDefault = this.isTypeNeedsDefaultProviderAccess(parent); - const typeDirectives = this.getDirectivesForRules(rules, includeDefault); - - if (typeDirectives.length > 0) { - this.extendTypeWithDirectives(ctx, parent.name.value, typeDirectives); + if (ownerAuthorizationRules.find(r => r.provider === 'oidc')) { + authModesToCheck.add('oidc'); } - const isOpRule = (op: ModelOperation) => (rule: AuthRule) => { - if (rule.operations) { - const matchesOp = rule.operations.find(o => o === op) - return Boolean(matchesOp) - } else if (rule.operations === null) { - return false - } - return true + // If we've any modes to check, then add the authMode check code block + // to the start of the resolver. + if (authModesToCheck.size > 0) { + expressions.push(this.resources.getAuthModeDeterminationExpression(authModesToCheck)); } - const isReadRule = isOpRule('read'); - const isCreateRule = isOpRule('create'); - const isUpdateRule = isOpRule('update'); - const isDeleteRule = isOpRule('delete'); - // The field handler adds the read rule on the object - const readRules = rules.filter((rule: AuthRule) => isReadRule(rule)) - this.protectReadForField(ctx, parent, definition, readRules, modelConfiguration); - - // Protect mutations when objects including this field are trying to be created. - const createRules = rules.filter((rule: AuthRule) => isCreateRule(rule)) - this.protectCreateForField(ctx, parent, definition, createRules, modelConfiguration) - - // Protect update mutations when objects inluding this field are trying to be updated. - const updateRules = rules.filter((rule: AuthRule) => isUpdateRule(rule)) - this.protectUpdateForField(ctx, parent, definition, updateRules, modelConfiguration) - - // Delete operations are only protected by @auth directives on objects. - const deleteRules = rules.filter((rule: AuthRule) => isDeleteRule(rule)) - this.protectDeleteForField(ctx, parent, definition, deleteRules, modelConfiguration) - } - private protectReadForField(ctx: TransformerContext, parent: ObjectTypeDefinitionNode, field: FieldDefinitionNode, rules: AuthRule[], - modelConfiguration: ModelDirectiveConfiguration) { - if (rules && rules.length) { + // These statements will be wrapped into an authMode check if statement + const authCheckExpressions = [ + staticGroupAuthorizationExpression, + newline(), + dynamicGroupAuthorizationExpression, + newline(), + ownerAuthorizationExpression, + newline(), + throwIfUnauthorizedExpression, + ]; - // Get the directives we need to add to the GraphQL nodes - const directives = this.getDirectivesForRules(rules, false); + // Create the authMode if block and add it to the resolver + expressions.push(this.resources.getAuthModeCheckWrappedExpression(authModesToCheck, compoundExpression(authCheckExpressions))); - if (directives.length > 0) { - this.addDirectivesToField(ctx, parent.name.value, field.name.value, directives); - - const addDirectivesForOperation = (operationType: ModelDirectiveOperationType) => { - if (modelConfiguration.shouldHave(operationType)) { - const operationName = modelConfiguration.getName(operationType); - // If the parent type has any rules for this operation AND - // the default provider we've to get directives including the default - // as well. - const includeDefault = this.isTypeHasRulesForOperation(parent, operationType); - const operationDirectives = this.getDirectivesForRules(rules, includeDefault); - - this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); - } - } - - addDirectivesForOperation('get'); - addDirectivesForOperation('list'); - } + const templateParts = [ + print(iff(raw(`$ctx.args.input.containsKey("${field.name.value}")`), compoundExpression(expressions))), + createResolverResource.Properties.RequestMappingTemplate, + ]; + createResolverResource.Properties.RequestMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, createResolverResource); + } - const addResourceReference = (operationType: ModelDirectiveOperationType) => { - if (modelConfiguration.shouldHave(operationType)) { - const operationName = modelConfiguration.getName(operationType); - this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); - } - }; - - addResourceReference('get'); - addResourceReference('list'); - - const resolverResourceId = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value); - // If the resolver exists (e.g. @connection use it else make a blank one against None) - let resolver = ctx.getResource(resolverResourceId) - if (!resolver) { - // If we need a none data source for the blank resolver, add it. - const noneDS = ctx.getResource(ResourceConstants.RESOURCES.NoneDataSource) - if (!noneDS) { - ctx.setResource(ResourceConstants.RESOURCES.NoneDataSource, this.resources.noneDataSource()) - } - // We also need to add a stack mapping so that this resolver is added to the model stack. - ctx.mapResourceToStack(parent.name.value, resolverResourceId) - resolver = this.resources.blankResolver(parent.name.value, field.name.value) - } - const authExpression = this.authorizationExpressionOnSingleObject(rules, 'ctx.source') - // if subscriptions auth is enabled protect this field by checking for the operation - // if the operation is a mutation then we deny the a read operation on the field - if (modelConfiguration.getName('level') === 'on') { - if (field.type.kind === Kind.NON_NULL_TYPE) { - throw new InvalidDirectiveError(`\nPer-field auth on the required field ${field.name.value} is not supported with subscriptions. -Either make the field optional, set auth on the object and not the field, or disable subscriptions for the object (setting level to off or public)\n`) - } - // operation check in the protected field - resolver.Properties.ResponseMappingTemplate = print( - this.resources.operationCheckExpression(ctx.getMutationTypeName(), field.name.value)); - } - // If a resolver exists, a @connection for example. Prepend it to the req. - const templateParts = [ - print(authExpression), - resolver.Properties.RequestMappingTemplate - ] - resolver.Properties.RequestMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, resolver) + // if subscriptions is enabled the operation is specified in the mutation response resolver + if (modelConfiguration.shouldHave('onCreate') && (modelConfiguration.getName('level') as ModelSubscriptionLevel) === 'on') { + const getTemplateParts = [createResolverResource.Properties.ResponseMappingTemplate]; + + if (!this.isOperationExpressionSet(mutationTypeName, createResolverResource.Properties.ResponseMappingTemplate)) { + getTemplateParts.unshift(this.resources.setOperationExpression(mutationTypeName)); } + createResolverResource.Properties.ResponseMappingTemplate = getTemplateParts.join('\n\n'); + ctx.setResource(resolverResourceId, createResolverResource); + } } - - private protectUpdateForField(ctx: TransformerContext, parent: ObjectTypeDefinitionNode, field: FieldDefinitionNode, - rules: AuthRule[], modelConfiguration: ModelDirectiveConfiguration) { - const resolverResourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(parent.name.value); - const subscriptionOperation: ModelDirectiveOperationType = "onUpdate"; - this.protectUpdateMutation(ctx, resolverResourceId, rules, parent, modelConfiguration, field, subscriptionOperation) + } + + /** + * Takes a flat list of rules, each containing their own list of operations (or queries/mutations if an old API). + * This method splits those rules into buckets keyed by operation and implements some logic for backwards compatibility. + * @param rules The list of auth rules + */ + private splitRules(rules: AuthRule[]) { + // Create a reverse index on rules from operation -> rules list. + const queryRules: { [k in ModelQuery]: AuthRule[] } = { + get: [], + list: [], + }; + const operationRules: { [k in ModelOperation]: AuthRule[] } = { + create: [], + update: [], + delete: [], + read: [], + }; + const matchQuery = (op: ModelQuery) => (rule: AuthRule) => { + if (rule.queries) { + const matchesOp = rule.queries.find(o => o === op); + return Boolean(matchesOp); + } else if (rule.queries === null) { + return false; + } + return true; + }; + const matchMutation = (op: ModelMutation) => (rule: AuthRule) => { + if (rule.mutations) { + const matchesOp = rule.mutations.find(o => o === op); + return Boolean(matchesOp); + } else if (rule.mutations === null) { + return false; + } + return true; + }; + const matchOperation = (op: ModelOperation) => (rule: AuthRule) => { + if (rule.operations) { + const matchesOp = rule.operations.find(o => o === op); + return Boolean(matchesOp); + } else if (rule.operations === null) { + return false; + } + return true; + }; + for (const rule of rules) { + // If operations is provided, then it takes precendence. + if (isTruthyOrNull(rule.operations)) { + // If operations is given use it. + if (matchOperation('read')(rule)) { + queryRules.get.push(rule); + queryRules.list.push(rule); + operationRules.read.push(rule); + } + if (matchOperation('create')(rule)) { + operationRules.create.push(rule); + } + if (matchOperation('update')(rule)) { + operationRules.update.push(rule); + } + if (matchOperation('delete')(rule)) { + operationRules.delete.push(rule); + } + } else { + // If operations is not provided, either use the default behavior or deprecated + // behavior from the queries/mutations arguments for backwards compatibility. + + // Handle default or deprecated query use case + if (isUndefined(rule.queries)) { + // If both operations and queries are undefined, default to read operation protection. + // This is the default behavior. E.G. @auth(rules: [{ allow: owner }]) + queryRules.get.push(rule); + queryRules.list.push(rule); + operationRules.read.push(rule); + } else { + // If operations is undefined & queries is defined, use queries. + // This is the old behavior for backwards compatibility. + if (matchQuery('get')(rule)) { + queryRules.get.push(rule); + } + if (matchQuery('list')(rule)) { + queryRules.list.push(rule); + } + } + + // Handle default or deprecated mutation use case + if (isUndefined(rule.mutations)) { + // If both operations and mutations are undefined, default to create, update, delete + // operation protection. This is the default behavior. E.G. @auth(rules: [{ allow: owner }]) + operationRules.create.push(rule); + operationRules.update.push(rule); + operationRules.delete.push(rule); + } else { + // If operations is undefined & mutations is defined, use mutations. + // This is the old behavior for backwards compatibility. + if (matchMutation('create')(rule)) { + operationRules.create.push(rule); + } + if (matchMutation('update')(rule)) { + operationRules.update.push(rule); + } + if (matchMutation('delete')(rule)) { + operationRules.delete.push(rule); + } + } + } } - - private protectDeleteForField(ctx: TransformerContext, parent: ObjectTypeDefinitionNode, field: FieldDefinitionNode, - rules: AuthRule[], modelConfiguration: ModelDirectiveConfiguration) { - const resolverResourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(parent.name.value); - const subscriptionOperation: ModelDirectiveOperationType = "onDelete"; - this.protectDeleteMutation(ctx, resolverResourceId, rules, parent, modelConfiguration, field, subscriptionOperation) + return { + operationRules, + queryRules, + }; + } + + private validateRules(rules: AuthRule[]) { + for (const rule of rules) { + this.validateRuleAuthStrategy(rule); + + const { queries, mutations, operations } = rule; + if (mutations && operations) { + console.warn(`It is not recommended to use 'mutations' and 'operations'. The 'operations' argument will be used.`); + } + if (queries && operations) { + console.warn(`It is not recommended to use 'queries' and 'operations'. The 'operations' argument will be used.`); + } + this.commonRuleValidation(rule); } + } - /** - * Protects a create mutation based on an @auth rule specified on a @model field. - * @param ctx The context. - * @param typeName The parent type name. - * @param fieldName The name of the field with the @auth directive. - * @param rules The set of rules that should be applied to create operations. - */ - private protectCreateForField(ctx: TransformerContext, parent: ObjectTypeDefinitionNode, - field: FieldDefinitionNode, rules: AuthRule[], modelConfiguration: ModelDirectiveConfiguration) { - const typeName = parent.name.value; - const resolverResourceId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName); - const createResolverResource = ctx.getResource(resolverResourceId); - const mutationTypeName = ctx.getMutationTypeName(); - if (rules && rules.length && createResolverResource) { - // Get the directives we need to add to the GraphQL nodes - const directives = this.getDirectivesForRules(rules, false); - let operationName: string = undefined; + private validateFieldRules(rules: AuthRule[]) { + for (const rule of rules) { + this.validateRuleAuthStrategy(rule); - if (directives.length > 0) { - this.addDirectivesToField(ctx, typeName, field.name.value, directives); - - if (modelConfiguration.shouldHave('create')) { - // If the parent type has any rules for this operation AND - // the default provider we've to get directives including the default - // as well. - const includeDefault = this.isTypeHasRulesForOperation(parent, 'create'); - const operationDirectives = this.getDirectivesForRules(rules, includeDefault); - - operationName = modelConfiguration.getName('create'); - - this.addDirectivesToOperation(ctx, mutationTypeName, operationName, operationDirectives); - } - } - - if (operationName) { - this.addFieldToResourceReferences(mutationTypeName, operationName, rules); - } - - // Break the rules out by strategy. - const staticGroupAuthorizationRules = this.getStaticGroupRules(rules) - const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules) - const ownerAuthorizationRules = this.getOwnerRules(rules) - - if (staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0 || - ownerAuthorizationRules.length > 0) { - - // Generate the expressions to validate each strategy. - const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression( - staticGroupAuthorizationRules, - field) - - // In create mutations, the dynamic group and ownership authorization checks - // are done before calling PutItem. - const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForCreateOperationsByField( - dynamicGroupAuthorizationRules, - field.name.value - ) - const fieldIsList = (fieldName: string) => { - const field = parent.fields.find(field => field.name.value === fieldName); - if (field) { - return isListType(field.type); - } - return false; - } - const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForCreateOperationsByField( - ownerAuthorizationRules, - field.name.value, - fieldIsList - ) - - const throwIfUnauthorizedExpression = this.resources.throwIfUnauthorized(field) - - // Populate a list of configured authentication providers based on the rules - const authModesToCheck = new Set(); - const expressions: Array = new Array(); - - if (ownerAuthorizationRules.find((r) => r.provider === 'userPools') || - staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0) { - authModesToCheck.add('userPools'); - } - if (ownerAuthorizationRules.find((r) => r.provider === 'oidc')) { - authModesToCheck.add('oidc'); - } - - // If we've any modes to check, then add the authMode check code block - // to the start of the resolver. - if (authModesToCheck.size > 0) { - expressions.push (this.resources.getAuthModeDeterminationExpression(authModesToCheck)); - } - - // These statements will be wrapped into an authMode check if statement - const authCheckExpressions = [ - staticGroupAuthorizationExpression, - newline(), - dynamicGroupAuthorizationExpression, - newline(), - ownerAuthorizationExpression, - newline(), - throwIfUnauthorizedExpression - ]; - - // Create the authMode if block and add it to the resolver - expressions.push( - this.resources.getAuthModeCheckWrappedExpression( - authModesToCheck, - compoundExpression(authCheckExpressions)) - ); - - const templateParts = [ - print( - iff( - raw(`$ctx.args.input.containsKey("${field.name.value}")`), - compoundExpression(expressions) - ) - ), - createResolverResource.Properties.RequestMappingTemplate - ] - createResolverResource.Properties.RequestMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, createResolverResource) - } - - // if subscriptions is enabled the operation is specified in the mutation response resolver - if (modelConfiguration.shouldHave('onCreate') && - modelConfiguration.getName('level') as ModelSubscriptionLevel === 'on') { - const getTemplateParts = [ - createResolverResource.Properties.ResponseMappingTemplate, - ]; - - if (!this.isOperationExpressionSet(mutationTypeName, - createResolverResource.Properties.ResponseMappingTemplate)) { - getTemplateParts.unshift(this.resources.setOperationExpression(mutationTypeName)); - } - createResolverResource.Properties.ResponseMappingTemplate = getTemplateParts.join('\n\n') - ctx.setResource(resolverResourceId, createResolverResource) - } - } + const { queries, mutations } = rule; + if (queries || mutations) { + throw new InvalidDirectiveError( + `@auth directives used on field definitions may not specify the 'queries' or 'mutations' arguments. \ +All @auth directives used on field definitions are performed when the field is resolved and can be thought of as 'read' operations.` + ); + } + this.commonRuleValidation(rule); } - - /** - * Takes a flat list of rules, each containing their own list of operations (or queries/mutations if an old API). - * This method splits those rules into buckets keyed by operation and implements some logic for backwards compatibility. - * @param rules The list of auth rules - */ - private splitRules(rules: AuthRule[]) { - // Create a reverse index on rules from operation -> rules list. - const queryRules: { [k in ModelQuery]: AuthRule[] } = { - get: [], - list: [], - } - const operationRules: { [k in ModelOperation]: AuthRule[] } = { - create: [], - update: [], - delete: [], - read: [] - } - const matchQuery = (op: ModelQuery) => (rule: AuthRule) => { - if (rule.queries) { - const matchesOp = rule.queries.find(o => o === op) - return Boolean(matchesOp) - } else if (rule.queries === null) { - return false - } - return true - } - const matchMutation = (op: ModelMutation) => (rule: AuthRule) => { - if (rule.mutations) { - const matchesOp = rule.mutations.find(o => o === op) - return Boolean(matchesOp) - } else if (rule.mutations === null) { - return false - } - return true - } - const matchOperation = (op: ModelOperation) => (rule: AuthRule) => { - if (rule.operations) { - const matchesOp = rule.operations.find(o => o === op) - return Boolean(matchesOp) - } else if (rule.operations === null) { - return false - } - return true - } - for (const rule of rules) { - // If operations is provided, then it takes precendence. - if ( - isTruthyOrNull(rule.operations) - ) { - // If operations is given use it. - if (matchOperation('read')(rule)) { - queryRules.get.push(rule) - queryRules.list.push(rule) - operationRules.read.push(rule) - } - if (matchOperation('create')(rule)) { - operationRules.create.push(rule) - } - if (matchOperation('update')(rule)) { - operationRules.update.push(rule) - } - if (matchOperation('delete')(rule)) { - operationRules.delete.push(rule) - } - } else { - // If operations is not provided, either use the default behavior or deprecated - // behavior from the queries/mutations arguments for backwards compatibility. - - // Handle default or deprecated query use case - if ( - isUndefined(rule.queries) - ) { - // If both operations and queries are undefined, default to read operation protection. - // This is the default behavior. E.G. @auth(rules: [{ allow: owner }]) - queryRules.get.push(rule) - queryRules.list.push(rule) - operationRules.read.push(rule) - } else { - // If operations is undefined & queries is defined, use queries. - // This is the old behavior for backwards compatibility. - if (matchQuery('get')(rule)) { - queryRules.get.push(rule) - } - if (matchQuery('list')(rule)) { - queryRules.list.push(rule) - } - } - - // Handle default or deprecated mutation use case - if ( - isUndefined(rule.mutations) - ) { - // If both operations and mutations are undefined, default to create, update, delete - // operation protection. This is the default behavior. E.G. @auth(rules: [{ allow: owner }]) - operationRules.create.push(rule) - operationRules.update.push(rule) - operationRules.delete.push(rule) - } else { - // If operations is undefined & mutations is defined, use mutations. - // This is the old behavior for backwards compatibility. - if (matchMutation('create')(rule)) { - operationRules.create.push(rule) - } - if (matchMutation('update')(rule)) { - operationRules.update.push(rule) - } - if (matchMutation('delete')(rule)) { - operationRules.delete.push(rule) - } - } - } - } - return { - operationRules, - queryRules - } + } + + // commmon rule validation between obj and field + private commonRuleValidation(rule: AuthRule) { + const { identityField, identityClaim, allow, groups, groupsField, groupClaim } = rule; + if (allow === 'groups' && (identityClaim || identityField)) { + throw new InvalidDirectiveError(` + @auth identityField/Claim can only be used for 'allow: owner'`); } - - private validateRules(rules: AuthRule[]) { - for (const rule of rules) { - - this.validateRuleAuthStrategy(rule); - - const { queries, mutations, operations } = rule; - if (mutations && operations) { - console.warn( - `It is not recommended to use 'mutations' and 'operations'. The 'operations' argument will be used.`) - } - if (queries && operations) { - console.warn( - `It is not recommended to use 'queries' and 'operations'. The 'operations' argument will be used.`) - } - this.commonRuleValidation(rule); - } + if (allow === 'owner' && groupClaim) { + throw new InvalidDirectiveError(` + @auth groupClaim can only be used 'allow: groups'`); } - - private validateFieldRules(rules: AuthRule[]) { - for (const rule of rules) { - - this.validateRuleAuthStrategy(rule); - - const { queries, mutations } = rule; - if (queries || mutations) { - throw new InvalidDirectiveError( - `@auth directives used on field definitions may not specify the 'queries' or 'mutations' arguments. \ -All @auth directives used on field definitions are performed when the field is resolved and can be thought of as 'read' operations.`) - } - this.commonRuleValidation(rule); - } + if (groupsField && groups) { + throw new InvalidDirectiveError('This rule has groupsField and groups, please use one or the other'); } - - // commmon rule validation between obj and field - private commonRuleValidation(rule: AuthRule) { - const { identityField, identityClaim, allow, - groups, groupsField, groupClaim - } = rule; - if ( allow === 'groups' && (identityClaim || identityField)) { - throw new InvalidDirectiveError(` - @auth identityField/Claim can only be used for 'allow: owner'`) - } - if (allow === 'owner' && groupClaim) { - throw new InvalidDirectiveError(` - @auth groupClaim can only be used 'allow: groups'`); - } - if ( groupsField && groups) { - throw new InvalidDirectiveError("This rule has groupsField and groups, please use one or the other") - } - if (identityField && identityClaim) { - throw new InvalidDirectiveError("Please use consider IdentifyClaim over IdentityField as it is deprecated.") - } + if (identityField && identityClaim) { + throw new InvalidDirectiveError('Please use consider IdentifyClaim over IdentityField as it is deprecated.'); } - - /** - * Protect get queries. - * If static group: - * If statically authorized then allow the operation. Stop. - * If owner and/or dynamic group: - * If the result item satisfies the owner/group authorization condition - * then allow it. - * @param ctx The transformer context. - * @param resolverResourceId The logical id of the get resolver. - * @param rules The auth rules to apply. - */ - private protectGetQuery(ctx: TransformerContext, resolverResourceId: string, rules: AuthRule[], - parent: ObjectTypeDefinitionNode | null, modelConfiguration: ModelDirectiveConfiguration) { - - const resolver = ctx.getResource(resolverResourceId) - if (!rules || rules.length === 0 || !resolver) { - return - } else { - let operationName: string = undefined; - - if (modelConfiguration.shouldHave('get')) { - operationName = modelConfiguration.getName('get'); - // If the parent type has any rules for this operation AND - // the default provider we've to get directives including the default - // as well. - const includeDefault = parent !== null ? this.isTypeHasRulesForOperation(parent, 'get') : false; - const operationDirectives = this.getDirectivesForRules(rules, includeDefault); - - if (operationDirectives.length > 0) { - this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); - } - } - - if (operationName) { - this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); - } - - const authExpression = this.authorizationExpressionOnSingleObject(rules); - - if (authExpression) { - const templateParts = [ - print(authExpression), - resolver.Properties.ResponseMappingTemplate - ] - resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, resolver) - } - } + } + + /** + * Protect get queries. + * If static group: + * If statically authorized then allow the operation. Stop. + * If owner and/or dynamic group: + * If the result item satisfies the owner/group authorization condition + * then allow it. + * @param ctx The transformer context. + * @param resolverResourceId The logical id of the get resolver. + * @param rules The auth rules to apply. + */ + private protectGetQuery( + ctx: TransformerContext, + resolverResourceId: string, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode | null, + modelConfiguration: ModelDirectiveConfiguration + ) { + const resolver = ctx.getResource(resolverResourceId); + if (!rules || rules.length === 0 || !resolver) { + return; + } else { + let operationName: string = undefined; + + if (modelConfiguration.shouldHave('get')) { + operationName = modelConfiguration.getName('get'); + // If the parent type has any rules for this operation AND + // the default provider we've to get directives including the default + // as well. + const includeDefault = parent !== null ? this.isTypeHasRulesForOperation(parent, 'get') : false; + const operationDirectives = this.getDirectivesForRules(rules, includeDefault); + + if (operationDirectives.length > 0) { + this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); + } + } + + if (operationName) { + this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); + } + + const authExpression = this.authorizationExpressionOnSingleObject(rules); + + if (authExpression) { + const templateParts = [print(authExpression), resolver.Properties.ResponseMappingTemplate]; + resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, resolver); + } } - - private authorizationExpressionOnSingleObject(rules: AuthRule[], objectPath: string = 'ctx.result') { - // Break the rules out by strategy. - const staticGroupAuthorizationRules = this.getStaticGroupRules(rules) - const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules) - const ownerAuthorizationRules = this.getOwnerRules(rules) - - if (staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0 || - ownerAuthorizationRules.length > 0) { - - // Generate the expressions to validate each strategy. - const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules) - const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForReadOperations( - dynamicGroupAuthorizationRules, - objectPath - ) - const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForReadOperations( - ownerAuthorizationRules, - objectPath - ) - const throwIfUnauthorizedExpression = this.resources.throwIfUnauthorized() - - // If we've any modes to check, then add the authMode check code block - // to the start of the resolver. - const authModesToCheck = new Set(); - const expressions: Array = new Array(); - - if (ownerAuthorizationRules.find((r) => r.provider === 'userPools') || - staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0) { - authModesToCheck.add('userPools'); - } - if (ownerAuthorizationRules.find((r) => r.provider === 'oidc')) { - authModesToCheck.add('oidc'); - } - - if (authModesToCheck.size > 0) { - expressions.push (this.resources.getAuthModeDeterminationExpression(authModesToCheck)); - } - - // Update the existing resolver with the authorization checks. - // These statements will be wrapped into an authMode check if statement - const templateExpressions = [ - staticGroupAuthorizationExpression, - newline(), - dynamicGroupAuthorizationExpression, - newline(), - ownerAuthorizationExpression, - newline(), - throwIfUnauthorizedExpression - ]; - - // These statements will be wrapped into an authMode check if statement - expressions.push( - this.resources.getAuthModeCheckWrappedExpression( - authModesToCheck, - compoundExpression(templateExpressions)) - ); - - return compoundExpression(expressions); - } + } + + private authorizationExpressionOnSingleObject(rules: AuthRule[], objectPath: string = 'ctx.result') { + // Break the rules out by strategy. + const staticGroupAuthorizationRules = this.getStaticGroupRules(rules); + const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules); + const ownerAuthorizationRules = this.getOwnerRules(rules); + + if (staticGroupAuthorizationRules.length > 0 || dynamicGroupAuthorizationRules.length > 0 || ownerAuthorizationRules.length > 0) { + // Generate the expressions to validate each strategy. + const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules); + const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForReadOperations( + dynamicGroupAuthorizationRules, + objectPath + ); + const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForReadOperations( + ownerAuthorizationRules, + objectPath + ); + const throwIfUnauthorizedExpression = this.resources.throwIfUnauthorized(); + + // If we've any modes to check, then add the authMode check code block + // to the start of the resolver. + const authModesToCheck = new Set(); + const expressions: Array = new Array(); + + if ( + ownerAuthorizationRules.find(r => r.provider === 'userPools') || + staticGroupAuthorizationRules.length > 0 || + dynamicGroupAuthorizationRules.length > 0 + ) { + authModesToCheck.add('userPools'); + } + if (ownerAuthorizationRules.find(r => r.provider === 'oidc')) { + authModesToCheck.add('oidc'); + } + + if (authModesToCheck.size > 0) { + expressions.push(this.resources.getAuthModeDeterminationExpression(authModesToCheck)); + } + + // Update the existing resolver with the authorization checks. + // These statements will be wrapped into an authMode check if statement + const templateExpressions = [ + staticGroupAuthorizationExpression, + newline(), + dynamicGroupAuthorizationExpression, + newline(), + ownerAuthorizationExpression, + newline(), + throwIfUnauthorizedExpression, + ]; + + // These statements will be wrapped into an authMode check if statement + expressions.push(this.resources.getAuthModeCheckWrappedExpression(authModesToCheck, compoundExpression(templateExpressions))); + + return compoundExpression(expressions); } - - /** - * Protect list queries. - * If static group: - * If the user is statically authorized then return items and stop. - * If dynamic group and/or owner: - * Loop through all items and find items that satisfy any of the group or - * owner conditions. - * @param ctx The transformer context. - * @param resolverResourceId The logical id of the resolver to be updated in the CF template. - * @param rules The set of rules that apply to the operation. - */ - private protectListQuery(ctx: TransformerContext, resolverResourceId: string, rules: AuthRule[], - parent: ObjectTypeDefinitionNode | null, modelConfiguration: ModelDirectiveConfiguration, - explicitOperationName: string = undefined) { - - const resolver = ctx.getResource(resolverResourceId) - if (!rules || rules.length === 0 || !resolver) { - return - } else { - if (modelConfiguration.shouldHave('list')) { - const operationName = explicitOperationName ? explicitOperationName : modelConfiguration.getName('list'); - // If the parent type has any rules for this operation AND - // the default provider we've to get directives including the default - // as well. - const includeDefault = parent !== null ? this.isTypeHasRulesForOperation(parent, 'list') : false; - const operationDirectives = this.getDirectivesForRules(rules, includeDefault); - - if (operationDirectives.length > 0) { - this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); - } - - this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); - } - - const authExpression = this.authorizationExpressionForListResult(rules); - - if (authExpression) { - const templateParts = [ - print(authExpression), - resolver.Properties.ResponseMappingTemplate - ] - resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, resolver) - } - } + } + + /** + * Protect list queries. + * If static group: + * If the user is statically authorized then return items and stop. + * If dynamic group and/or owner: + * Loop through all items and find items that satisfy any of the group or + * owner conditions. + * @param ctx The transformer context. + * @param resolverResourceId The logical id of the resolver to be updated in the CF template. + * @param rules The set of rules that apply to the operation. + */ + private protectListQuery( + ctx: TransformerContext, + resolverResourceId: string, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode | null, + modelConfiguration: ModelDirectiveConfiguration, + explicitOperationName: string = undefined + ) { + const resolver = ctx.getResource(resolverResourceId); + if (!rules || rules.length === 0 || !resolver) { + return; + } else { + if (modelConfiguration.shouldHave('list')) { + const operationName = explicitOperationName ? explicitOperationName : modelConfiguration.getName('list'); + // If the parent type has any rules for this operation AND + // the default provider we've to get directives including the default + // as well. + const includeDefault = parent !== null ? this.isTypeHasRulesForOperation(parent, 'list') : false; + const operationDirectives = this.getDirectivesForRules(rules, includeDefault); + + if (operationDirectives.length > 0) { + this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); + } + + this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); + } + + const authExpression = this.authorizationExpressionForListResult(rules); + + if (authExpression) { + const templateParts = [print(authExpression), resolver.Properties.ResponseMappingTemplate]; + resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, resolver); + } } - - /** - * Returns a VTL expression that will authorize a list of results based on a set of auth rules. - * @param rules The auth rules. - * - * If an itemList is specifed in @param itemList it will use this ref to filter out items in this list that are not authorized - */ - private authorizationExpressionForListResult(rules: AuthRule[], itemList: string = 'ctx.result.items') { - // Break the rules out by strategy. - const staticGroupAuthorizationRules = this.getStaticGroupRules(rules) - const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules) - const ownerAuthorizationRules = this.getOwnerRules(rules) - - if (staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0 || - ownerAuthorizationRules.length > 0) { - - // Generate the expressions to validate each strategy. - const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules) - - // In list queries, the dynamic group and ownership authorization checks - // occur on a per item basis. The helpers take the variable names - // as parameters to allow for this use case. - const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForReadOperations( - dynamicGroupAuthorizationRules, - 'item', - ResourceConstants.SNIPPETS.IsLocalDynamicGroupAuthorizedVariable, - raw(`false`) - ) - const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForReadOperations( - ownerAuthorizationRules, - 'item', - ResourceConstants.SNIPPETS.IsLocalOwnerAuthorizedVariable, - raw(`false`) - ) - const appendIfLocallyAuthorized = this.resources.appendItemIfLocallyAuthorized() - - const ifNotStaticallyAuthedFilterObjects = iff( - raw(`! $${ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable}`), - compoundExpression([ - set(ref('items'), list([])), - forEach( - ref('item'), - ref(itemList), - [ - dynamicGroupAuthorizationExpression, - newline(), - ownerAuthorizationExpression, - newline(), - appendIfLocallyAuthorized - ] - ), - set(ref(itemList), ref('items')) - ]) - ) - - // If we've any modes to check, then add the authMode check code block - // to the start of the resolver. - const authModesToCheck = new Set(); - const expressions: Array = new Array(); - - if (ownerAuthorizationRules.find((r) => r.provider === 'userPools') || - staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0) { - authModesToCheck.add('userPools'); - } - if (ownerAuthorizationRules.find((r) => r.provider === 'oidc')) { - authModesToCheck.add('oidc'); - } - - if (authModesToCheck.size > 0) { - expressions.push (this.resources.getAuthModeDeterminationExpression(authModesToCheck)); - } - - // These statements will be wrapped into an authMode check if statement - const templateExpressions = [ - staticGroupAuthorizationExpression, - newline(), - comment('[Start] If not static group authorized, filter items'), - ifNotStaticallyAuthedFilterObjects, - comment('[End] If not static group authorized, filter items') - ]; - - // Create the authMode if block and add it to the resolver - expressions.push( - this.resources.getAuthModeCheckWrappedExpression( - authModesToCheck, - compoundExpression(templateExpressions)) - ); - - return compoundExpression(expressions); - } + } + + /** + * Returns a VTL expression that will authorize a list of results based on a set of auth rules. + * @param rules The auth rules. + * + * If an itemList is specifed in @param itemList it will use this ref to filter out items in this list that are not authorized + */ + private authorizationExpressionForListResult(rules: AuthRule[], itemList: string = 'ctx.result.items') { + // Break the rules out by strategy. + const staticGroupAuthorizationRules = this.getStaticGroupRules(rules); + const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules); + const ownerAuthorizationRules = this.getOwnerRules(rules); + + if (staticGroupAuthorizationRules.length > 0 || dynamicGroupAuthorizationRules.length > 0 || ownerAuthorizationRules.length > 0) { + // Generate the expressions to validate each strategy. + const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules); + + // In list queries, the dynamic group and ownership authorization checks + // occur on a per item basis. The helpers take the variable names + // as parameters to allow for this use case. + const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForReadOperations( + dynamicGroupAuthorizationRules, + 'item', + ResourceConstants.SNIPPETS.IsLocalDynamicGroupAuthorizedVariable, + raw(`false`) + ); + const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForReadOperations( + ownerAuthorizationRules, + 'item', + ResourceConstants.SNIPPETS.IsLocalOwnerAuthorizedVariable, + raw(`false`) + ); + const appendIfLocallyAuthorized = this.resources.appendItemIfLocallyAuthorized(); + + const ifNotStaticallyAuthedFilterObjects = iff( + raw(`! $${ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable}`), + compoundExpression([ + set(ref('items'), list([])), + forEach(ref('item'), ref(itemList), [ + dynamicGroupAuthorizationExpression, + newline(), + ownerAuthorizationExpression, + newline(), + appendIfLocallyAuthorized, + ]), + set(ref(itemList), ref('items')), + ]) + ); + + // If we've any modes to check, then add the authMode check code block + // to the start of the resolver. + const authModesToCheck = new Set(); + const expressions: Array = new Array(); + + if ( + ownerAuthorizationRules.find(r => r.provider === 'userPools') || + staticGroupAuthorizationRules.length > 0 || + dynamicGroupAuthorizationRules.length > 0 + ) { + authModesToCheck.add('userPools'); + } + if (ownerAuthorizationRules.find(r => r.provider === 'oidc')) { + authModesToCheck.add('oidc'); + } + + if (authModesToCheck.size > 0) { + expressions.push(this.resources.getAuthModeDeterminationExpression(authModesToCheck)); + } + + // These statements will be wrapped into an authMode check if statement + const templateExpressions = [ + staticGroupAuthorizationExpression, + newline(), + comment('[Start] If not static group authorized, filter items'), + ifNotStaticallyAuthedFilterObjects, + comment('[End] If not static group authorized, filter items'), + ]; + + // Create the authMode if block and add it to the resolver + expressions.push(this.resources.getAuthModeCheckWrappedExpression(authModesToCheck, compoundExpression(templateExpressions))); + + return compoundExpression(expressions); } + } + + /** + * Inject auth rules for create mutations. + * If owner auth: + * If the owner field exists in the input, validate that it against the identity. + * If the owner field dne in the input, insert the identity. + * If group: + * If the user is static group authorized allow operation no matter what. + * If dynamic group and the input defines a group(s) validate it against the identity. + * @param ctx + * @param resolverResourceId + * @param rules + */ + private protectCreateMutation( + ctx: TransformerContext, + resolverResourceId: string, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + modelConfiguration: ModelDirectiveConfiguration + ) { + const resolver = ctx.getResource(resolverResourceId); + if (!rules || rules.length === 0 || !resolver) { + return; + } else { + const mutationTypeName = ctx.getMutationTypeName(); + + if (modelConfiguration.shouldHave('create')) { + const operationName = modelConfiguration.getName('create'); + // If the parent type has any rules for this operation AND + // the default provider we've to get directives including the default + // as well. + const includeDefault = this.isTypeHasRulesForOperation(parent, 'create'); + const operationDirectives = this.getDirectivesForRules(rules, includeDefault); + + if (operationDirectives.length > 0) { + this.addDirectivesToOperation(ctx, mutationTypeName, operationName, operationDirectives); + } + + this.addFieldToResourceReferences(mutationTypeName, operationName, rules); + } + + // Break the rules out by strategy. + const staticGroupAuthorizationRules = this.getStaticGroupRules(rules); + const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules); + const ownerAuthorizationRules = this.getOwnerRules(rules); + + if (staticGroupAuthorizationRules.length > 0 || dynamicGroupAuthorizationRules.length > 0 || ownerAuthorizationRules.length > 0) { + // Generate the expressions to validate each strategy. + const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules); + + // In create mutations, the dynamic group and ownership authorization checks + // are done before calling PutItem. + const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForCreateOperations( + dynamicGroupAuthorizationRules + ); + const fieldIsList = (fieldName: string) => { + const field = parent.fields.find(field => field.name.value === fieldName); + if (field) { + return isListType(field.type); + } + return false; + }; + const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForCreateOperations( + ownerAuthorizationRules, + fieldIsList + ); - /** - * Inject auth rules for create mutations. - * If owner auth: - * If the owner field exists in the input, validate that it against the identity. - * If the owner field dne in the input, insert the identity. - * If group: - * If the user is static group authorized allow operation no matter what. - * If dynamic group and the input defines a group(s) validate it against the identity. - * @param ctx - * @param resolverResourceId - * @param rules - */ - private protectCreateMutation( - ctx: TransformerContext, - resolverResourceId: string, - rules: AuthRule[], - parent: ObjectTypeDefinitionNode, - modelConfiguration: ModelDirectiveConfiguration - ) { - const resolver = ctx.getResource(resolverResourceId) - if (!rules || rules.length === 0 || !resolver) { - return - } else { - const mutationTypeName = ctx.getMutationTypeName(); - - if (modelConfiguration.shouldHave('create')) { - const operationName = modelConfiguration.getName('create'); - // If the parent type has any rules for this operation AND - // the default provider we've to get directives including the default - // as well. - const includeDefault = this.isTypeHasRulesForOperation(parent, 'create'); - const operationDirectives = this.getDirectivesForRules(rules, includeDefault); - - if (operationDirectives.length > 0) { - this.addDirectivesToOperation(ctx, mutationTypeName, operationName, operationDirectives); - } + const throwIfUnauthorizedExpression = this.resources.throwIfUnauthorized(); - this.addFieldToResourceReferences(mutationTypeName, operationName, rules); - } + // If we've any modes to check, then add the authMode check code block + // to the start of the resolver. + const authModesToCheck = new Set(); + const expressions: Array = new Array(); - // Break the rules out by strategy. - const staticGroupAuthorizationRules = this.getStaticGroupRules(rules) - const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules) - const ownerAuthorizationRules = this.getOwnerRules(rules) - - if (staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0 || - ownerAuthorizationRules.length > 0) { - - // Generate the expressions to validate each strategy. - const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules) - - // In create mutations, the dynamic group and ownership authorization checks - // are done before calling PutItem. - const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForCreateOperations( - dynamicGroupAuthorizationRules - ) - const fieldIsList = (fieldName: string) => { - const field = parent.fields.find(field => field.name.value === fieldName); - if (field) { - return isListType(field.type); - } - return false; - } - const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForCreateOperations( - ownerAuthorizationRules, - fieldIsList - ) - - const throwIfUnauthorizedExpression = this.resources.throwIfUnauthorized(); - - // If we've any modes to check, then add the authMode check code block - // to the start of the resolver. - const authModesToCheck = new Set(); - const expressions: Array = new Array(); - - if (ownerAuthorizationRules.find((r) => r.provider === 'userPools') || - staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0) { - authModesToCheck.add('userPools'); - } - if (ownerAuthorizationRules.find((r) => r.provider === 'oidc')) { - authModesToCheck.add('oidc'); - } - - if (authModesToCheck.size > 0) { - expressions.push (this.resources.getAuthModeDeterminationExpression(authModesToCheck)); - } - - // These statements will be wrapped into an authMode check if statement - const authCheckExpressions = [ - staticGroupAuthorizationExpression, - newline(), - dynamicGroupAuthorizationExpression, - newline(), - ownerAuthorizationExpression, - newline(), - throwIfUnauthorizedExpression - ]; - - // Create the authMode if block and add it to the resolver - expressions.push( - this.resources.getAuthModeCheckWrappedExpression( - authModesToCheck, - compoundExpression(authCheckExpressions)) - ); - - const templateParts = [ - print(compoundExpression(expressions)), - resolver.Properties.RequestMappingTemplate - ] - resolver.Properties.RequestMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, resolver) - } + if ( + ownerAuthorizationRules.find(r => r.provider === 'userPools') || + staticGroupAuthorizationRules.length > 0 || + dynamicGroupAuthorizationRules.length > 0 + ) { + authModesToCheck.add('userPools'); + } + if (ownerAuthorizationRules.find(r => r.provider === 'oidc')) { + authModesToCheck.add('oidc'); } - } - - /** - * Protect update and delete mutations. - * If Owner: - * Update the conditional expression such that the update only works if - * the user is the owner. - * If dynamic group: - * Update the conditional expression such that it succeeds if the user is - * dynamic group authorized. If the operation is also owner authorized this - * should be joined with an OR expression. - * If static group: - * If the user is statically authorized then allow no matter what. This can - * be done by removing the conditional expression as long as static group - * auth is always checked last. - * @param ctx The transformer context. - * @param resolverResourceId The logical id of the resolver in the template. - * @param rules The list of rules to apply. - */ - private protectUpdateOrDeleteMutation( - ctx: TransformerContext, - resolverResourceId: string, - rules: AuthRule[], - parent: ObjectTypeDefinitionNode, - modelConfiguration: ModelDirectiveConfiguration, - isUpdate: boolean, - field?: FieldDefinitionNode, - ifCondition?: Expression, - subscriptionOperation?: ModelDirectiveOperationType, - ) { - const resolver = ctx.getResource(resolverResourceId) - if (!rules || rules.length === 0 || !resolver) { - return - } else { - const mutationTypeName = ctx.getMutationTypeName(); - - if (modelConfiguration.shouldHave(isUpdate ? 'update' : 'delete')) { - const operationName = modelConfiguration.getName(isUpdate ? 'update' : 'delete'); - // If the parent type has any rules for this operation AND - // the default provider we've to get directives including the default - // as well. - const includeDefault = Boolean(!field && this.isTypeHasRulesForOperation(parent, isUpdate ? 'update' : 'delete')); - const operationDirectives = this.getDirectivesForRules(rules, includeDefault); - if (operationDirectives.length > 0) { - this.addDirectivesToOperation(ctx, mutationTypeName, operationName, operationDirectives); - } + if (authModesToCheck.size > 0) { + expressions.push(this.resources.getAuthModeDeterminationExpression(authModesToCheck)); + } - this.addFieldToResourceReferences(mutationTypeName, operationName, rules); - } + // These statements will be wrapped into an authMode check if statement + const authCheckExpressions = [ + staticGroupAuthorizationExpression, + newline(), + dynamicGroupAuthorizationExpression, + newline(), + ownerAuthorizationExpression, + newline(), + throwIfUnauthorizedExpression, + ]; - // Break the rules out by strategy. - const staticGroupAuthorizationRules = this.getStaticGroupRules(rules) - const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules) - const ownerAuthorizationRules = this.getOwnerRules(rules) - - if (staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0 || - ownerAuthorizationRules.length > 0) { - - // Generate the expressions to validate each strategy. - const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression( - staticGroupAuthorizationRules, - field) - - // In create mutations, the dynamic group and ownership authorization checks - // are done before calling PutItem. - const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForUpdateOrDeleteOperations( - dynamicGroupAuthorizationRules, - field ? field.name.value : undefined - ) - - const fieldIsList = (fieldName: string) => { - const field = parent.fields.find(field => field.name.value === fieldName); - if (field) { - return isListType(field.type); - } - return false; - } - const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForUpdateOrDeleteOperations( - ownerAuthorizationRules, - fieldIsList, - field ? field.name.value : undefined - ) - - const collectAuthCondition = this.resources.collectAuthCondition() - const staticGroupAuthorizedVariable = this.resources.getStaticAuthorizationVariable(field); - const ifNotStaticallyAuthedCreateAuthCondition = iff( - raw(`! $${staticGroupAuthorizedVariable}`), - compoundExpression([ - dynamicGroupAuthorizationExpression, - newline(), - ownerAuthorizationExpression, - newline(), - collectAuthCondition - ]) - ) - - const throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty = this.resources.throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty( - field) - - // If we've any modes to check, then add the authMode check code block - // to the start of the resolver. - const authModesToCheck = new Set(); - const expressions: Array = new Array(); - - if (ownerAuthorizationRules.find((r) => r.provider === 'userPools') || - staticGroupAuthorizationRules.length > 0 || - dynamicGroupAuthorizationRules.length > 0) { - authModesToCheck.add('userPools'); - } - if (ownerAuthorizationRules.find((r) => r.provider === 'oidc')) { - authModesToCheck.add('oidc'); - } - - if (authModesToCheck.size > 0) { - expressions.push (this.resources.getAuthModeDeterminationExpression(authModesToCheck)); - } - - // These statements will be wrapped into an authMode check if statement - const authorizationLogic = compoundExpression([ - staticGroupAuthorizationExpression, - newline(), - ifNotStaticallyAuthedCreateAuthCondition, - newline(), - throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty - ]); - - // Create the authMode if block and add it to the resolver - expressions.push( - this.resources.getAuthModeCheckWrappedExpression( - authModesToCheck, - authorizationLogic) - ); - - const templateParts = [ - print(field && ifCondition ? - iff(ifCondition, compoundExpression(expressions)) : - compoundExpression(expressions)), - resolver.Properties.RequestMappingTemplate - ] - resolver.Properties.RequestMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, resolver) - } + // Create the authMode if block and add it to the resolver + expressions.push(this.resources.getAuthModeCheckWrappedExpression(authModesToCheck, compoundExpression(authCheckExpressions))); - // if protect is for field and there is a subscription for update / delete then protect the field in that operation - if (field && subscriptionOperation && - modelConfiguration.shouldHave(subscriptionOperation) && - modelConfiguration.getName('level') as ModelSubscriptionLevel === 'on') { - let mutationResolver = resolver; - let mutationResolverResourceID = resolverResourceId; - // if we are protecting delete then we need to get the delete resolver - if (subscriptionOperation === 'onDelete') { - mutationResolverResourceID = ResolverResourceIDs.DynamoDBDeleteResolverResourceID(parent.name.value); - mutationResolver = ctx.getResource(mutationResolverResourceID); - } - const getTemplateParts = [ - mutationResolver.Properties.ResponseMappingTemplate, - ]; - if (!this.isOperationExpressionSet(mutationTypeName, - mutationResolver.Properties.ResponseMappingTemplate)) { - getTemplateParts.unshift(this.resources.setOperationExpression(mutationTypeName)) - } - mutationResolver.Properties.ResponseMappingTemplate = getTemplateParts.join('\n\n') - ctx.setResource(mutationResolverResourceID, mutationResolver) - } - } + const templateParts = [print(compoundExpression(expressions)), resolver.Properties.RequestMappingTemplate]; + resolver.Properties.RequestMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, resolver); + } } + } + + /** + * Protect update and delete mutations. + * If Owner: + * Update the conditional expression such that the update only works if + * the user is the owner. + * If dynamic group: + * Update the conditional expression such that it succeeds if the user is + * dynamic group authorized. If the operation is also owner authorized this + * should be joined with an OR expression. + * If static group: + * If the user is statically authorized then allow no matter what. This can + * be done by removing the conditional expression as long as static group + * auth is always checked last. + * @param ctx The transformer context. + * @param resolverResourceId The logical id of the resolver in the template. + * @param rules The list of rules to apply. + */ + private protectUpdateOrDeleteMutation( + ctx: TransformerContext, + resolverResourceId: string, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + modelConfiguration: ModelDirectiveConfiguration, + isUpdate: boolean, + field?: FieldDefinitionNode, + ifCondition?: Expression, + subscriptionOperation?: ModelDirectiveOperationType + ) { + const resolver = ctx.getResource(resolverResourceId); + if (!rules || rules.length === 0 || !resolver) { + return; + } else { + const mutationTypeName = ctx.getMutationTypeName(); + + if (modelConfiguration.shouldHave(isUpdate ? 'update' : 'delete')) { + const operationName = modelConfiguration.getName(isUpdate ? 'update' : 'delete'); + // If the parent type has any rules for this operation AND + // the default provider we've to get directives including the default + // as well. + const includeDefault = Boolean(!field && this.isTypeHasRulesForOperation(parent, isUpdate ? 'update' : 'delete')); + const operationDirectives = this.getDirectivesForRules(rules, includeDefault); + + if (operationDirectives.length > 0) { + this.addDirectivesToOperation(ctx, mutationTypeName, operationName, operationDirectives); + } + + this.addFieldToResourceReferences(mutationTypeName, operationName, rules); + } + + // Break the rules out by strategy. + const staticGroupAuthorizationRules = this.getStaticGroupRules(rules); + const dynamicGroupAuthorizationRules = this.getDynamicGroupRules(rules); + const ownerAuthorizationRules = this.getOwnerRules(rules); + + if (staticGroupAuthorizationRules.length > 0 || dynamicGroupAuthorizationRules.length > 0 || ownerAuthorizationRules.length > 0) { + // Generate the expressions to validate each strategy. + const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules, field); + + // In create mutations, the dynamic group and ownership authorization checks + // are done before calling PutItem. + const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForUpdateOrDeleteOperations( + dynamicGroupAuthorizationRules, + field ? field.name.value : undefined + ); - /** - * If we are protecting the mutation for a field level @auth directive, include - * the necessary if condition. - * @param ctx The transformer context - * @param resolverResourceId The resolver resource id - * @param rules The delete rules - * @param parent The parent object - * @param field The optional field - */ - private protectUpdateMutation( - ctx: TransformerContext, resolverResourceId: string, - rules: AuthRule[], parent: ObjectTypeDefinitionNode, - modelConfiguration: ModelDirectiveConfiguration, - field?: FieldDefinitionNode, - subscriptionOperation?: ModelDirectiveOperationType, - ) { - return this.protectUpdateOrDeleteMutation( - ctx, resolverResourceId, rules, parent, modelConfiguration, true, field, - field ? raw(`$ctx.args.input.containsKey("${field.name.value}")`) : undefined, subscriptionOperation + const fieldIsList = (fieldName: string) => { + const field = parent.fields.find(field => field.name.value === fieldName); + if (field) { + return isListType(field.type); + } + return false; + }; + const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForUpdateOrDeleteOperations( + ownerAuthorizationRules, + fieldIsList, + field ? field.name.value : undefined ); - } - /** - * If we are protecting the mutation for a field level @auth directive, include - * the necessary if condition. - * @param ctx The transformer context - * @param resolverResourceId The resolver resource id - * @param rules The delete rules - * @param parent The parent object - * @param field The optional field - */ - private protectDeleteMutation( - ctx: TransformerContext, resolverResourceId: string, - rules: AuthRule[], parent: ObjectTypeDefinitionNode, - modelConfiguration: ModelDirectiveConfiguration, - field?: FieldDefinitionNode, - subscriptionOperation?: ModelDirectiveOperationType - ) { - return this.protectUpdateOrDeleteMutation( - ctx, resolverResourceId, rules, parent, modelConfiguration, false, field, - field ? raw(`$ctx.args.input.containsKey("${field.name.value}") && $util.isNull($ctx.args.input.get("${field.name.value}"))`) : undefined, - subscriptionOperation - ) - } + const collectAuthCondition = this.resources.collectAuthCondition(); + const staticGroupAuthorizedVariable = this.resources.getStaticAuthorizationVariable(field); + const ifNotStaticallyAuthedCreateAuthCondition = iff( + raw(`! $${staticGroupAuthorizedVariable}`), + compoundExpression([ + dynamicGroupAuthorizationExpression, + newline(), + ownerAuthorizationExpression, + newline(), + collectAuthCondition, + ]) + ); - /** - * When read operations are protected via @auth, all @connection resolvers will be protected. - * Find the directives & update their resolvers with auth logic - */ - private protectConnections(ctx: TransformerContext, def: ObjectTypeDefinitionNode, rules: AuthRule[], - modelConfiguration: ModelDirectiveConfiguration) { - const thisModelName = def.name.value; - for (const inputDef of ctx.inputDocument.definitions) { - if (inputDef.kind === Kind.OBJECT_TYPE_DEFINITION) { - for (const field of inputDef.fields) { - const returnTypeName = getBaseType(field.type) - if (fieldHasDirective(field, 'connection') && returnTypeName === thisModelName) { - const resolverResourceId = ResolverResourceIDs.ResolverResourceID(inputDef.name.value, field.name.value) - - // Add the auth directives to the connection to make sure the - // member is accessible. - const directives = this.getDirectivesForRules(rules, false); - if (directives.length > 0) { - this.addDirectivesToField(ctx, inputDef.name.value, field.name.value, directives); - } - - if (isListType(field.type)) { - this.protectListQuery(ctx, resolverResourceId, rules, null, modelConfiguration) - } else { - this.protectGetQuery(ctx, resolverResourceId, rules, null, modelConfiguration) - } - } - } - } - } - } + const throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty = this.resources.throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty( + field + ); - /** - * When read operations are protected via @auth, all secondary @key query resolvers will be protected. - * Find the directives & update their resolvers with auth logic - */ - private protectQueries(ctx: TransformerContext, def: ObjectTypeDefinitionNode, rules: AuthRule[], - modelConfiguration: ModelDirectiveConfiguration) { - const secondaryKeyDirectivesWithQueries = (def.directives || []).filter(d => { - const isKey = d.name.value === 'key'; - const args = getDirectiveArguments(d); - // @key with a name is a secondary key. - const isSecondaryKey = Boolean(args.name); - const hasQueryField = Boolean(args.queryField); - return isKey && isSecondaryKey && hasQueryField; - }); - for (const keyWithQuery of secondaryKeyDirectivesWithQueries) { - const args = getDirectiveArguments(keyWithQuery); - const resolverResourceId = ResolverResourceIDs.ResolverResourceID(ctx.getQueryTypeName(), args.queryField); - this.protectListQuery(ctx, resolverResourceId, rules, null, modelConfiguration, args.queryField) - } - } + // If we've any modes to check, then add the authMode check code block + // to the start of the resolver. + const authModesToCheck = new Set(); + const expressions: Array = new Array(); - private protectSearchQuery(ctx: TransformerContext, def: ObjectTypeDefinitionNode, - resolverResourceId: string, rules: AuthRule[]) { - const resolver = ctx.getResource(resolverResourceId); - if (!rules || rules.length === 0 || !resolver) { - return - } else { - const operationName = resolver.Properties.FieldName; - const includeDefault = def !== null ? this.isTypeHasRulesForOperation(def, 'list') : false; - const operationDirectives = this.getDirectivesForRules(rules, includeDefault); - if (operationDirectives.length > 0) { - this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); - } - this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); - // create auth expression - const authExpression = this.authorizationExpressionForListResult(rules, 'es_items'); - if (authExpression) { - const templateParts = [ - print(this.resources.makeESItemsExpression()), - print(authExpression), - print(this.resources.makeESToGQLExpression()) - ] - resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, resolver) - } - } + if ( + ownerAuthorizationRules.find(r => r.provider === 'userPools') || + staticGroupAuthorizationRules.length > 0 || + dynamicGroupAuthorizationRules.length > 0 + ) { + authModesToCheck.add('userPools'); } - - // OnCreate Subscription - private protectOnCreateSubscription(ctx: TransformerContext, rules: AuthRule[], - parent: ObjectTypeDefinitionNode, modelConfiguration: ModelDirectiveConfiguration) { - const names = modelConfiguration.getNames('onCreate'); - const level = modelConfiguration.getName('level') as ModelSubscriptionLevel; - if (names) { - names.forEach( (name) => { - this.addSubscriptionResolvers(ctx, rules, parent, level, name, 'create') - }) + if (ownerAuthorizationRules.find(r => r.provider === 'oidc')) { + authModesToCheck.add('oidc'); } - } - // OnUpdate Subscription - private protectOnUpdateSubscription(ctx: TransformerContext, rules: AuthRule[], - parent: ObjectTypeDefinitionNode, modelConfiguration: ModelDirectiveConfiguration) { - const names = modelConfiguration.getNames('onUpdate'); - const level = modelConfiguration.getName('level') as ModelSubscriptionLevel; - if (names) { - names.forEach( (name) => { - this.addSubscriptionResolvers(ctx, rules, parent, level, name, 'update') - }) + if (authModesToCheck.size > 0) { + expressions.push(this.resources.getAuthModeDeterminationExpression(authModesToCheck)); } - } - // OnDelete Subscription - private protectOnDeleteSubscription(ctx: TransformerContext, rules: AuthRule[], - parent: ObjectTypeDefinitionNode, modelConfiguration: ModelDirectiveConfiguration) { - const names = modelConfiguration.getNames('onDelete'); - const level = modelConfiguration.getName('level') as ModelSubscriptionLevel; - if (names) { - names.forEach( (name) => { - this.addSubscriptionResolvers(ctx, rules, parent, level, name, 'delete') - }) - } - } + // These statements will be wrapped into an authMode check if statement + const authorizationLogic = compoundExpression([ + staticGroupAuthorizationExpression, + newline(), + ifNotStaticallyAuthedCreateAuthCondition, + newline(), + throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty, + ]); - // adds subscription resolvers (request / response) based on the operation provided - private addSubscriptionResolvers(ctx: TransformerContext, rules: AuthRule[], - parent: ObjectTypeDefinitionNode, level: ModelSubscriptionLevel, fieldName: string, - mutationOperation: ModelDirectiveOperationType) { - const resolverResourceId = ResolverResourceIDs.ResolverResourceID("Subscription", fieldName); - const resolver = this.resources.generateSubscriptionResolver(fieldName); - // If the data source does not exist it is created and added as a resource for public && on levels - const noneDS = ctx.getResource(ResourceConstants.RESOURCES.NoneDataSource) - - // add the rules in the subscription resolver - if (!rules || rules.length === 0) { - return; - } else if (level === 'public') { - // set the resource with no auth logic - ctx.setResource(resolverResourceId, resolver); - } else { - // Get the directives we need to add to the GraphQL nodes - const includeDefault = parent !== null ? this.isTypeHasRulesForOperation(parent, mutationOperation) : false; - const directives = this.getDirectivesForRules(rules, includeDefault); + // Create the authMode if block and add it to the resolver + expressions.push(this.resources.getAuthModeCheckWrappedExpression(authModesToCheck, authorizationLogic)); + const templateParts = [ + print(field && ifCondition ? iff(ifCondition, compoundExpression(expressions)) : compoundExpression(expressions)), + resolver.Properties.RequestMappingTemplate, + ]; + resolver.Properties.RequestMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, resolver); + } + + // if protect is for field and there is a subscription for update / delete then protect the field in that operation + if ( + field && + subscriptionOperation && + modelConfiguration.shouldHave(subscriptionOperation) && + (modelConfiguration.getName('level') as ModelSubscriptionLevel) === 'on' + ) { + let mutationResolver = resolver; + let mutationResolverResourceID = resolverResourceId; + // if we are protecting delete then we need to get the delete resolver + if (subscriptionOperation === 'onDelete') { + mutationResolverResourceID = ResolverResourceIDs.DynamoDBDeleteResolverResourceID(parent.name.value); + mutationResolver = ctx.getResource(mutationResolverResourceID); + } + const getTemplateParts = [mutationResolver.Properties.ResponseMappingTemplate]; + if (!this.isOperationExpressionSet(mutationTypeName, mutationResolver.Properties.ResponseMappingTemplate)) { + getTemplateParts.unshift(this.resources.setOperationExpression(mutationTypeName)); + } + mutationResolver.Properties.ResponseMappingTemplate = getTemplateParts.join('\n\n'); + ctx.setResource(mutationResolverResourceID, mutationResolver); + } + } + } + + /** + * If we are protecting the mutation for a field level @auth directive, include + * the necessary if condition. + * @param ctx The transformer context + * @param resolverResourceId The resolver resource id + * @param rules The delete rules + * @param parent The parent object + * @param field The optional field + */ + private protectUpdateMutation( + ctx: TransformerContext, + resolverResourceId: string, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + modelConfiguration: ModelDirectiveConfiguration, + field?: FieldDefinitionNode, + subscriptionOperation?: ModelDirectiveOperationType + ) { + return this.protectUpdateOrDeleteMutation( + ctx, + resolverResourceId, + rules, + parent, + modelConfiguration, + true, + field, + field ? raw(`$ctx.args.input.containsKey("${field.name.value}")`) : undefined, + subscriptionOperation + ); + } + + /** + * If we are protecting the mutation for a field level @auth directive, include + * the necessary if condition. + * @param ctx The transformer context + * @param resolverResourceId The resolver resource id + * @param rules The delete rules + * @param parent The parent object + * @param field The optional field + */ + private protectDeleteMutation( + ctx: TransformerContext, + resolverResourceId: string, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + modelConfiguration: ModelDirectiveConfiguration, + field?: FieldDefinitionNode, + subscriptionOperation?: ModelDirectiveOperationType + ) { + return this.protectUpdateOrDeleteMutation( + ctx, + resolverResourceId, + rules, + parent, + modelConfiguration, + false, + field, + field + ? raw(`$ctx.args.input.containsKey("${field.name.value}") && $util.isNull($ctx.args.input.get("${field.name.value}"))`) + : undefined, + subscriptionOperation + ); + } + + /** + * When read operations are protected via @auth, all @connection resolvers will be protected. + * Find the directives & update their resolvers with auth logic + */ + private protectConnections( + ctx: TransformerContext, + def: ObjectTypeDefinitionNode, + rules: AuthRule[], + modelConfiguration: ModelDirectiveConfiguration + ) { + const thisModelName = def.name.value; + for (const inputDef of ctx.inputDocument.definitions) { + if (inputDef.kind === Kind.OBJECT_TYPE_DEFINITION) { + for (const field of inputDef.fields) { + const returnTypeName = getBaseType(field.type); + if (fieldHasDirective(field, 'connection') && returnTypeName === thisModelName) { + const resolverResourceId = ResolverResourceIDs.ResolverResourceID(inputDef.name.value, field.name.value); + + // Add the auth directives to the connection to make sure the + // member is accessible. + const directives = this.getDirectivesForRules(rules, false); if (directives.length > 0) { - this.addDirectivesToField(ctx, ctx.getSubscriptionTypeName(), fieldName, directives); + this.addDirectivesToField(ctx, inputDef.name.value, field.name.value, directives); } - this.addFieldToResourceReferences(ctx.getSubscriptionTypeName(), fieldName, rules); - - // Break the rules out by strategy. - const staticGroupAuthorizationRules = this.getStaticGroupRules(rules); - const ownerAuthorizationRules = this.getOwnerRules(rules); - - if (staticGroupAuthorizationRules.length > 0 || - ownerAuthorizationRules.length > 0 ) { - const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression( - staticGroupAuthorizationRules); - const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForSubscriptions( - ownerAuthorizationRules); - - const throwIfUnauthorizedExpression = this.resources.throwIfSubscriptionUnauthorized(); - - // Populate a list of configured authentication providers based on the rules - const authModesToCheck = new Set(); - const expressions: Array = new Array(); - - if (ownerAuthorizationRules.find((r) => r.provider === 'userPools') || - staticGroupAuthorizationRules.length > 0) { - authModesToCheck.add('userPools'); - } - if (ownerAuthorizationRules.find((r) => r.provider === 'oidc')) { - authModesToCheck.add('oidc'); - } - - // If we've any modes to check, then add the authMode check code block - // to the start of the resolver. - if (authModesToCheck.size > 0) { - expressions.push (this.resources.getAuthModeDeterminationExpression(authModesToCheck)); - } - - const authCheckExpressions = [ - staticGroupAuthorizationExpression, - newline(), - ownerAuthorizationExpression, - newline(), - throwIfUnauthorizedExpression - ]; - - // Create the authMode if block and add it to the resolver - expressions.push( - this.resources.getAuthModeCheckWrappedExpression( - authModesToCheck, - compoundExpression(authCheckExpressions)) - ); - - const templateParts = [ - print(compoundExpression(expressions)), - resolver.Properties.ResponseMappingTemplate - ]; - - resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n') - ctx.setResource(resolverResourceId, resolver); - - // check if owner is enabled in auth - const ownerRules = rules.filter(rule => rule.allow === OWNER_AUTH_STRATEGY); - const needsDefaultOwnerField = ownerRules.find(rule => !rule.ownerField); - const hasStaticGroupAuth = rules.find(rule => rule.allow === GROUPS_AUTH_STRATEGY && !rule.groupsField); - if (ownerRules) { - // if there is an owner rule without ownerField add the owner field in the type - if (needsDefaultOwnerField) { - this.addOwner(ctx, parent.name.value); - } - // If static group is specified in any of the rules then it would specify the owner arg(s) as optional - const makeNonNull = hasStaticGroupAuth ? false : true; - this.addSubscriptionOwnerArgument(ctx, resolver, ownerRules, makeNonNull); - } + if (isListType(field.type)) { + this.protectListQuery(ctx, resolverResourceId, rules, null, modelConfiguration); + } else { + this.protectGetQuery(ctx, resolverResourceId, rules, null, modelConfiguration); } + } } - // If the subscription level is set to public it adds the subscription resolver with no auth logic - if (!noneDS) { - ctx.setResource(ResourceConstants.RESOURCES.NoneDataSource, this.resources.noneDataSource()) - } - // finally map the resource to the stack - ctx.mapResourceToStack(parent.name.value, resolverResourceId); + } } - - private addSubscriptionOwnerArgument(ctx: TransformerContext, resolver: Resolver, - ownerRules: AuthRule[], makeNonNull: boolean = false) { - let subscription = ctx.getSubscription(); - let createField: FieldDefinitionNode = subscription.fields.find( - field => field.name.value === resolver.Properties.FieldName, - ) as FieldDefinitionNode; - const nameNode: any = makeNonNull ? makeNonNullType(makeNamedType('String')) : makeNamedType('String'); - // const createArguments = [makeInputValueDefinition(DEFAULT_OWNER_FIELD, nameNode)]; - const ownerArgumentList = ownerRules.map( rule => { - return makeInputValueDefinition(rule.ownerField || DEFAULT_OWNER_FIELD, nameNode); - }) - createField = { - ...createField, - arguments: ownerArgumentList, - }; - subscription = { - ...subscription, - fields: subscription.fields.map( - field => field.name.value === resolver.Properties.FieldName ? createField : field, - ), - }; - ctx.putType(subscription); + } + + /** + * When read operations are protected via @auth, all secondary @key query resolvers will be protected. + * Find the directives & update their resolvers with auth logic + */ + private protectQueries( + ctx: TransformerContext, + def: ObjectTypeDefinitionNode, + rules: AuthRule[], + modelConfiguration: ModelDirectiveConfiguration + ) { + const secondaryKeyDirectivesWithQueries = (def.directives || []).filter(d => { + const isKey = d.name.value === 'key'; + const args = getDirectiveArguments(d); + // @key with a name is a secondary key. + const isSecondaryKey = Boolean(args.name); + const hasQueryField = Boolean(args.queryField); + return isKey && isSecondaryKey && hasQueryField; + }); + for (const keyWithQuery of secondaryKeyDirectivesWithQueries) { + const args = getDirectiveArguments(keyWithQuery); + const resolverResourceId = ResolverResourceIDs.ResolverResourceID(ctx.getQueryTypeName(), args.queryField); + this.protectListQuery(ctx, resolverResourceId, rules, null, modelConfiguration, args.queryField); } - - private addOwner(ctx: TransformerContext, parent: string) { - const modelType: any = ctx.getType(parent); - const fields = getFieldArguments(modelType); - if (!("owner" in fields)) { - modelType.fields.push( - makeField( - DEFAULT_OWNER_FIELD, - [], - makeNamedType('String'), - ) - ) - } - ctx.putType(modelType); + } + + private protectSearchQuery(ctx: TransformerContext, def: ObjectTypeDefinitionNode, resolverResourceId: string, rules: AuthRule[]) { + const resolver = ctx.getResource(resolverResourceId); + if (!rules || rules.length === 0 || !resolver) { + return; + } else { + const operationName = resolver.Properties.FieldName; + const includeDefault = def !== null ? this.isTypeHasRulesForOperation(def, 'list') : false; + const operationDirectives = this.getDirectivesForRules(rules, includeDefault); + if (operationDirectives.length > 0) { + this.addDirectivesToOperation(ctx, ctx.getQueryTypeName(), operationName, operationDirectives); + } + this.addFieldToResourceReferences(ctx.getQueryTypeName(), operationName, rules); + // create auth expression + const authExpression = this.authorizationExpressionForListResult(rules, 'es_items'); + if (authExpression) { + const templateParts = [ + print(this.resources.makeESItemsExpression()), + print(authExpression), + print(this.resources.makeESToGQLExpression()), + ]; + resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, resolver); + } } - - private getOwnerRules(rules: AuthRule[]): AuthRule[] { - return rules.filter(rule => rule.allow === 'owner'); + } + + // OnCreate Subscription + private protectOnCreateSubscription( + ctx: TransformerContext, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + modelConfiguration: ModelDirectiveConfiguration + ) { + const names = modelConfiguration.getNames('onCreate'); + const level = modelConfiguration.getName('level') as ModelSubscriptionLevel; + if (names) { + names.forEach(name => { + this.addSubscriptionResolvers(ctx, rules, parent, level, name, 'create'); + }); } - - private getStaticGroupRules(rules: AuthRule[]): AuthRule[] { - return rules.filter(rule => rule.allow === 'groups' && Boolean(rule.groups)); + } + + // OnUpdate Subscription + private protectOnUpdateSubscription( + ctx: TransformerContext, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + modelConfiguration: ModelDirectiveConfiguration + ) { + const names = modelConfiguration.getNames('onUpdate'); + const level = modelConfiguration.getName('level') as ModelSubscriptionLevel; + if (names) { + names.forEach(name => { + this.addSubscriptionResolvers(ctx, rules, parent, level, name, 'update'); + }); } - - private getDynamicGroupRules(rules: AuthRule[]): AuthRule[] { - return rules.filter(rule => rule.allow === 'groups' && !Boolean(rule.groups)); + } + + // OnDelete Subscription + private protectOnDeleteSubscription( + ctx: TransformerContext, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + modelConfiguration: ModelDirectiveConfiguration + ) { + const names = modelConfiguration.getNames('onDelete'); + const level = modelConfiguration.getName('level') as ModelSubscriptionLevel; + if (names) { + names.forEach(name => { + this.addSubscriptionResolvers(ctx, rules, parent, level, name, 'delete'); + }); } + } + + // adds subscription resolvers (request / response) based on the operation provided + private addSubscriptionResolvers( + ctx: TransformerContext, + rules: AuthRule[], + parent: ObjectTypeDefinitionNode, + level: ModelSubscriptionLevel, + fieldName: string, + mutationOperation: ModelDirectiveOperationType + ) { + const resolverResourceId = ResolverResourceIDs.ResolverResourceID('Subscription', fieldName); + const resolver = this.resources.generateSubscriptionResolver(fieldName); + // If the data source does not exist it is created and added as a resource for public && on levels + const noneDS = ctx.getResource(ResourceConstants.RESOURCES.NoneDataSource); + + // add the rules in the subscription resolver + if (!rules || rules.length === 0) { + return; + } else if (level === 'public') { + // set the resource with no auth logic + ctx.setResource(resolverResourceId, resolver); + } else { + // Get the directives we need to add to the GraphQL nodes + const includeDefault = parent !== null ? this.isTypeHasRulesForOperation(parent, mutationOperation) : false; + const directives = this.getDirectivesForRules(rules, includeDefault); + + if (directives.length > 0) { + this.addDirectivesToField(ctx, ctx.getSubscriptionTypeName(), fieldName, directives); + } + + this.addFieldToResourceReferences(ctx.getSubscriptionTypeName(), fieldName, rules); + + // Break the rules out by strategy. + const staticGroupAuthorizationRules = this.getStaticGroupRules(rules); + const ownerAuthorizationRules = this.getOwnerRules(rules); + + if (staticGroupAuthorizationRules.length > 0 || ownerAuthorizationRules.length > 0) { + const staticGroupAuthorizationExpression = this.resources.staticGroupAuthorizationExpression(staticGroupAuthorizationRules); + const ownerAuthorizationExpression = this.resources.ownerAuthorizationExpressionForSubscriptions(ownerAuthorizationRules); + + const throwIfUnauthorizedExpression = this.resources.throwIfSubscriptionUnauthorized(); + + // Populate a list of configured authentication providers based on the rules + const authModesToCheck = new Set(); + const expressions: Array = new Array(); + + if (ownerAuthorizationRules.find(r => r.provider === 'userPools') || staticGroupAuthorizationRules.length > 0) { + authModesToCheck.add('userPools'); + } + if (ownerAuthorizationRules.find(r => r.provider === 'oidc')) { + authModesToCheck.add('oidc'); + } + + // If we've any modes to check, then add the authMode check code block + // to the start of the resolver. + if (authModesToCheck.size > 0) { + expressions.push(this.resources.getAuthModeDeterminationExpression(authModesToCheck)); + } + + const authCheckExpressions = [ + staticGroupAuthorizationExpression, + newline(), + ownerAuthorizationExpression, + newline(), + throwIfUnauthorizedExpression, + ]; - private extendTypeWithDirectives(ctx: TransformerContext, typeName: string, directives: DirectiveNode[]) { - let objectTypeExtension = blankObjectExtension(typeName); - - objectTypeExtension = extensionWithDirectives( - objectTypeExtension, - directives - ); - - ctx.addObjectExtension(objectTypeExtension); - } - - private addDirectivesToOperation(ctx: TransformerContext, typeName: string, operationName: string, directives: DirectiveNode[]) { - // Add the directives to the given operation - this.addDirectivesToField(ctx, typeName, operationName, directives); - - // Add the directives to the result type of the operation; - const type = ctx.getType(typeName) as ObjectTypeDefinitionNode; - - if (type) { - const field = type.fields.find((f) => f.name.value === operationName); + // Create the authMode if block and add it to the resolver + expressions.push(this.resources.getAuthModeCheckWrappedExpression(authModesToCheck, compoundExpression(authCheckExpressions))); - if (field) { - const returnFieldType = field.type as NamedTypeNode; + const templateParts = [print(compoundExpression(expressions)), resolver.Properties.ResponseMappingTemplate]; - if (returnFieldType.name) { - const returnTypeName = returnFieldType.name.value; + resolver.Properties.ResponseMappingTemplate = templateParts.join('\n\n'); + ctx.setResource(resolverResourceId, resolver); - this.extendTypeWithDirectives(ctx, returnTypeName, directives); - } - } + // check if owner is enabled in auth + const ownerRules = rules.filter(rule => rule.allow === OWNER_AUTH_STRATEGY); + const needsDefaultOwnerField = ownerRules.find(rule => !rule.ownerField); + const hasStaticGroupAuth = rules.find(rule => rule.allow === GROUPS_AUTH_STRATEGY && !rule.groupsField); + if (ownerRules) { + // if there is an owner rule without ownerField add the owner field in the type + if (needsDefaultOwnerField) { + this.addOwner(ctx, parent.name.value); + } + // If static group is specified in any of the rules then it would specify the owner arg(s) as optional + const makeNonNull = hasStaticGroupAuth ? false : true; + this.addSubscriptionOwnerArgument(ctx, resolver, ownerRules, makeNonNull); } + } + } + // If the subscription level is set to public it adds the subscription resolver with no auth logic + if (!noneDS) { + ctx.setResource(ResourceConstants.RESOURCES.NoneDataSource, this.resources.noneDataSource()); } + // finally map the resource to the stack + ctx.mapResourceToStack(parent.name.value, resolverResourceId); + } + + private addSubscriptionOwnerArgument(ctx: TransformerContext, resolver: Resolver, ownerRules: AuthRule[], makeNonNull: boolean = false) { + let subscription = ctx.getSubscription(); + let createField: FieldDefinitionNode = subscription.fields.find( + field => field.name.value === resolver.Properties.FieldName + ) as FieldDefinitionNode; + const nameNode: any = makeNonNull ? makeNonNullType(makeNamedType('String')) : makeNamedType('String'); + // const createArguments = [makeInputValueDefinition(DEFAULT_OWNER_FIELD, nameNode)]; + const ownerArgumentList = ownerRules.map(rule => { + return makeInputValueDefinition(rule.ownerField || DEFAULT_OWNER_FIELD, nameNode); + }); + createField = { + ...createField, + arguments: ownerArgumentList, + }; + subscription = { + ...subscription, + fields: subscription.fields.map(field => (field.name.value === resolver.Properties.FieldName ? createField : field)), + }; + ctx.putType(subscription); + } + + private addOwner(ctx: TransformerContext, parent: string) { + const modelType: any = ctx.getType(parent); + const fields = getFieldArguments(modelType); + if (!('owner' in fields)) { + modelType.fields.push(makeField(DEFAULT_OWNER_FIELD, [], makeNamedType('String'))); + } + ctx.putType(modelType); + } - private addDirectivesToField(ctx: TransformerContext, typeName: string, fieldName: string, directives: DirectiveNode[]) { - const type = ctx.getType(typeName) as ObjectTypeDefinitionNode; + private getOwnerRules(rules: AuthRule[]): AuthRule[] { + return rules.filter(rule => rule.allow === 'owner'); + } - if (type) { - const field = type.fields.find((f) => f.name.value === fieldName); + private getStaticGroupRules(rules: AuthRule[]): AuthRule[] { + return rules.filter(rule => rule.allow === 'groups' && Boolean(rule.groups)); + } - if (field) { - const newFields = [ - ...type.fields.filter((f) => f.name.value !== field.name.value), - extendFieldWithDirectives(field, directives) - ]; + private getDynamicGroupRules(rules: AuthRule[]): AuthRule[] { + return rules.filter(rule => rule.allow === 'groups' && !Boolean(rule.groups)); + } - const newMutation = { - ...type, - fields: newFields - }; + private extendTypeWithDirectives(ctx: TransformerContext, typeName: string, directives: DirectiveNode[]) { + let objectTypeExtension = blankObjectExtension(typeName); - ctx.putType(newMutation); - } - } - } + objectTypeExtension = extensionWithDirectives(objectTypeExtension, directives); - private getDirectivesForRules(rules: AuthRule[], addDefaultIfNeeded: boolean = true): DirectiveNode[] { - if (!rules || rules.length === 0) { - return []; - } + ctx.addObjectExtension(objectTypeExtension); + } - const directives: DirectiveNode[] = new Array(); - - // - // We only add a directive if it is not the default auth or - // if it is the default one, but there are other rules for a - // different provider. - // For fields we don't add the default, since it would open up - // the access rights. - // - - const addDirectiveIfNeeded = (provider: AuthProvider, directiveName: string) => { - if ((this.configuredAuthProviders.default !== provider && - Boolean(rules.find((r) => r.provider === provider))) || - (this.configuredAuthProviders.default === provider && - Boolean(rules.find((r) => r.provider !== provider && - addDefaultIfNeeded === true)) - )) { - directives.push(makeDirective(directiveName, [])); - } - } + private addDirectivesToOperation(ctx: TransformerContext, typeName: string, operationName: string, directives: DirectiveNode[]) { + // Add the directives to the given operation + this.addDirectivesToField(ctx, typeName, operationName, directives); - const authProviderDirectiveMap = new Map([ - ['apiKey', 'aws_api_key'], - ['iam', 'aws_iam'], - ['oidc', 'aws_oidc'], - ['userPools', 'aws_cognito_user_pools'], - ]); + // Add the directives to the result type of the operation; + const type = ctx.getType(typeName) as ObjectTypeDefinitionNode; - for (const entry of authProviderDirectiveMap.entries()) { - addDirectiveIfNeeded(entry[0], entry[1]); - } + if (type) { + const field = type.fields.find(f => f.name.value === operationName); - // - // If we've any rules for other than the default provider AND - // we've rules for the default provider as well add the default provider's - // directive, regardless of the addDefaultIfNeeded flag. - // - // For example if we've this rule and the default is API_KEY: - // - // @auth(rules: [{allow: owner},{allow: public, operations: [read]}]) - // - // Then we need to add @aws_api_key on the create mutation together with the - // @aws_cognito_useR_pools, but we cannot add @was_api_key to other operations - // since that is not allowed by the rule. - // - - if (Boolean(rules.find((r) => r.provider === this.configuredAuthProviders.default)) && - Boolean(rules.find((r) => r.provider !== this.configuredAuthProviders.default) && - !Boolean(directives.find((d) => d.name.value === - authProviderDirectiveMap.get(this.configuredAuthProviders.default)))) - ) { - directives.push(makeDirective( - authProviderDirectiveMap.get(this.configuredAuthProviders.default), [])); - } + if (field) { + const returnFieldType = field.type as NamedTypeNode; - return directives; - } + if (returnFieldType.name) { + const returnTypeName = returnFieldType.name.value; - private ensureDefaultAuthProviderAssigned(rules: AuthRule[]) { - // We assign the default provider if an override is not present make further handling easier. - for (const rule of rules) { - if (!rule.provider) { - switch (rule.allow) { - case 'owner': - case 'groups': - rule.provider = 'userPools'; - break; - case 'private': - rule.provider = 'userPools' - break; - case 'public': - rule.provider = 'apiKey' - break; - default: - rule.provider = null; - break; - } - } + this.extendTypeWithDirectives(ctx, returnTypeName, directives); } + } } + } - private validateRuleAuthStrategy(rule: AuthRule) { - // - // Groups - // + private addDirectivesToField(ctx: TransformerContext, typeName: string, fieldName: string, directives: DirectiveNode[]) { + const type = ctx.getType(typeName) as ObjectTypeDefinitionNode; - if (rule.allow === 'groups' && rule.provider !== 'userPools') { - throw new InvalidDirectiveError( - `@auth directive with 'groups' strategy only supports 'userPools' provider, but found '${rule.provider}' assigned.`); - } + if (type) { + const field = type.fields.find(f => f.name.value === fieldName); - // - // Owner - // + if (field) { + const newFields = [...type.fields.filter(f => f.name.value !== field.name.value), extendFieldWithDirectives(field, directives)]; - if (rule.allow === 'owner') { - if (rule.provider !== null && rule.provider !== 'userPools' && rule.provider !== 'oidc') { - throw new InvalidDirectiveError( - `@auth directive with 'owner' strategy only supports 'userPools' (default) and 'oidc' providers, but \ -found '${rule.provider}' assigned.`); - } - } - - // - // Public - // + const newMutation = { + ...type, + fields: newFields, + }; - if (rule.allow === 'public') { - if (rule.provider !== null && rule.provider !== 'apiKey' && rule.provider !== 'iam') { - throw new InvalidDirectiveError( - `@auth directive with 'public' strategy only supports 'apiKey' (default) and 'iam' providers, but \ -found '${rule.provider}' assigned.`); - } - } + ctx.putType(newMutation); + } + } + } - // - // Private - // + private getDirectivesForRules(rules: AuthRule[], addDefaultIfNeeded: boolean = true): DirectiveNode[] { + if (!rules || rules.length === 0) { + return []; + } - if (rule.allow === 'private') { - if (rule.provider !== null && rule.provider !== 'userPools' && rule.provider !== 'iam') { - throw new InvalidDirectiveError( - `@auth directive with 'private' strategy only supports 'userPools' (default) and 'iam' providers, but \ -found '${rule.provider}' assigned.`); - } - } + const directives: DirectiveNode[] = new Array(); + + // + // We only add a directive if it is not the default auth or + // if it is the default one, but there are other rules for a + // different provider. + // For fields we don't add the default, since it would open up + // the access rights. + // + + const addDirectiveIfNeeded = (provider: AuthProvider, directiveName: string) => { + if ( + (this.configuredAuthProviders.default !== provider && Boolean(rules.find(r => r.provider === provider))) || + (this.configuredAuthProviders.default === provider && + Boolean(rules.find(r => r.provider !== provider && addDefaultIfNeeded === true))) + ) { + directives.push(makeDirective(directiveName, [])); + } + }; + + const authProviderDirectiveMap = new Map([ + ['apiKey', 'aws_api_key'], + ['iam', 'aws_iam'], + ['oidc', 'aws_oidc'], + ['userPools', 'aws_cognito_user_pools'], + ]); + + for (const entry of authProviderDirectiveMap.entries()) { + addDirectiveIfNeeded(entry[0], entry[1]); + } - // - // Validate provider values against project configuration. - // - - if (rule.provider === 'apiKey' && this.configuredAuthProviders.hasApiKey === false) { - throw new InvalidDirectiveError( - `@auth directive with 'apiKey' provider found, but the project has no API Key authentication provider configured.`); - } else if (rule.provider === 'oidc' && this.configuredAuthProviders.hasOIDC === false) { - throw new InvalidDirectiveError( - `@auth directive with 'oidc' provider found, but the project has no OPENID_CONNECT authentication provider configured.`); - } else if (rule.provider === 'userPools' && this.configuredAuthProviders.hasUserPools === false) { - throw new InvalidDirectiveError( - `@auth directive with 'userPools' provider found, but the project has no Cognito User Pools authentication provider configured.`); - } else if (rule.provider === 'iam' && this.configuredAuthProviders.hasIAM === false) { - throw new InvalidDirectiveError( - `@auth directive with 'iam' provider found, but the project has no IAM authentication provider configured.`); - } + // + // If we've any rules for other than the default provider AND + // we've rules for the default provider as well add the default provider's + // directive, regardless of the addDefaultIfNeeded flag. + // + // For example if we've this rule and the default is API_KEY: + // + // @auth(rules: [{allow: owner},{allow: public, operations: [read]}]) + // + // Then we need to add @aws_api_key on the create mutation together with the + // @aws_cognito_useR_pools, but we cannot add @was_api_key to other operations + // since that is not allowed by the rule. + // + + if ( + Boolean(rules.find(r => r.provider === this.configuredAuthProviders.default)) && + Boolean( + rules.find(r => r.provider !== this.configuredAuthProviders.default) && + !Boolean(directives.find(d => d.name.value === authProviderDirectiveMap.get(this.configuredAuthProviders.default))) + ) + ) { + directives.push(makeDirective(authProviderDirectiveMap.get(this.configuredAuthProviders.default), [])); } - private getConfiguredAuthProviders(): ConfiguredAuthProviders { - const providers = [ - this.config.authConfig.defaultAuthentication.authenticationType, - ...this.config.authConfig.additionalAuthenticationProviders.map((p) => p.authenticationType) - ]; + return directives; + } + + private ensureDefaultAuthProviderAssigned(rules: AuthRule[]) { + // We assign the default provider if an override is not present make further handling easier. + for (const rule of rules) { + if (!rule.provider) { + switch (rule.allow) { + case 'owner': + case 'groups': + rule.provider = 'userPools'; + break; + case 'private': + rule.provider = 'userPools'; + break; + case 'public': + rule.provider = 'apiKey'; + break; + default: + rule.provider = null; + break; + } + } + } + } - const getAuthProvider = (authType: AppSyncAuthMode): AuthProvider => { - switch (authType) { - case "AMAZON_COGNITO_USER_POOLS": - return "userPools"; - case "API_KEY": - return "apiKey"; - case "AWS_IAM": - return "iam"; - case "OPENID_CONNECT": - return "oidc"; - } - }; + private validateRuleAuthStrategy(rule: AuthRule) { + // + // Groups + // - return { - default: getAuthProvider(this.config.authConfig.defaultAuthentication.authenticationType), - onlyDefaultAuthProviderConfigured: this.config.authConfig.additionalAuthenticationProviders.length === 0, - hasApiKey: providers.find((p) => p === 'API_KEY') ? true : false, - hasUserPools: providers.find((p) => p === 'AMAZON_COGNITO_USER_POOLS') ? true : false, - hasOIDC: providers.find((p) => p === 'OPENID_CONNECT') ? true : false, - hasIAM: providers.find((p) => p === "AWS_IAM") ? true : false - }; + if (rule.allow === 'groups' && rule.provider !== 'userPools') { + throw new InvalidDirectiveError( + `@auth directive with 'groups' strategy only supports 'userPools' provider, but found '${rule.provider}' assigned.` + ); } - private setAuthPolicyFlag(rules: AuthRule[]): void { - if (!rules || rules.length === 0 || this.generateIAMPolicyforAuthRole === true) { - return; - } + // + // Owner + // - for (const rule of rules) { - if ((rule.allow === 'private' || rule.allow === 'public') && rule.provider === 'iam') { - this.generateIAMPolicyforAuthRole = true; - return; - } - } + if (rule.allow === 'owner') { + if (rule.provider !== null && rule.provider !== 'userPools' && rule.provider !== 'oidc') { + throw new InvalidDirectiveError( + `@auth directive with 'owner' strategy only supports 'userPools' (default) and 'oidc' providers, but \ +found '${rule.provider}' assigned.` + ); + } } - private setUnauthPolicyFlag(rules: AuthRule[]): void { - if (!rules || rules.length === 0 || this.generateIAMPolicyforUnauthRole === true) { - return; - } + // + // Public + // - for (const rule of rules) { - if (rule.allow === 'public' && rule.provider === 'iam') { - this.generateIAMPolicyforUnauthRole = true; - return; - } - } + if (rule.allow === 'public') { + if (rule.provider !== null && rule.provider !== 'apiKey' && rule.provider !== 'iam') { + throw new InvalidDirectiveError( + `@auth directive with 'public' strategy only supports 'apiKey' (default) and 'iam' providers, but \ +found '${rule.provider}' assigned.` + ); + } } - private getAuthRulesFromDirective(directive: DirectiveNode): AuthRule[] { - const get = (s: string) => (arg: ArgumentNode) => arg.name.value === s - const getArg = (arg: string, dflt?: any) => { - const argument = directive.arguments.find(get(arg)) - return argument ? valueFromASTUntyped(argument.value) : dflt - } + // + // Private + // - // Get and validate the auth rules. - return getArg('rules', []) as AuthRule[]; + if (rule.allow === 'private') { + if (rule.provider !== null && rule.provider !== 'userPools' && rule.provider !== 'iam') { + throw new InvalidDirectiveError( + `@auth directive with 'private' strategy only supports 'userPools' (default) and 'iam' providers, but \ +found '${rule.provider}' assigned.` + ); + } } - private isTypeNeedsDefaultProviderAccess(def: ObjectTypeDefinitionNode): boolean { - const authDirective = def.directives.find((dir) => dir.name.value === 'auth'); - if (!authDirective) { - return true; - } - - // Get and validate the auth rules. - const rules = this.getAuthRulesFromDirective(authDirective); - // Assign default providers to rules where no provider was explicitly defined - this.ensureDefaultAuthProviderAssigned(rules); + // + // Validate provider values against project configuration. + // + + if (rule.provider === 'apiKey' && this.configuredAuthProviders.hasApiKey === false) { + throw new InvalidDirectiveError( + `@auth directive with 'apiKey' provider found, but the project has no API Key authentication provider configured.` + ); + } else if (rule.provider === 'oidc' && this.configuredAuthProviders.hasOIDC === false) { + throw new InvalidDirectiveError( + `@auth directive with 'oidc' provider found, but the project has no OPENID_CONNECT authentication provider configured.` + ); + } else if (rule.provider === 'userPools' && this.configuredAuthProviders.hasUserPools === false) { + throw new InvalidDirectiveError( + `@auth directive with 'userPools' provider found, but the project has no Cognito User Pools authentication provider configured.` + ); + } else if (rule.provider === 'iam' && this.configuredAuthProviders.hasIAM === false) { + throw new InvalidDirectiveError( + `@auth directive with 'iam' provider found, but the project has no IAM authentication provider configured.` + ); + } + } + + private getConfiguredAuthProviders(): ConfiguredAuthProviders { + const providers = [ + this.config.authConfig.defaultAuthentication.authenticationType, + ...this.config.authConfig.additionalAuthenticationProviders.map(p => p.authenticationType), + ]; + + const getAuthProvider = (authType: AppSyncAuthMode): AuthProvider => { + switch (authType) { + case 'AMAZON_COGNITO_USER_POOLS': + return 'userPools'; + case 'API_KEY': + return 'apiKey'; + case 'AWS_IAM': + return 'iam'; + case 'OPENID_CONNECT': + return 'oidc'; + } + }; + + return { + default: getAuthProvider(this.config.authConfig.defaultAuthentication.authenticationType), + onlyDefaultAuthProviderConfigured: this.config.authConfig.additionalAuthenticationProviders.length === 0, + hasApiKey: providers.find(p => p === 'API_KEY') ? true : false, + hasUserPools: providers.find(p => p === 'AMAZON_COGNITO_USER_POOLS') ? true : false, + hasOIDC: providers.find(p => p === 'OPENID_CONNECT') ? true : false, + hasIAM: providers.find(p => p === 'AWS_IAM') ? true : false, + }; + } + + private setAuthPolicyFlag(rules: AuthRule[]): void { + if (!rules || rules.length === 0 || this.generateIAMPolicyforAuthRole === true) { + return; + } - return Boolean(rules.find((r) => r.provider === this.configuredAuthProviders.default)); + for (const rule of rules) { + if ((rule.allow === 'private' || rule.allow === 'public') && rule.provider === 'iam') { + this.generateIAMPolicyforAuthRole = true; + return; + } } + } - private isTypeHasRulesForOperation(def: ObjectTypeDefinitionNode, operation: ModelDirectiveOperationType): boolean { - const authDirective = def.directives.find((dir) => dir.name.value === 'auth'); - if (!authDirective) { - return false; - } + private setUnauthPolicyFlag(rules: AuthRule[]): void { + if (!rules || rules.length === 0 || this.generateIAMPolicyforUnauthRole === true) { + return; + } - // Get and validate the auth rules. - const rules = this.getAuthRulesFromDirective(authDirective); - // Assign default providers to rules where no provider was explicitly defined - this.ensureDefaultAuthProviderAssigned(rules); + for (const rule of rules) { + if (rule.allow === 'public' && rule.provider === 'iam') { + this.generateIAMPolicyforUnauthRole = true; + return; + } + } + } + + private getAuthRulesFromDirective(directive: DirectiveNode): AuthRule[] { + const get = (s: string) => (arg: ArgumentNode) => arg.name.value === s; + const getArg = (arg: string, dflt?: any) => { + const argument = directive.arguments.find(get(arg)); + return argument ? valueFromASTUntyped(argument.value) : dflt; + }; + + // Get and validate the auth rules. + return getArg('rules', []) as AuthRule[]; + } + + private isTypeNeedsDefaultProviderAccess(def: ObjectTypeDefinitionNode): boolean { + const authDirective = def.directives.find(dir => dir.name.value === 'auth'); + if (!authDirective) { + return true; + } - const { operationRules, queryRules } = this.splitRules(rules); + // Get and validate the auth rules. + const rules = this.getAuthRulesFromDirective(authDirective); + // Assign default providers to rules where no provider was explicitly defined + this.ensureDefaultAuthProviderAssigned(rules); - const hasRulesForDefaultProvider = (operationRules: AuthRule[]) => { - return Boolean(operationRules.find((r) => r.provider === this.configuredAuthProviders.default)); - }; + return Boolean(rules.find(r => r.provider === this.configuredAuthProviders.default)); + } - switch (operation) { - case 'create': - return hasRulesForDefaultProvider(operationRules.create); - case 'update': - return hasRulesForDefaultProvider(operationRules.update); - case 'delete': - return hasRulesForDefaultProvider(operationRules.delete); - case 'get': - return hasRulesForDefaultProvider(operationRules.read) || - hasRulesForDefaultProvider(queryRules.get); - case 'list': - return hasRulesForDefaultProvider(operationRules.read) || - hasRulesForDefaultProvider(queryRules.list); - } + private isTypeHasRulesForOperation(def: ObjectTypeDefinitionNode, operation: ModelDirectiveOperationType): boolean { + const authDirective = def.directives.find(dir => dir.name.value === 'auth'); + if (!authDirective) { + return false; + } - return false; + // Get and validate the auth rules. + const rules = this.getAuthRulesFromDirective(authDirective); + // Assign default providers to rules where no provider was explicitly defined + this.ensureDefaultAuthProviderAssigned(rules); + + const { operationRules, queryRules } = this.splitRules(rules); + + const hasRulesForDefaultProvider = (operationRules: AuthRule[]) => { + return Boolean(operationRules.find(r => r.provider === this.configuredAuthProviders.default)); + }; + + switch (operation) { + case 'create': + return hasRulesForDefaultProvider(operationRules.create); + case 'update': + return hasRulesForDefaultProvider(operationRules.update); + case 'delete': + return hasRulesForDefaultProvider(operationRules.delete); + case 'get': + return hasRulesForDefaultProvider(operationRules.read) || hasRulesForDefaultProvider(queryRules.get); + case 'list': + return hasRulesForDefaultProvider(operationRules.read) || hasRulesForDefaultProvider(queryRules.list); } - private addTypeToResourceReferences(typeName: string, rules: AuthRule[]) : void { - const iamPublicRules = rules.filter(r => r.allow === 'public' && r.provider === 'iam'); - const iamPrivateRules = rules.filter(r => r.allow === 'private' && r.provider === 'iam'); + return false; + } - if (iamPublicRules.length > 0) { - this.unauthPolicyResources.add(`${typeName}/null`); - this.authPolicyResources.add(`${typeName}/null`); - } - if (iamPrivateRules.length > 0) { - this.authPolicyResources.add(`${typeName}/null`); - } + private addTypeToResourceReferences(typeName: string, rules: AuthRule[]): void { + const iamPublicRules = rules.filter(r => r.allow === 'public' && r.provider === 'iam'); + const iamPrivateRules = rules.filter(r => r.allow === 'private' && r.provider === 'iam'); + + if (iamPublicRules.length > 0) { + this.unauthPolicyResources.add(`${typeName}/null`); + this.authPolicyResources.add(`${typeName}/null`); + } + if (iamPrivateRules.length > 0) { + this.authPolicyResources.add(`${typeName}/null`); } + } - private addFieldToResourceReferences(typeName: string, fieldName: string, rules: AuthRule[]) : void { - const iamPublicRules = rules.filter(r => r.allow === 'public' && r.provider === 'iam'); - const iamPrivateRules = rules.filter(r => r.allow === 'private' && r.provider === 'iam'); + private addFieldToResourceReferences(typeName: string, fieldName: string, rules: AuthRule[]): void { + const iamPublicRules = rules.filter(r => r.allow === 'public' && r.provider === 'iam'); + const iamPrivateRules = rules.filter(r => r.allow === 'private' && r.provider === 'iam'); - if (iamPublicRules.length > 0) { - this.unauthPolicyResources.add(`${typeName}/${fieldName}`); - this.authPolicyResources.add(`${typeName}/${fieldName}`); - } - if (iamPrivateRules.length > 0) { - this.authPolicyResources.add(`${typeName}/${fieldName}`); - } + if (iamPublicRules.length > 0) { + this.unauthPolicyResources.add(`${typeName}/${fieldName}`); + this.authPolicyResources.add(`${typeName}/${fieldName}`); } - - private isOperationExpressionSet(operationTypeName: string, template: string): boolean { - return template.includes(`$context.result.operation = "${operationTypeName}"`); + if (iamPrivateRules.length > 0) { + this.authPolicyResources.add(`${typeName}/${fieldName}`); } + } + + private isOperationExpressionSet(operationTypeName: string, template: string): boolean { + return template.includes(`$context.result.operation = "${operationTypeName}"`); + } } function fieldHasDirective(field: FieldDefinitionNode, directiveName: string): boolean { - return field.directives && field.directives.length && Boolean(field.directives.find( - (d: DirectiveNode) => d.name.value === directiveName - )) + return ( + field.directives && field.directives.length && Boolean(field.directives.find((d: DirectiveNode) => d.name.value === directiveName)) + ); } function isTruthyOrNull(obj: any): boolean { - return obj || obj === null; + return obj || obj === null; } function isUndefined(obj: any): boolean { - return obj === undefined; + return obj === undefined; } diff --git a/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts b/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts index c4f8b11b27..744718a7f5 100644 --- a/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts +++ b/packages/graphql-auth-transformer/src/ModelDirectiveConfiguration.ts @@ -1,195 +1,194 @@ -import { getDirectiveArguments } from 'graphql-transformer-core' -import { graphqlName, toUpper, plurality } from 'graphql-transformer-common' +import { getDirectiveArguments } from 'graphql-transformer-core'; +import { graphqlName, toUpper, plurality } from 'graphql-transformer-common'; import { ModelQuery, ModelMutation } from './AuthRule'; import { DirectiveNode, ObjectTypeDefinitionNode } from 'graphql'; export interface QueryNameMap { - get?: string; - list?: string; - query?: string; + get?: string; + list?: string; + query?: string; } export interface MutationNameMap { - create?: string; - update?: string; - delete?: string; + create?: string; + update?: string; + delete?: string; } export type ModelSubscriptionLevel = 'off' | 'public' | 'on'; export interface SubscriptionNameMap { - onCreate?: string[]; - onUpdate?: string[]; - onDelete?: string[]; - level?: ModelSubscriptionLevel; + onCreate?: string[]; + onUpdate?: string[]; + onDelete?: string[]; + level?: ModelSubscriptionLevel; } export interface ModelDirectiveArgs { - queries?: QueryNameMap, - mutations?: MutationNameMap, - subscriptions?: SubscriptionNameMap, + queries?: QueryNameMap; + mutations?: MutationNameMap; + subscriptions?: SubscriptionNameMap; } export type ModelDirectiveOperationType = ModelQuery | ModelMutation | 'onCreate' | 'onUpdate' | 'onDelete' | 'level'; type ModelDirectiveOperation = { - shouldHave: boolean, - name?: string, - names?: string[] + shouldHave: boolean; + name?: string; + names?: string[]; }; export class ModelDirectiveConfiguration { - map: Map = new Map(); - - constructor(directive: DirectiveNode, def: ObjectTypeDefinitionNode) { - const typeName = def.name.value; - const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive) - - const makeName = (operation: ModelDirectiveOperationType, nameOverride?: string, isList: boolean = false) => - nameOverride ? nameOverride : graphqlName(operation + (isList ? plurality(toUpper(typeName)) : toUpper(typeName))); - - let shouldHaveCreate = true; - let shouldHaveUpdate = true; - let shouldHaveDelete = true; - let shouldHaveGet = true; - let shouldHaveList = true; - let shouldHaveOnCreate = true; - let shouldHaveOnUpdate = true; - let shouldHaveOnDelete = true; - let shouldHaveLevel = true; - let createName: string; - let updateName: string; - let deleteName: string; - let getName: string; - let listName: string; - let onCreateNames: string[] = []; - let onUpdateNames: string[] = []; - let onDeleteNames: string[] = []; - let level: ModelSubscriptionLevel = "on"; - - // Figure out which mutations to make and if they have name overrides - if (directiveArguments.mutations === null) { - shouldHaveCreate = false; - shouldHaveUpdate = false; - shouldHaveDelete = false; - } else if (directiveArguments.mutations) { - if (!directiveArguments.mutations.create) { - shouldHaveCreate = false; - } else { - createName = makeName('create', directiveArguments.mutations.create); - } - if (!directiveArguments.mutations.update) { - shouldHaveUpdate = false; - } else { - updateName = makeName('update', directiveArguments.mutations.update); - } - if (!directiveArguments.mutations.delete) { - shouldHaveDelete = false; - } else { - deleteName = makeName('delete', directiveArguments.mutations.delete); - } - } else { - createName = makeName('create'); - updateName = makeName('update'); - deleteName = makeName('delete'); - } - - // Figure out which queries to make and if they have name overrides. - // If queries is undefined (default), create all queries - // If queries is explicetly set to null, do not create any - // else if queries is defined, check overrides - if (directiveArguments.queries === null) { - shouldHaveGet = false; - shouldHaveList = false; - } else if (directiveArguments.queries) { - if (!directiveArguments.queries.get) { - shouldHaveGet = false; - } else { - getName = makeName('get', directiveArguments.queries.get); - } - if (!directiveArguments.queries.list) { - shouldHaveList = false; - } else { - listName = makeName('list', directiveArguments.queries.list, true); - } - } else { - getName = makeName('get'); - listName = makeName('list', null, true); - } - - const subscriptions = directiveArguments.subscriptions; - if (subscriptions === null) { - shouldHaveOnCreate = false; - shouldHaveOnUpdate = false; - shouldHaveOnDelete = false; - level = 'off'; - } else if (subscriptions && - (subscriptions.onCreate || subscriptions.onUpdate || subscriptions.onDelete)) { - if (!directiveArguments.subscriptions.onCreate) { - shouldHaveOnCreate = false; - } else { - directiveArguments.subscriptions.onCreate.forEach( (name) => { - onCreateNames.push(makeName('onCreate', name)) - }); - } - if (!directiveArguments.subscriptions.onUpdate) { - shouldHaveOnUpdate = false; - } else { - directiveArguments.subscriptions.onUpdate.forEach( (name) => { - onUpdateNames.push(makeName('onUpdate', name)) - }); - } - if (!directiveArguments.subscriptions.onDelete) { - shouldHaveOnDelete = false; - } else { - directiveArguments.subscriptions.onDelete.forEach( (name) => { - onDeleteNames.push(makeName('onDelete', name)) - }); - } - } else { - onCreateNames.push(makeName('onCreate')); - onUpdateNames.push(makeName('onUpdate')); - onDeleteNames.push(makeName('onDelete')); - } - - // seperate check for level to see if it was specified in subscriptions - if (directiveArguments.subscriptions && directiveArguments.subscriptions.level) { - level = directiveArguments.subscriptions.level - } - - // if a mutation operation is missing there shouldn't be subscription operation around it - shouldHaveOnCreate = shouldHaveCreate; - shouldHaveOnUpdate = shouldHaveUpdate; - shouldHaveOnDelete = shouldHaveDelete; - - this.map.set('create', { shouldHave: shouldHaveCreate, name: createName }); - this.map.set('update', { shouldHave: shouldHaveUpdate, name: updateName }); - this.map.set('delete', { shouldHave: shouldHaveDelete, name: deleteName }); - this.map.set('get', { shouldHave: shouldHaveGet, name: getName }); - this.map.set('list', { shouldHave: shouldHaveList, name: listName }); - this.map.set('onCreate', { shouldHave: shouldHaveOnCreate, names: onCreateNames }); - this.map.set('onUpdate', { shouldHave: shouldHaveOnUpdate, names: onUpdateNames }); - this.map.set('onDelete', { shouldHave: shouldHaveOnDelete, names: onDeleteNames }); - this.map.set('level', { shouldHave: shouldHaveLevel, name: level }) + map: Map = new Map(); + + constructor(directive: DirectiveNode, def: ObjectTypeDefinitionNode) { + const typeName = def.name.value; + const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive); + + const makeName = (operation: ModelDirectiveOperationType, nameOverride?: string, isList: boolean = false) => + nameOverride ? nameOverride : graphqlName(operation + (isList ? plurality(toUpper(typeName)) : toUpper(typeName))); + + let shouldHaveCreate = true; + let shouldHaveUpdate = true; + let shouldHaveDelete = true; + let shouldHaveGet = true; + let shouldHaveList = true; + let shouldHaveOnCreate = true; + let shouldHaveOnUpdate = true; + let shouldHaveOnDelete = true; + let shouldHaveLevel = true; + let createName: string; + let updateName: string; + let deleteName: string; + let getName: string; + let listName: string; + let onCreateNames: string[] = []; + let onUpdateNames: string[] = []; + let onDeleteNames: string[] = []; + let level: ModelSubscriptionLevel = 'on'; + + // Figure out which mutations to make and if they have name overrides + if (directiveArguments.mutations === null) { + shouldHaveCreate = false; + shouldHaveUpdate = false; + shouldHaveDelete = false; + } else if (directiveArguments.mutations) { + if (!directiveArguments.mutations.create) { + shouldHaveCreate = false; + } else { + createName = makeName('create', directiveArguments.mutations.create); + } + if (!directiveArguments.mutations.update) { + shouldHaveUpdate = false; + } else { + updateName = makeName('update', directiveArguments.mutations.update); + } + if (!directiveArguments.mutations.delete) { + shouldHaveDelete = false; + } else { + deleteName = makeName('delete', directiveArguments.mutations.delete); + } + } else { + createName = makeName('create'); + updateName = makeName('update'); + deleteName = makeName('delete'); } - public shouldHave(op: ModelDirectiveOperationType): boolean { - return this.map.get(op).shouldHave; + // Figure out which queries to make and if they have name overrides. + // If queries is undefined (default), create all queries + // If queries is explicetly set to null, do not create any + // else if queries is defined, check overrides + if (directiveArguments.queries === null) { + shouldHaveGet = false; + shouldHaveList = false; + } else if (directiveArguments.queries) { + if (!directiveArguments.queries.get) { + shouldHaveGet = false; + } else { + getName = makeName('get', directiveArguments.queries.get); + } + if (!directiveArguments.queries.list) { + shouldHaveList = false; + } else { + listName = makeName('list', directiveArguments.queries.list, true); + } + } else { + getName = makeName('get'); + listName = makeName('list', null, true); } - public getName(op: ModelDirectiveOperationType): string | undefined { - const { shouldHave, name } = this.map.get(op); + const subscriptions = directiveArguments.subscriptions; + if (subscriptions === null) { + shouldHaveOnCreate = false; + shouldHaveOnUpdate = false; + shouldHaveOnDelete = false; + level = 'off'; + } else if (subscriptions && (subscriptions.onCreate || subscriptions.onUpdate || subscriptions.onDelete)) { + if (!directiveArguments.subscriptions.onCreate) { + shouldHaveOnCreate = false; + } else { + directiveArguments.subscriptions.onCreate.forEach(name => { + onCreateNames.push(makeName('onCreate', name)); + }); + } + if (!directiveArguments.subscriptions.onUpdate) { + shouldHaveOnUpdate = false; + } else { + directiveArguments.subscriptions.onUpdate.forEach(name => { + onUpdateNames.push(makeName('onUpdate', name)); + }); + } + if (!directiveArguments.subscriptions.onDelete) { + shouldHaveOnDelete = false; + } else { + directiveArguments.subscriptions.onDelete.forEach(name => { + onDeleteNames.push(makeName('onDelete', name)); + }); + } + } else { + onCreateNames.push(makeName('onCreate')); + onUpdateNames.push(makeName('onUpdate')); + onDeleteNames.push(makeName('onDelete')); + } + + // seperate check for level to see if it was specified in subscriptions + if (directiveArguments.subscriptions && directiveArguments.subscriptions.level) { + level = directiveArguments.subscriptions.level; + } - if (shouldHave) { - return name; - } + // if a mutation operation is missing there shouldn't be subscription operation around it + shouldHaveOnCreate = shouldHaveCreate; + shouldHaveOnUpdate = shouldHaveUpdate; + shouldHaveOnDelete = shouldHaveDelete; + + this.map.set('create', { shouldHave: shouldHaveCreate, name: createName }); + this.map.set('update', { shouldHave: shouldHaveUpdate, name: updateName }); + this.map.set('delete', { shouldHave: shouldHaveDelete, name: deleteName }); + this.map.set('get', { shouldHave: shouldHaveGet, name: getName }); + this.map.set('list', { shouldHave: shouldHaveList, name: listName }); + this.map.set('onCreate', { shouldHave: shouldHaveOnCreate, names: onCreateNames }); + this.map.set('onUpdate', { shouldHave: shouldHaveOnUpdate, names: onUpdateNames }); + this.map.set('onDelete', { shouldHave: shouldHaveOnDelete, names: onDeleteNames }); + this.map.set('level', { shouldHave: shouldHaveLevel, name: level }); + } + + public shouldHave(op: ModelDirectiveOperationType): boolean { + return this.map.get(op).shouldHave; + } + + public getName(op: ModelDirectiveOperationType): string | undefined { + const { shouldHave, name } = this.map.get(op); + + if (shouldHave) { + return name; } + } - public getNames(op: ModelDirectiveOperationType): string [] | undefined { - const { shouldHave, names } = this.map.get(op); + public getNames(op: ModelDirectiveOperationType): string[] | undefined { + const { shouldHave, names } = this.map.get(op); - if (shouldHave) { - return names; - } + if (shouldHave) { + return names; } + } } diff --git a/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts b/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts index 4a63c20cb6..d349ec408d 100644 --- a/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/GroupAuthTransformer.test.ts @@ -1,38 +1,39 @@ -import GraphQLTransform from 'graphql-transformer-core' -import { ResourceConstants } from 'graphql-transformer-common' -import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer' -import { ModelAuthTransformer } from '../ModelAuthTransformer' +import GraphQLTransform from 'graphql-transformer-core'; +import { ResourceConstants } from 'graphql-transformer-common'; +import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; +import { ModelAuthTransformer } from '../ModelAuthTransformer'; test('Test ModelAuthTransformer validation happy case w/ static groups', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [{allow: groups, groups: ["Admin", "Dev"]}]) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); }); test('Test ModelAuthTransformer validation happy case w/ dynamic groups', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [{allow: groups, groupsField: "groups"}]) { id: ID! title: String! @@ -40,28 +41,29 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic groups', () => createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); }); test('Test ModelAuthTransformer validation happy case w/ dynamic group', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [{allow: groups, groupsField: "group"}]) { id: ID! title: String! @@ -69,29 +71,30 @@ test('Test ModelAuthTransformer validation happy case w/ dynamic group', () => { createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); }); test('Test ModelAuthTransformer validation @auth on non @model. Should fail.', () => { - try { - const validSchema = ` + try { + const validSchema = ` type Post @auth(rules: [{allow: groups, groupsField: "groups"}]) { id: ID! title: String! @@ -99,22 +102,23 @@ test('Test ModelAuthTransformer validation @auth on non @model. Should fail.', ( createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(true).toEqual(false) - } catch (e) { - expect(e).toBeDefined() - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(true).toEqual(false); + } catch (e) { + expect(e).toBeDefined(); + } }); diff --git a/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts b/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts index dd78618176..e2e93ba468 100644 --- a/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/MultiAuth.test.ts @@ -1,63 +1,60 @@ -import { - ObjectTypeDefinitionNode, parse, DocumentNode, - Kind -} from 'graphql' -import GraphQLTransform from 'graphql-transformer-core' -import { ResourceConstants } from 'graphql-transformer-common' -import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer' +import { ObjectTypeDefinitionNode, parse, DocumentNode, Kind } from 'graphql'; +import GraphQLTransform from 'graphql-transformer-core'; +import { ResourceConstants } from 'graphql-transformer-common'; +import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; import { ModelConnectionTransformer } from 'graphql-connection-transformer'; -import { ModelAuthTransformer, AppSyncAuthConfiguration, AppSyncAuthMode } from '../ModelAuthTransformer' +import { ModelAuthTransformer, AppSyncAuthConfiguration, AppSyncAuthMode } from '../ModelAuthTransformer'; const noAuthModeDefaultConfig: AppSyncAuthConfiguration = { - defaultAuthentication: { - authenticationType: undefined - }, - additionalAuthenticationProviders: [] + defaultAuthentication: { + authenticationType: undefined, + }, + additionalAuthenticationProviders: [], }; const userPoolsDefaultConfig: AppSyncAuthConfiguration = { - defaultAuthentication: { - authenticationType: 'AMAZON_COGNITO_USER_POOLS' - }, - additionalAuthenticationProviders: [] + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], }; const apiKeyDefaultConfig: AppSyncAuthConfiguration = { - defaultAuthentication: { - authenticationType: 'API_KEY' - }, - additionalAuthenticationProviders: [] + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], }; const openIdDefaultConfig: AppSyncAuthConfiguration = { - defaultAuthentication: { - authenticationType: 'OPENID_CONNECT' - }, - additionalAuthenticationProviders: [] + defaultAuthentication: { + authenticationType: 'OPENID_CONNECT', + }, + additionalAuthenticationProviders: [], }; const iamDefaultConfig: AppSyncAuthConfiguration = { - defaultAuthentication: { - authenticationType: 'AWS_IAM' - }, - additionalAuthenticationProviders: [] + defaultAuthentication: { + authenticationType: 'AWS_IAM', + }, + additionalAuthenticationProviders: [], }; const withAuthModes = (authConfig: AppSyncAuthConfiguration, authModes: AppSyncAuthMode[]): AppSyncAuthConfiguration => { - const newAuthConfig = { - defaultAuthentication: { - authenticationType: authConfig.defaultAuthentication.authenticationType - }, - additionalAuthenticationProviders: [] - } + const newAuthConfig = { + defaultAuthentication: { + authenticationType: authConfig.defaultAuthentication.authenticationType, + }, + additionalAuthenticationProviders: [], + }; - for (const authMode of authModes) { - newAuthConfig.additionalAuthenticationProviders.push({ - authenticationType: authMode - }) - } + for (const authMode of authModes) { + newAuthConfig.additionalAuthenticationProviders.push({ + authenticationType: authMode, + }); + } - return newAuthConfig; + return newAuthConfig; }; const apiKeyDirectiveName = 'aws_api_key'; @@ -65,7 +62,8 @@ const userPoolsDirectiveName = 'aws_cognito_user_pools'; const iamDirectiveName = 'aws_iam'; const openIdDirectiveName = 'aws_oidc'; -const multiAuthDirective = '@auth(rules: [{allow: private}, {allow: public}, {allow: private, provider: iam }, {allow: owner, provider: oidc }])'; +const multiAuthDirective = + '@auth(rules: [{allow: private}, {allow: public}, {allow: private, provider: iam }, {allow: owner, provider: oidc }])'; const ownerAuthDirective = '@auth(rules: [{allow: owner}])'; const ownerWithIAMAuthDirective = '@auth(rules: [{allow: owner, provider: iam }])'; const ownerRestrictedPublicAuthDirective = '@auth(rules: [{allow: owner},{allow: public, operations: [read]}])'; @@ -80,7 +78,7 @@ const publicAuthDirective = '@auth(rules: [{allow: public}])'; const publicUserPoolsAuthDirective = '@auth(rules: [{allow: public, provider: userPools}])'; const getSchema = (authDirective: string) => { - return ` + return ` type Post @model ${authDirective} { id: ID! title: String! @@ -90,7 +88,7 @@ const getSchema = (authDirective: string) => { }; const getSchemaWithFieldAuth = (authDirective: string) => { - return ` + return ` type Post @model { id: ID! title: String! @@ -101,7 +99,7 @@ const getSchemaWithFieldAuth = (authDirective: string) => { }; const getSchemaWithTypeAndFieldAuth = (typeAuthDirective: string, fieldAuthDirective: string) => { - return ` + return ` type Post @model ${typeAuthDirective} { id: ID! title: String! @@ -112,347 +110,331 @@ const getSchemaWithTypeAndFieldAuth = (typeAuthDirective: string, fieldAuthDirec }; const getTransformer = (authConfig: AppSyncAuthConfiguration) => - new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new ModelAuthTransformer({ authConfig }) - ] - }); + new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer(), new ModelAuthTransformer({ authConfig })], + }); const getObjectType = (doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined => { - return doc.definitions.find( - (def) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type - ) as ObjectTypeDefinitionNode | undefined; -} + return doc.definitions.find(def => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; +}; -const expectNone = (fieldOrType) => { - expect(fieldOrType.directives.length === 0); -} +const expectNone = fieldOrType => { + expect(fieldOrType.directives.length === 0); +}; const expectOne = (fieldOrType, directiveName) => { - expect(fieldOrType.directives.length === 1); - expect(fieldOrType.directives.find((d) => d.name.value === directiveName)).toBeDefined(); -} + expect(fieldOrType.directives.length === 1); + expect(fieldOrType.directives.find(d => d.name.value === directiveName)).toBeDefined(); +}; const expectTwo = (fieldOrType, directiveNames) => { - expect(directiveNames).toBeDefined(); - expect(directiveNames).toHaveLength(2); - expect(fieldOrType.directives.length === 2); - expect(fieldOrType.directives.find((d) => d.name.value === directiveNames[0])).toBeDefined(); - expect(fieldOrType.directives.find((d) => d.name.value === directiveNames[1])).toBeDefined(); -} + expect(directiveNames).toBeDefined(); + expect(directiveNames).toHaveLength(2); + expect(fieldOrType.directives.length === 2); + expect(fieldOrType.directives.find(d => d.name.value === directiveNames[0])).toBeDefined(); + expect(fieldOrType.directives.find(d => d.name.value === directiveNames[1])).toBeDefined(); +}; -const getField = (type, name) => type.fields.find((f) => f.name.value === name); +const getField = (type, name) => type.fields.find(f => f.name.value === name); describe('Validation tests', () => { - const validationTest = (authDirective, authConfig, expectedError) => { - const schema = getSchema(authDirective); - const transformer = getTransformer(authConfig); - - const t = () => { - const out = transformer.transform(schema); - }; + const validationTest = (authDirective, authConfig, expectedError) => { + const schema = getSchema(authDirective); + const transformer = getTransformer(authConfig); - expect(t).toThrowError(expectedError); + const t = () => { + const out = transformer.transform(schema); }; - test('AMAZON_COGNITO_USER_POOLS not configured for project', () => { - validationTest( - privateAuthDirective, - apiKeyDefaultConfig, - `@auth directive with 'userPools' provider found, but the project has no Cognito User \ -Pools authentication provider configured.` - ); - }); + expect(t).toThrowError(expectedError); + }; - test('API_KEY not configured for project', () => { - validationTest( - publicAuthDirective, - userPoolsDefaultConfig, - `@auth directive with 'apiKey' provider found, but the project has no API Key \ + test('AMAZON_COGNITO_USER_POOLS not configured for project', () => { + validationTest( + privateAuthDirective, + apiKeyDefaultConfig, + `@auth directive with 'userPools' provider found, but the project has no Cognito User \ +Pools authentication provider configured.` + ); + }); + + test('API_KEY not configured for project', () => { + validationTest( + publicAuthDirective, + userPoolsDefaultConfig, + `@auth directive with 'apiKey' provider found, but the project has no API Key \ authentication provider configured.` - ); - }); - - test('AWS_IAM not configured for project', () => { - validationTest( - publicIAMAuthDirective, - userPoolsDefaultConfig, - `@auth directive with 'iam' provider found, but the project has no IAM \ + ); + }); + + test('AWS_IAM not configured for project', () => { + validationTest( + publicIAMAuthDirective, + userPoolsDefaultConfig, + `@auth directive with 'iam' provider found, but the project has no IAM \ authentication provider configured.` - ); - }); - - test('OPENID_CONNECT not configured for project', () => { - validationTest( - ownerOpenIdAuthDirective, - userPoolsDefaultConfig, - `@auth directive with 'oidc' provider found, but the project has no OPENID_CONNECT \ + ); + }); + + test('OPENID_CONNECT not configured for project', () => { + validationTest( + ownerOpenIdAuthDirective, + userPoolsDefaultConfig, + `@auth directive with 'oidc' provider found, but the project has no OPENID_CONNECT \ authentication provider configured.` - ); - }); - - test(`'group' cannot have provider`, () => { - validationTest( - groupsWithProviderAuthDirective, - userPoolsDefaultConfig, - `@auth directive with 'groups' strategy only supports 'userPools' provider, but found \ + ); + }); + + test(`'group' cannot have provider`, () => { + validationTest( + groupsWithProviderAuthDirective, + userPoolsDefaultConfig, + `@auth directive with 'groups' strategy only supports 'userPools' provider, but found \ 'iam' assigned` - ); - }); - - test(`'owner' has invalid IAM provider`, () => { - validationTest( - ownerWithIAMAuthDirective, - userPoolsDefaultConfig, - `@auth directive with 'owner' strategy only supports 'userPools' (default) and \ + ); + }); + + test(`'owner' has invalid IAM provider`, () => { + validationTest( + ownerWithIAMAuthDirective, + userPoolsDefaultConfig, + `@auth directive with 'owner' strategy only supports 'userPools' (default) and \ 'oidc' providers, but found 'iam' assigned.` - ); - }); - - test(`'public' has invalid 'userPools' provider`, () => { - validationTest( - publicUserPoolsAuthDirective, - userPoolsDefaultConfig, - `@auth directive with 'public' strategy only supports 'apiKey' (default) and 'iam' providers, but \ + ); + }); + + test(`'public' has invalid 'userPools' provider`, () => { + validationTest( + publicUserPoolsAuthDirective, + userPoolsDefaultConfig, + `@auth directive with 'public' strategy only supports 'apiKey' (default) and 'iam' providers, but \ found 'userPools' assigned.` - ); - }); - - test(`'private' has invalid 'apiKey' provider`, () => { - validationTest( - privateWithApiKeyAuthDirective, - userPoolsDefaultConfig, - `@auth directive with 'private' strategy only supports 'userPools' (default) and 'iam' providers, but \ + ); + }); + + test(`'private' has invalid 'apiKey' provider`, () => { + validationTest( + privateWithApiKeyAuthDirective, + userPoolsDefaultConfig, + `@auth directive with 'private' strategy only supports 'userPools' (default) and 'iam' providers, but \ found 'apiKey' assigned.` - ); - }); + ); + }); }); describe('Type directive transformation tests', () => { - const transformTest = (authDirective, authConfig, expectedDirectiveNames?: string[] | undefined) => { - const schema = getSchema(authDirective); - const transformer = getTransformer(authConfig); + const transformTest = (authDirective, authConfig, expectedDirectiveNames?: string[] | undefined) => { + const schema = getSchema(authDirective); + const transformer = getTransformer(authConfig); - const out = transformer.transform(schema); + const out = transformer.transform(schema); - const schemaDoc = parse(out.schema); + const schemaDoc = parse(out.schema); - const postType = getObjectType(schemaDoc, 'Post'); + const postType = getObjectType(schemaDoc, 'Post'); - if (expectedDirectiveNames && expectedDirectiveNames.length > 0) { - let expectedDireciveNameCount = 0; + if (expectedDirectiveNames && expectedDirectiveNames.length > 0) { + let expectedDireciveNameCount = 0; - for (const expectedDirectiveName of expectedDirectiveNames) { - expect(postType.directives.find((d) => d.name.value === expectedDirectiveName)).toBeDefined(); - expectedDireciveNameCount++; - } + for (const expectedDirectiveName of expectedDirectiveNames) { + expect(postType.directives.find(d => d.name.value === expectedDirectiveName)).toBeDefined(); + expectedDireciveNameCount++; + } - expect(expectedDireciveNameCount).toEqual(postType.directives.length); - } + expect(expectedDireciveNameCount).toEqual(postType.directives.length); + } + }; + + test(`When provider is the same as default, then no directive added`, () => { + transformTest(ownerAuthDirective, userPoolsDefaultConfig); + }); + + // Disabling until troubleshooting the changes + // test(`When provider is not the same as default directive is added`, () => { + // transformTest( + // ownerAuthDirective, + // withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS']), + // [userPoolsDirectiveName, apiKeyDirectiveName] + // ); + // }); + + test(`When all providers are configured all of them are added`, () => { + const authConfig = withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS', 'AWS_IAM', 'OPENID_CONNECT']); + + authConfig.additionalAuthenticationProviders[2].openIDConnectConfig = { + name: 'Test Provider', + issuerUrl: 'https://abc.def/', }; - test(`When provider is the same as default, then no directive added`, () => { - transformTest( - ownerAuthDirective, - userPoolsDefaultConfig - ); - }); + transformTest(multiAuthDirective, authConfig, [userPoolsDirectiveName, iamDirectiveName, openIdDirectiveName, apiKeyDirectiveName]); + }); - // Disabling until troubleshooting the changes - // test(`When provider is not the same as default directive is added`, () => { - // transformTest( - // ownerAuthDirective, - // withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS']), - // [userPoolsDirectiveName, apiKeyDirectiveName] - // ); - // }); - - test(`When all providers are configured all of them are added`, () => { - const authConfig = withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS', 'AWS_IAM', 'OPENID_CONNECT']); - - authConfig.additionalAuthenticationProviders[2].openIDConnectConfig = { - name: 'Test Provider', - issuerUrl: 'https://abc.def/' - } - - transformTest( - multiAuthDirective, - authConfig, - [userPoolsDirectiveName, iamDirectiveName, openIdDirectiveName, apiKeyDirectiveName] - ); - }); + test(`Operation fields are getting the directive added, when type has the @auth for all operations`, () => { + const schema = getSchema(ownerAuthDirective); + const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); - test(`Operation fields are getting the directive added, when type has the @auth for all operations`, () => { - const schema = getSchema(ownerAuthDirective); - const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); - - const out = transformer.transform(schema); - const schemaDoc = parse(out.schema); - const queryType = getObjectType(schemaDoc, 'Query'); - const mutationType = getObjectType(schemaDoc, 'Mutation'); - - const fields = [ - ...queryType.fields, - ...mutationType.fields - ]; - - for (const field of fields) { - expect(field.directives.length === 1); - expect(field.directives.find((d) => d.name.value === userPoolsDirectiveName)).toBeDefined(); - } - - // Check that resolvers containing the authMode check block - const authModeCheckSnippet = '## [Start] Determine request authentication mode'; - - expect(out.resolvers['Query.getPost.res.vtl']).toContain(authModeCheckSnippet); - expect(out.resolvers['Query.listPosts.res.vtl']).toContain(authModeCheckSnippet); - expect(out.resolvers['Mutation.createPost.req.vtl']).toContain(authModeCheckSnippet); - expect(out.resolvers['Mutation.updatePost.req.vtl']).toContain(authModeCheckSnippet); - expect(out.resolvers['Mutation.deletePost.req.vtl']).toContain(authModeCheckSnippet); - }); + const out = transformer.transform(schema); + const schemaDoc = parse(out.schema); + const queryType = getObjectType(schemaDoc, 'Query'); + const mutationType = getObjectType(schemaDoc, 'Mutation'); - test(`Operation fields are getting the directive added, when type has the @auth only for allowed operations`, () => { - const schema = getSchema(ownerRestrictedPublicAuthDirective); - const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); + const fields = [...queryType.fields, ...mutationType.fields]; - const out = transformer.transform(schema); - const schemaDoc = parse(out.schema); - const queryType = getObjectType(schemaDoc, 'Query'); - const mutationType = getObjectType(schemaDoc, 'Mutation'); + for (const field of fields) { + expect(field.directives.length === 1); + expect(field.directives.find(d => d.name.value === userPoolsDirectiveName)).toBeDefined(); + } - expect (expectTwo(getField(queryType, 'getPost'), ['aws_cognito_user_pools', 'aws_api_key'])); - expect (expectTwo(getField(queryType, 'listPosts'), ['aws_cognito_user_pools', 'aws_api_key'])); + // Check that resolvers containing the authMode check block + const authModeCheckSnippet = '## [Start] Determine request authentication mode'; - expect (expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); - }); + expect(out.resolvers['Query.getPost.res.vtl']).toContain(authModeCheckSnippet); + expect(out.resolvers['Query.listPosts.res.vtl']).toContain(authModeCheckSnippet); + expect(out.resolvers['Mutation.createPost.req.vtl']).toContain(authModeCheckSnippet); + expect(out.resolvers['Mutation.updatePost.req.vtl']).toContain(authModeCheckSnippet); + expect(out.resolvers['Mutation.deletePost.req.vtl']).toContain(authModeCheckSnippet); + }); - test(`'public' with IAM provider adds policy for Unauth role`, () => { - const schema = getSchema(publicIAMAuthDirective); - const transformer = getTransformer(withAuthModes(userPoolsDefaultConfig, ['AWS_IAM'])); + test(`Operation fields are getting the directive added, when type has the @auth only for allowed operations`, () => { + const schema = getSchema(ownerRestrictedPublicAuthDirective); + const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); - const out = transformer.transform(schema); + const out = transformer.transform(schema); + const schemaDoc = parse(out.schema); + const queryType = getObjectType(schemaDoc, 'Query'); + const mutationType = getObjectType(schemaDoc, 'Mutation'); - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.UnauthRolePolicy] - ).toBeDefined(); - }); + expect(expectTwo(getField(queryType, 'getPost'), ['aws_cognito_user_pools', 'aws_api_key'])); + expect(expectTwo(getField(queryType, 'listPosts'), ['aws_cognito_user_pools', 'aws_api_key'])); - test(`Field level @auth is propagated to type and the type related operations`, () => { - const schema = getSchemaWithFieldAuth(ownerRestrictedPublicAuthDirective); - const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); + expect(expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); + }); - const out = transformer.transform(schema); - const schemaDoc = parse(out.schema); - const queryType = getObjectType(schemaDoc, 'Query'); - const mutationType = getObjectType(schemaDoc, 'Mutation'); + test(`'public' with IAM provider adds policy for Unauth role`, () => { + const schema = getSchema(publicIAMAuthDirective); + const transformer = getTransformer(withAuthModes(userPoolsDefaultConfig, ['AWS_IAM'])); - expect (expectTwo(getField(queryType, 'getPost'), ['aws_cognito_user_pools', 'aws_api_key'])); - expect (expectTwo(getField(queryType, 'listPosts'), ['aws_cognito_user_pools', 'aws_api_key'])); + const out = transformer.transform(schema); - expect (expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.UnauthRolePolicy]).toBeDefined(); + }); - const postType = getObjectType(schemaDoc, 'Post'); - expect (expectTwo(getField(postType, 'protected'), ['aws_cognito_user_pools', 'aws_api_key'])); + test(`Field level @auth is propagated to type and the type related operations`, () => { + const schema = getSchemaWithFieldAuth(ownerRestrictedPublicAuthDirective); + const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); - // Check that resolvers containing the authMode check block - const authModeCheckSnippet = '## [Start] Determine request authentication mode'; + const out = transformer.transform(schema); + const schemaDoc = parse(out.schema); + const queryType = getObjectType(schemaDoc, 'Query'); + const mutationType = getObjectType(schemaDoc, 'Mutation'); - expect(out.resolvers['Post.protected.req.vtl']).toContain(authModeCheckSnippet); - }); + expect(expectTwo(getField(queryType, 'getPost'), ['aws_cognito_user_pools', 'aws_api_key'])); + expect(expectTwo(getField(queryType, 'listPosts'), ['aws_cognito_user_pools', 'aws_api_key'])); - test(`'groups' @auth at field level is propagated to type and the type related operations`, () => { - const schema = getSchemaWithFieldAuth(groupsAuthDirective); - const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); + expect(expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); - const out = transformer.transform(schema); - const schemaDoc = parse(out.schema); - const queryType = getObjectType(schemaDoc, 'Query'); - const mutationType = getObjectType(schemaDoc, 'Mutation'); + const postType = getObjectType(schemaDoc, 'Post'); + expect(expectTwo(getField(postType, 'protected'), ['aws_cognito_user_pools', 'aws_api_key'])); - expect (expectOne(getField(queryType, 'getPost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(queryType, 'listPosts'), 'aws_cognito_user_pools')); + // Check that resolvers containing the authMode check block + const authModeCheckSnippet = '## [Start] Determine request authentication mode'; - expect (expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); + expect(out.resolvers['Post.protected.req.vtl']).toContain(authModeCheckSnippet); + }); - const postType = getObjectType(schemaDoc, 'Post'); - expect (expectOne(getField(postType, 'protected'), 'aws_cognito_user_pools')); + test(`'groups' @auth at field level is propagated to type and the type related operations`, () => { + const schema = getSchemaWithFieldAuth(groupsAuthDirective); + const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); - // Check that resolvers containing the authMode check block - const authModeCheckSnippet = '## [Start] Determine request authentication mode'; + const out = transformer.transform(schema); + const schemaDoc = parse(out.schema); + const queryType = getObjectType(schemaDoc, 'Query'); + const mutationType = getObjectType(schemaDoc, 'Mutation'); - expect(out.resolvers['Post.protected.req.vtl']).toContain(authModeCheckSnippet); - }); + expect(expectOne(getField(queryType, 'getPost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(queryType, 'listPosts'), 'aws_cognito_user_pools')); - test(`'groups' @auth at field level is propagated to type and the type related operations, also default provider for read`, () => { - const schema = getSchemaWithTypeAndFieldAuth(groupsAuthDirective, groupsWithApiKeyAuthDirective); - const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); + expect(expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); - const out = transformer.transform(schema); - const schemaDoc = parse(out.schema); - const queryType = getObjectType(schemaDoc, 'Query'); - const mutationType = getObjectType(schemaDoc, 'Mutation'); + const postType = getObjectType(schemaDoc, 'Post'); + expect(expectOne(getField(postType, 'protected'), 'aws_cognito_user_pools')); - expect (expectTwo(getField(queryType, 'getPost'), ['aws_cognito_user_pools', 'aws_api_key'])); - expect (expectTwo(getField(queryType, 'listPosts'), ['aws_cognito_user_pools', 'aws_api_key'])); + // Check that resolvers containing the authMode check block + const authModeCheckSnippet = '## [Start] Determine request authentication mode'; - expect (expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); - expect (expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); + expect(out.resolvers['Post.protected.req.vtl']).toContain(authModeCheckSnippet); + }); - const postType = getObjectType(schemaDoc, 'Post'); - expect (expectTwo(getField(postType, 'protected'), ['aws_cognito_user_pools', 'aws_api_key'])); + test(`'groups' @auth at field level is propagated to type and the type related operations, also default provider for read`, () => { + const schema = getSchemaWithTypeAndFieldAuth(groupsAuthDirective, groupsWithApiKeyAuthDirective); + const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); - // Check that resolvers containing the authMode check block - const authModeCheckSnippet = '## [Start] Determine request authentication mode'; + const out = transformer.transform(schema); + const schemaDoc = parse(out.schema); + const queryType = getObjectType(schemaDoc, 'Query'); + const mutationType = getObjectType(schemaDoc, 'Mutation'); - expect(out.resolvers['Post.protected.req.vtl']).toContain(authModeCheckSnippet); - }); + expect(expectTwo(getField(queryType, 'getPost'), ['aws_cognito_user_pools', 'aws_api_key'])); + expect(expectTwo(getField(queryType, 'listPosts'), ['aws_cognito_user_pools', 'aws_api_key'])); + + expect(expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); + expect(expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); + + const postType = getObjectType(schemaDoc, 'Post'); + expect(expectTwo(getField(postType, 'protected'), ['aws_cognito_user_pools', 'aws_api_key'])); + + // Check that resolvers containing the authMode check block + const authModeCheckSnippet = '## [Start] Determine request authentication mode'; + + expect(out.resolvers['Post.protected.req.vtl']).toContain(authModeCheckSnippet); + }); + + // Disabling until troubleshooting the changes + // test(`Connected type is also getting the directives added, when a field has @connection`, () => { + // const schema = ` + // type Post @model @auth(rules:[{allow: private}]){ + // id: ID! + // title: String! + // comments: [Comment] @connection(name: "PostComments") + // } + + // type Comment @model { + // id: ID! + // content: String! + // post: Post @connection(name: "PostComments") + // } + // `; + + // const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); + // const out = transformer.transform(schema); + // const schemaDoc = parse(out.schema); + // const queryType = getObjectType(schemaDoc, 'Query'); + // const mutationType = getObjectType(schemaDoc, 'Mutation'); + + // expect (expectOne(getField(queryType, 'getPost'), 'aws_cognito_user_pools')); + // expect (expectOne(getField(queryType, 'listPosts'), 'aws_cognito_user_pools')); + + // expect (expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); + // expect (expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); + // expect (expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); + + // const postType = getObjectType(schemaDoc, 'Post'); + // expect (expectTwo(postType, ['aws_api_key', 'aws_cognito_user_pools'])); + // expect (expectNone(getField(postType, 'comments'))); + + // const commentType = getObjectType(schemaDoc, 'Comment'); + // expect (expectOne(getField(commentType, 'post'), 'aws_cognito_user_pools')); - // Disabling until troubleshooting the changes - // test(`Connected type is also getting the directives added, when a field has @connection`, () => { - // const schema = ` - // type Post @model @auth(rules:[{allow: private}]){ - // id: ID! - // title: String! - // comments: [Comment] @connection(name: "PostComments") - // } - - // type Comment @model { - // id: ID! - // content: String! - // post: Post @connection(name: "PostComments") - // } - // `; - - // const transformer = getTransformer(withAuthModes(apiKeyDefaultConfig, ['AMAZON_COGNITO_USER_POOLS'])); - // const out = transformer.transform(schema); - // const schemaDoc = parse(out.schema); - // const queryType = getObjectType(schemaDoc, 'Query'); - // const mutationType = getObjectType(schemaDoc, 'Mutation'); - - // expect (expectOne(getField(queryType, 'getPost'), 'aws_cognito_user_pools')); - // expect (expectOne(getField(queryType, 'listPosts'), 'aws_cognito_user_pools')); - - // expect (expectOne(getField(mutationType, 'createPost'), 'aws_cognito_user_pools')); - // expect (expectOne(getField(mutationType, 'updatePost'), 'aws_cognito_user_pools')); - // expect (expectOne(getField(mutationType, 'deletePost'), 'aws_cognito_user_pools')); - - // const postType = getObjectType(schemaDoc, 'Post'); - // expect (expectTwo(postType, ['aws_api_key', 'aws_cognito_user_pools'])); - // expect (expectNone(getField(postType, 'comments'))); - - // const commentType = getObjectType(schemaDoc, 'Comment'); - // expect (expectOne(getField(commentType, 'post'), 'aws_cognito_user_pools')); - - // const modelPostConnectionType = getObjectType(schemaDoc, 'ModelPostConnection'); - // expect (expectOne(modelPostConnectionType, 'aws_cognito_user_pools')); - // }); + // const modelPostConnectionType = getObjectType(schemaDoc, 'ModelPostConnection'); + // expect (expectOne(modelPostConnectionType, 'aws_cognito_user_pools')); + // }); }); diff --git a/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts b/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts index 4f9431ce00..9f72fc5acc 100644 --- a/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/OperationsArgument.test.ts @@ -1,140 +1,119 @@ -import GraphQLTransform from 'graphql-transformer-core' -import { ResourceConstants } from 'graphql-transformer-common' -import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer' -import { ModelAuthTransformer } from '../ModelAuthTransformer' +import GraphQLTransform from 'graphql-transformer-core'; +import { ResourceConstants } from 'graphql-transformer-common'; +import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; +import { ModelAuthTransformer } from '../ModelAuthTransformer'; test('Test "read" auth operation', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [{allow: groups, groups: ["Admin", "Dev"], operations: [read]}]) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') - expect( - out.resolvers['Query.getPost.res.vtl'] - ).toContain('Authorization rule:') - expect( - out.resolvers['Query.listPosts.res.vtl'] - ).toContain('Authorization rule:') + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); + expect(out.resolvers['Query.getPost.res.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Query.listPosts.res.vtl']).toContain('Authorization rule:'); }); test('Test "create", "update", "delete" auth operations', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [{allow: groups, groups: ["Admin", "Dev"], operations: [create, update, delete]}]) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') - expect( - out.resolvers['Query.getPost.res.vtl'] - ).not.toContain('Authorization rule:') - expect(out.resolvers['Query.getPost.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Query.listPosts.res.vtl'] - ).not.toContain('Authorization rule:') - expect(out.resolvers['Query.listPosts.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.createPost.req.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Mutation.createPost.req.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.updatePost.req.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Mutation.updatePost.req.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.deletePost.req.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Mutation.deletePost.req.vtl']).toMatchSnapshot(); + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); + expect(out.resolvers['Query.getPost.res.vtl']).not.toContain('Authorization rule:'); + expect(out.resolvers['Query.getPost.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Query.listPosts.res.vtl']).not.toContain('Authorization rule:'); + expect(out.resolvers['Query.listPosts.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.createPost.req.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Mutation.createPost.req.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.updatePost.req.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Mutation.updatePost.req.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.deletePost.req.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Mutation.deletePost.req.vtl']).toMatchSnapshot(); }); test('Test that operation overwrites queries in auth operations', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [{allow: groups, groups: ["Admin", "Dev"], queries: [get, list], operations: [create, update, delete]}]) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') - expect( - out.resolvers['Query.getPost.res.vtl'] - ).not.toContain('Authorization rule:') - expect(out.resolvers['Query.getPost.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Query.listPosts.res.vtl'] - ).not.toContain('Authorization rule:') - expect(out.resolvers['Query.listPosts.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.createPost.req.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Mutation.createPost.req.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.updatePost.req.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Mutation.updatePost.req.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.deletePost.req.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Mutation.deletePost.req.vtl']).toMatchSnapshot(); + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); + expect(out.resolvers['Query.getPost.res.vtl']).not.toContain('Authorization rule:'); + expect(out.resolvers['Query.getPost.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Query.listPosts.res.vtl']).not.toContain('Authorization rule:'); + expect(out.resolvers['Query.listPosts.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.createPost.req.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Mutation.createPost.req.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.updatePost.req.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Mutation.updatePost.req.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.deletePost.req.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Mutation.deletePost.req.vtl']).toMatchSnapshot(); }); test('Test that checks subscription resolvers are generated with auth logic', () => { - const validSchema = ` + const validSchema = ` type Salary @model @auth( rules: [ {allow: owner}, @@ -143,41 +122,36 @@ test('Test that checks subscription resolvers are generated with auth logic', () wage: Int owner: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - // expect to generate subcriptions resolvers - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') - expect( - out.resolvers['Subscription.onCreateSalary.res.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Subscription.onDeleteSalary.res.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Subscription.onDeleteSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Subscription.onUpdateSalary.res.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toMatchSnapshot(); + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + // expect to generate subcriptions resolvers + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); + expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Subscription.onDeleteSalary.res.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onDeleteSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toMatchSnapshot(); }); test('Test that checks subscription resolvers are created without auth logic', () => { - const validSchema = ` + const validSchema = ` type Salary @model( subscriptions: { level: public @@ -188,41 +162,36 @@ test('Test that checks subscription resolvers are created without auth logic', ( wage: Int owner: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - // expect to generate subcriptions resolvers - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') - expect( - out.resolvers['Subscription.onCreateSalary.res.vtl'] - ).not.toContain('Authorization rule:') - expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Subscription.onDeleteSalary.res.vtl'] - ).not.toContain('Authorization rule:') - expect(out.resolvers['Subscription.onDeleteSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Subscription.onUpdateSalary.res.vtl'] - ).not.toContain('Authorization rule:') - expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toMatchSnapshot(); + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + // expect to generate subcriptions resolvers + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); + expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).not.toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Subscription.onDeleteSalary.res.vtl']).not.toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onDeleteSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).not.toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toMatchSnapshot(); }); test('Test that subscriptions are only generated if the respective mutation operation exists', () => { - const validSchema = ` + const validSchema = ` type Salary @model( mutations: { create: "makeIT", @@ -236,35 +205,29 @@ test('Test that subscriptions are only generated if the respective mutation oper wage: Int owner: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - } - }) - ] - }) - const out = transformer.transform(validSchema) - // expect to generate subscription resolvers for create and update only - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') - expect( - out.resolvers['Subscription.onCreateSalary.res.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Subscription.onUpdateSalary.res.vtl'] - ).toContain('Authorization rule:') - expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Subscription.onDeleteSalary.res.vtl'] - ).toBeUndefined() -}) + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + // expect to generate subscription resolvers for create and update only + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); + expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onCreateSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toContain('Authorization rule:'); + expect(out.resolvers['Subscription.onUpdateSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Subscription.onDeleteSalary.res.vtl']).toBeUndefined(); +}); diff --git a/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts b/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts index f4e21bf1a9..37689ea256 100644 --- a/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/OwnerAuthTransformer.test.ts @@ -1,43 +1,48 @@ import { - ObjectTypeDefinitionNode, parse, FieldDefinitionNode, DocumentNode, - DefinitionNode, Kind, InputObjectTypeDefinitionNode -} from 'graphql' -import GraphQLTransform from 'graphql-transformer-core' -import { ResourceConstants } from 'graphql-transformer-common' -import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer' -import { ModelAuthTransformer } from '../ModelAuthTransformer' + ObjectTypeDefinitionNode, + parse, + FieldDefinitionNode, + DocumentNode, + DefinitionNode, + Kind, + InputObjectTypeDefinitionNode, +} from 'graphql'; +import GraphQLTransform from 'graphql-transformer-core'; +import { ResourceConstants } from 'graphql-transformer-common'; +import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; +import { ModelAuthTransformer } from '../ModelAuthTransformer'; test('Test ModelAuthTransformer validation happy case', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [{allow: owner}]) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); }); - test('Test OwnerField with Subscriptions', () => { - const validSchema = ` + const validSchema = ` type Post @model @auth(rules: [ {allow: owner, ownerField: "postOwner"} @@ -46,41 +51,36 @@ test('Test OwnerField with Subscriptions', () => { id: ID! title: String postOwner: String - }` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() + }`; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); - // expect 'postOwner' as an argument for subscription operations - expect(out.schema).toContain( - 'onCreatePost(postOwner: String!)' - ) - expect(out.schema).toContain( - 'onUpdatePost(postOwner: String!)' - ) - expect(out.schema).toContain( - 'onDeletePost(postOwner: String!)' - ) + // expect 'postOwner' as an argument for subscription operations + expect(out.schema).toContain('onCreatePost(postOwner: String!)'); + expect(out.schema).toContain('onUpdatePost(postOwner: String!)'); + expect(out.schema).toContain('onDeletePost(postOwner: String!)'); - // expect logic in the resolvers to check for postOwner args as an allowerOwner - expect( - out.resolvers['Subscription.onCreatePost.res.vtl'] - ).toContain('#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.postOwner, null) )') - expect( - out.resolvers['Subscription.onUpdatePost.res.vtl'] - ).toContain('#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.postOwner, null) )') - expect( - out.resolvers['Subscription.onDeletePost.res.vtl'] - ).toContain('#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.postOwner, null) )') -}) + // expect logic in the resolvers to check for postOwner args as an allowerOwner + expect(out.resolvers['Subscription.onCreatePost.res.vtl']).toContain( + '#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.postOwner, null) )' + ); + expect(out.resolvers['Subscription.onUpdatePost.res.vtl']).toContain( + '#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.postOwner, null) )' + ); + expect(out.resolvers['Subscription.onDeletePost.res.vtl']).toContain( + '#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.postOwner, null) )' + ); +}); diff --git a/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts b/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts index d7c322452b..c42ed6a60d 100644 --- a/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/PerFieldAuthArgument.test.ts @@ -1,10 +1,10 @@ -import GraphQLTransform from 'graphql-transformer-core' -import { ResourceConstants } from 'graphql-transformer-common' -import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer' -import { ModelAuthTransformer } from '../ModelAuthTransformer' +import GraphQLTransform from 'graphql-transformer-core'; +import { ResourceConstants } from 'graphql-transformer-common'; +import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; +import { ModelAuthTransformer } from '../ModelAuthTransformer'; test('Test that subscriptions are only generated if the respective mutation operation exists', () => { - const validSchema = ` + const validSchema = ` type Salary @model @auth(rules: [ @@ -17,43 +17,35 @@ test('Test that subscriptions are only generated if the respective mutation oper owner: String secret: String @auth(rules: [{allow: owner}]) } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - } - }) - ] - }) - const out = transformer.transform(validSchema) - // expect to generate subscription resolvers for create and update only - expect(out).toBeDefined() - expect( - out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType - ).toEqual('AMAZON_COGNITO_USER_POOLS') - expect( - out.resolvers['Salary.secret.res.vtl'] - ).toContain('#if( $operation == "Mutation" )') - expect(out.resolvers['Salary.secret.res.vtl']).toMatchSnapshot(); + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + // expect to generate subscription resolvers for create and update only + expect(out).toBeDefined(); + expect(out.rootStack.Resources[ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual( + 'AMAZON_COGNITO_USER_POOLS' + ); + expect(out.resolvers['Salary.secret.res.vtl']).toContain('#if( $operation == "Mutation" )'); + expect(out.resolvers['Salary.secret.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.createSalary.res.vtl'] - ).toContain('#set( $context.result.operation = "Mutation" )') - expect(out.resolvers['Mutation.createSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.createSalary.res.vtl']).toContain('#set( $context.result.operation = "Mutation" )'); + expect(out.resolvers['Mutation.createSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.updateSalary.res.vtl'] - ).toContain('#set( $context.result.operation = "Mutation" )') - expect(out.resolvers['Mutation.updateSalary.res.vtl']).toMatchSnapshot(); + expect(out.resolvers['Mutation.updateSalary.res.vtl']).toContain('#set( $context.result.operation = "Mutation" )'); + expect(out.resolvers['Mutation.updateSalary.res.vtl']).toMatchSnapshot(); - expect( - out.resolvers['Mutation.deleteSalary.res.vtl'] - ).toContain('#set( $context.result.operation = "Mutation" )') - expect(out.resolvers['Mutation.deleteSalary.res.vtl']).toMatchSnapshot(); -}) + expect(out.resolvers['Mutation.deleteSalary.res.vtl']).toContain('#set( $context.result.operation = "Mutation" )'); + expect(out.resolvers['Mutation.deleteSalary.res.vtl']).toMatchSnapshot(); +}); diff --git a/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts b/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts index 12df77c11c..541ccf7f71 100644 --- a/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts +++ b/packages/graphql-auth-transformer/src/__tests__/SearchableAuthTransformer.test.ts @@ -1,10 +1,10 @@ -import GraphQLTransform from 'graphql-transformer-core' -import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer' -import { ModelAuthTransformer } from '../ModelAuthTransformer' -import { SearchableModelTransformer } from 'graphql-elasticsearch-transformer' +import GraphQLTransform from 'graphql-transformer-core'; +import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer'; +import { ModelAuthTransformer } from '../ModelAuthTransformer'; +import { SearchableModelTransformer } from 'graphql-elasticsearch-transformer'; test('test auth logic is enabled on owner/static rules in resposne es resolver', () => { - const validSchema = ` + const validSchema = ` type Comment @model @searchable @auth(rules: [ @@ -15,37 +15,36 @@ test('test auth logic is enabled on owner/static rules in resposne es resolver', id: ID! content: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new SearchableModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - } - }) - ] - }) + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new SearchableModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); - const out = transformer.transform(validSchema) - // expect response resolver to contain auth logic for owner rule - expect(out).toBeDefined() - expect( - out.resolvers['Query.searchComments.res.vtl'] - ).toContain('## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **') - // expect response resolver to contain auth logic for group rule - expect( - out.resolvers['Query.searchComments.res.vtl'] - ).toContain('## Authorization rule: { allow: groups, groups: ["writer"], groupClaim: "cognito:groups" } **') - -}) + const out = transformer.transform(validSchema); + // expect response resolver to contain auth logic for owner rule + expect(out).toBeDefined(); + expect(out.resolvers['Query.searchComments.res.vtl']).toContain( + '## Authorization rule: { allow: owner, ownerField: "owner", identityClaim: "cognito:username" } **' + ); + // expect response resolver to contain auth logic for group rule + expect(out.resolvers['Query.searchComments.res.vtl']).toContain( + '## Authorization rule: { allow: groups, groups: ["writer"], groupClaim: "cognito:groups" } **' + ); +}); test('test auth logic is enabled for iam/apiKey auth rules in response es resolver', () => { - const validSchema = ` + const validSchema = ` type Post @model @searchable @auth(rules: [ @@ -56,39 +55,35 @@ test('test auth logic is enabled for iam/apiKey auth rules in response es resolv content: String secret: String @auth(rules: [{ allow: private, provider: iam }]) # only auth role can do crud on this } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new SearchableModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [ - { - authenticationType: 'API_KEY', - apiKeyConfig: { - description: 'E2E Test API Key', - apiKeyExpirationDays: 300 - } - }, - { - authenticationType: 'AWS_IAM' - }, - ] - } - }) - ] - }) + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new SearchableModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [ + { + authenticationType: 'API_KEY', + apiKeyConfig: { + description: 'E2E Test API Key', + apiKeyExpirationDays: 300, + }, + }, + { + authenticationType: 'AWS_IAM', + }, + ], + }, + }), + ], + }); - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect( - out.schema - ).toContain('SearchablePostConnection @aws_api_key @aws_iam') - expect( - out.schema - ).toMatchSnapshot() -}) \ No newline at end of file + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.schema).toContain('SearchablePostConnection @aws_api_key @aws_iam'); + expect(out.schema).toMatchSnapshot(); +}); diff --git a/packages/graphql-auth-transformer/src/constants.ts b/packages/graphql-auth-transformer/src/constants.ts index 1024b55c01..c6b2788872 100644 --- a/packages/graphql-auth-transformer/src/constants.ts +++ b/packages/graphql-auth-transformer/src/constants.ts @@ -1,9 +1,9 @@ -export const OWNER_AUTH_STRATEGY = "owner" -export const DEFAULT_OWNER_FIELD = "owner" -export const DEFAULT_IDENTITY_FIELD = "username" -export const GROUPS_AUTH_STRATEGY = "groups" -export const DEFAULT_GROUPS_FIELD = "groups" -export const DEFAULT_GROUP_CLAIM = "cognito:groups" -export const ON_CREATE_FIELD = "onCreate" -export const ON_UPDATE_FIELD = "onUpdate" -export const ON_DELETE_FIELD = "onDelete" +export const OWNER_AUTH_STRATEGY = 'owner'; +export const DEFAULT_OWNER_FIELD = 'owner'; +export const DEFAULT_IDENTITY_FIELD = 'username'; +export const GROUPS_AUTH_STRATEGY = 'groups'; +export const DEFAULT_GROUPS_FIELD = 'groups'; +export const DEFAULT_GROUP_CLAIM = 'cognito:groups'; +export const ON_CREATE_FIELD = 'onCreate'; +export const ON_UPDATE_FIELD = 'onUpdate'; +export const ON_DELETE_FIELD = 'onDelete'; diff --git a/packages/graphql-auth-transformer/src/graphQlApi.ts b/packages/graphql-auth-transformer/src/graphQlApi.ts index 0214e6eec0..31f405e1b5 100644 --- a/packages/graphql-auth-transformer/src/graphQlApi.ts +++ b/packages/graphql-auth-transformer/src/graphQlApi.ts @@ -10,81 +10,81 @@ * us-west-2 (https://d201a2mn26r7lk.cloudfront.net/latest/gzip/CloudFormationResourceSpecification.json), version 4.0.0 */ -import {ResourceBase, ResourceTag, Value, List} from 'cloudform-types' +import { ResourceBase, ResourceTag, Value, List } from 'cloudform-types'; -export type Tags = List +export type Tags = List; export class UserPoolConfig { - AppIdClientRegex?: Value - UserPoolId?: Value - AwsRegion?: Value - DefaultAction?: Value - - constructor(properties: UserPoolConfig) { - Object.assign(this, properties) - } + AppIdClientRegex?: Value; + UserPoolId?: Value; + AwsRegion?: Value; + DefaultAction?: Value; + + constructor(properties: UserPoolConfig) { + Object.assign(this, properties); + } } export class OpenIDConnectConfig { - Issuer?: Value - ClientId?: Value - AuthTTL?: Value - IatTTL?: Value - - constructor(properties: OpenIDConnectConfig) { - Object.assign(this, properties) - } + Issuer?: Value; + ClientId?: Value; + AuthTTL?: Value; + IatTTL?: Value; + + constructor(properties: OpenIDConnectConfig) { + Object.assign(this, properties); + } } export class LogConfig { - CloudWatchLogsRoleArn?: Value - FieldLogLevel?: Value + CloudWatchLogsRoleArn?: Value; + FieldLogLevel?: Value; - constructor(properties: LogConfig) { - Object.assign(this, properties) - } + constructor(properties: LogConfig) { + Object.assign(this, properties); + } } export class CognitoUserPoolConfig { - AppIdClientRegex?: Value - UserPoolId?: Value - AwsRegion?: Value + AppIdClientRegex?: Value; + UserPoolId?: Value; + AwsRegion?: Value; - constructor(properties: CognitoUserPoolConfig) { - Object.assign(this, properties) - } + constructor(properties: CognitoUserPoolConfig) { + Object.assign(this, properties); + } } -export type AdditionalAuthenticationProviders = List +export type AdditionalAuthenticationProviders = List; export class AdditionalAuthenticationProvider { - OpenIDConnectConfig?: OpenIDConnectConfig - UserPoolConfig?: CognitoUserPoolConfig - AuthenticationType!: Value + OpenIDConnectConfig?: OpenIDConnectConfig; + UserPoolConfig?: CognitoUserPoolConfig; + AuthenticationType!: Value; - constructor(properties: AdditionalAuthenticationProvider) { - Object.assign(this, properties) - } + constructor(properties: AdditionalAuthenticationProvider) { + Object.assign(this, properties); + } } export interface GraphQLApiProperties { - OpenIDConnectConfig?: OpenIDConnectConfig - UserPoolConfig?: UserPoolConfig - Tags?: Tags - Name: Value - AuthenticationType: Value - LogConfig?: LogConfig - AdditionalAuthenticationProviders?: AdditionalAuthenticationProviders + OpenIDConnectConfig?: OpenIDConnectConfig; + UserPoolConfig?: UserPoolConfig; + Tags?: Tags; + Name: Value; + AuthenticationType: Value; + LogConfig?: LogConfig; + AdditionalAuthenticationProviders?: AdditionalAuthenticationProviders; } export default class GraphQLApi extends ResourceBase { - static UserPoolConfig = UserPoolConfig - static OpenIDConnectConfig = OpenIDConnectConfig - static LogConfig = LogConfig - static CognitoUserPoolConfig = CognitoUserPoolConfig - static AdditionalAuthenticationProvider = AdditionalAuthenticationProvider - - constructor(properties?: GraphQLApiProperties) { - super('AWS::AppSync::GraphQLApi', properties) - } + static UserPoolConfig = UserPoolConfig; + static OpenIDConnectConfig = OpenIDConnectConfig; + static LogConfig = LogConfig; + static CognitoUserPoolConfig = CognitoUserPoolConfig; + static AdditionalAuthenticationProvider = AdditionalAuthenticationProvider; + + constructor(properties?: GraphQLApiProperties) { + super('AWS::AppSync::GraphQLApi', properties); + } } diff --git a/packages/graphql-auth-transformer/src/index.ts b/packages/graphql-auth-transformer/src/index.ts index 17297f441f..57647eea12 100644 --- a/packages/graphql-auth-transformer/src/index.ts +++ b/packages/graphql-auth-transformer/src/index.ts @@ -1,3 +1,3 @@ -import { ModelAuthTransformer } from './ModelAuthTransformer' -export * from './ModelAuthTransformer' -export default ModelAuthTransformer \ No newline at end of file +import { ModelAuthTransformer } from './ModelAuthTransformer'; +export * from './ModelAuthTransformer'; +export default ModelAuthTransformer; diff --git a/packages/graphql-auth-transformer/src/resources.ts b/packages/graphql-auth-transformer/src/resources.ts index 66c9aa340c..55e9dbca49 100644 --- a/packages/graphql-auth-transformer/src/resources.ts +++ b/packages/graphql-auth-transformer/src/resources.ts @@ -1,1102 +1,1047 @@ -import Template from 'cloudform-types/types/template' +import Template from 'cloudform-types/types/template'; import Policy from 'cloudform-types/types/iam/policy'; -import { AppSync, Fn, StringParameter, Refs, NumberParameter, IAM, Value } from 'cloudform-types' -import { AuthRule, AuthProvider } from './AuthRule' +import { AppSync, Fn, StringParameter, Refs, NumberParameter, IAM, Value } from 'cloudform-types'; +import { AuthRule, AuthProvider } from './AuthRule'; import { - str, ref, obj, set, iff, list, raw, - forEach, compoundExpression, qref, equals, comment, - or, Expression, and, not, parens, toJson, - block, print, ifElse, newline -} from 'graphql-mapping-template' -import { ResourceConstants, NONE_VALUE } from 'graphql-transformer-common' -import GraphQLAPI, { UserPoolConfig, GraphQLApiProperties, OpenIDConnectConfig, AdditionalAuthenticationProvider } from './graphQlApi' -import * as Transformer from './ModelAuthTransformer' + str, + ref, + obj, + set, + iff, + list, + raw, + forEach, + compoundExpression, + qref, + equals, + comment, + or, + Expression, + and, + not, + parens, + toJson, + block, + print, + ifElse, + newline, +} from 'graphql-mapping-template'; +import { ResourceConstants, NONE_VALUE } from 'graphql-transformer-common'; +import GraphQLAPI, { UserPoolConfig, GraphQLApiProperties, OpenIDConnectConfig, AdditionalAuthenticationProvider } from './graphQlApi'; +import * as Transformer from './ModelAuthTransformer'; import { FieldDefinitionNode } from 'graphql'; -import { - DEFAULT_OWNER_FIELD, - DEFAULT_IDENTITY_FIELD, - DEFAULT_GROUPS_FIELD, - DEFAULT_GROUP_CLAIM -} from './constants' +import { DEFAULT_OWNER_FIELD, DEFAULT_IDENTITY_FIELD, DEFAULT_GROUPS_FIELD, DEFAULT_GROUP_CLAIM } from './constants'; function replaceIfUsername(identityClaim: string): string { - return (identityClaim === 'username') ? 'cognito:username' : identityClaim; + return identityClaim === 'username' ? 'cognito:username' : identityClaim; } function isUsername(identityClaim: string): boolean { - return identityClaim === 'username' + return identityClaim === 'username'; } export class ResourceFactory { - - public makeParams() { - return { - [ResourceConstants.PARAMETERS.AppSyncApiName]: new StringParameter({ - Description: 'The name of the AppSync API', - Default: 'AppSyncSimpleTransform' - }), - [ResourceConstants.PARAMETERS.APIKeyExpirationEpoch]: new NumberParameter({ - Description: 'The epoch time in seconds when the API Key should expire.' + - ' Setting this to 0 will default to 7 days from the deployment date.' + - ' Setting this to -1 will not create an API Key.', - Default: 0, - MinValue: -1 - }), - [ResourceConstants.PARAMETERS.CreateAPIKey]: new NumberParameter({ - Description: 'The boolean value to control if an API Key will be created or not.' + - ' The value of the property is automatically set by the CLI.' + - ' If the value is set to 0 no API Key will be created.', - Default: 0, - MinValue: 0, - MaxValue: 1 - }), - [ResourceConstants.PARAMETERS.AuthCognitoUserPoolId]: new StringParameter({ - Description: 'The id of an existing User Pool to connect. If this is changed, a user pool will not be created for you.', - Default: ResourceConstants.NONE - }) - } - } - - /** - * Creates the barebones template for an application. - */ - public initTemplate(apiKeyConfig: Transformer.ApiKeyConfig): Template { - return { - Parameters: this.makeParams(), - Resources: { - [ResourceConstants.RESOURCES.APIKeyLogicalID]: this.makeAppSyncApiKey(apiKeyConfig) - }, - Outputs: { - [ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput]: this.makeApiKeyOutput() - }, - Conditions: { - [ResourceConstants.CONDITIONS.ShouldCreateAPIKey]: - Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.CreateAPIKey), 1), - [ResourceConstants.CONDITIONS.APIKeyExpirationEpochIsPositive]: - Fn.And([ - Fn.Not(Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.APIKeyExpirationEpoch), -1)), - Fn.Not(Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.APIKeyExpirationEpoch), 0)) - ]), - } - } + public makeParams() { + return { + [ResourceConstants.PARAMETERS.AppSyncApiName]: new StringParameter({ + Description: 'The name of the AppSync API', + Default: 'AppSyncSimpleTransform', + }), + [ResourceConstants.PARAMETERS.APIKeyExpirationEpoch]: new NumberParameter({ + Description: + 'The epoch time in seconds when the API Key should expire.' + + ' Setting this to 0 will default to 7 days from the deployment date.' + + ' Setting this to -1 will not create an API Key.', + Default: 0, + MinValue: -1, + }), + [ResourceConstants.PARAMETERS.CreateAPIKey]: new NumberParameter({ + Description: + 'The boolean value to control if an API Key will be created or not.' + + ' The value of the property is automatically set by the CLI.' + + ' If the value is set to 0 no API Key will be created.', + Default: 0, + MinValue: 0, + MaxValue: 1, + }), + [ResourceConstants.PARAMETERS.AuthCognitoUserPoolId]: new StringParameter({ + Description: 'The id of an existing User Pool to connect. If this is changed, a user pool will not be created for you.', + Default: ResourceConstants.NONE, + }), + }; + } + + /** + * Creates the barebones template for an application. + */ + public initTemplate(apiKeyConfig: Transformer.ApiKeyConfig): Template { + return { + Parameters: this.makeParams(), + Resources: { + [ResourceConstants.RESOURCES.APIKeyLogicalID]: this.makeAppSyncApiKey(apiKeyConfig), + }, + Outputs: { + [ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput]: this.makeApiKeyOutput(), + }, + Conditions: { + [ResourceConstants.CONDITIONS.ShouldCreateAPIKey]: Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.CreateAPIKey), 1), + [ResourceConstants.CONDITIONS.APIKeyExpirationEpochIsPositive]: Fn.And([ + Fn.Not(Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.APIKeyExpirationEpoch), -1)), + Fn.Not(Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.APIKeyExpirationEpoch), 0)), + ]), + }, + }; + } + + public makeAppSyncApiKey(apiKeyConfig: Transformer.ApiKeyConfig) { + let expirationDays = 7; + if (apiKeyConfig && apiKeyConfig.apiKeyExpirationDays) { + expirationDays = apiKeyConfig.apiKeyExpirationDays; } - - public makeAppSyncApiKey(apiKeyConfig: Transformer.ApiKeyConfig) { - let expirationDays = 7; - if (apiKeyConfig && apiKeyConfig.apiKeyExpirationDays) { - expirationDays = apiKeyConfig.apiKeyExpirationDays; + const expirationDateInSeconds = 60 /* s */ * 60 /* m */ * 24 /* h */ * expirationDays; /* d */ + const nowEpochTime = Math.floor(Date.now() / 1000); + return new AppSync.ApiKey({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Description: apiKeyConfig && apiKeyConfig.description ? apiKeyConfig.description : undefined, + Expires: Fn.If( + ResourceConstants.CONDITIONS.APIKeyExpirationEpochIsPositive, + Fn.Ref(ResourceConstants.PARAMETERS.APIKeyExpirationEpoch), + nowEpochTime + expirationDateInSeconds + ), + }).condition(ResourceConstants.CONDITIONS.ShouldCreateAPIKey); + } + + /** + * Outputs + */ + public makeApiKeyOutput(): any { + return { + Description: "Your GraphQL API key. Provide via 'x-api-key' header.", + Value: Fn.GetAtt(ResourceConstants.RESOURCES.APIKeyLogicalID, 'ApiKey'), + Export: { + Name: Fn.Join(':', [Refs.StackName, 'GraphQLApiKey']), + }, + Condition: ResourceConstants.CONDITIONS.ShouldCreateAPIKey, + }; + } + + public updateGraphQLAPIWithAuth(apiRecord: GraphQLAPI, authConfig: Transformer.AppSyncAuthConfiguration) { + let properties: GraphQLApiProperties = { + ...apiRecord.Properties, + Name: apiRecord.Properties.Name, + AuthenticationType: authConfig.defaultAuthentication.authenticationType, + UserPoolConfig: undefined, + OpenIDConnectConfig: undefined, + }; + + switch (authConfig.defaultAuthentication.authenticationType) { + case 'AMAZON_COGNITO_USER_POOLS': + properties.UserPoolConfig = new UserPoolConfig({ + UserPoolId: Fn.Ref(ResourceConstants.PARAMETERS.AuthCognitoUserPoolId), + AwsRegion: Refs.Region, + DefaultAction: 'ALLOW', + }); + break; + case 'OPENID_CONNECT': + if (!authConfig.defaultAuthentication.openIDConnectConfig) { + throw new Error('openIDConnectConfig is not configured for defaultAuthentication'); } - const expirationDateInSeconds = 60 /* s */ * 60 /* m */ * 24 /* h */ * expirationDays /* d */ - const nowEpochTime = Math.floor(Date.now() / 1000) - return new AppSync.ApiKey({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Description: (apiKeyConfig && apiKeyConfig.description) ? apiKeyConfig.description : undefined, - Expires: Fn.If( - ResourceConstants.CONDITIONS.APIKeyExpirationEpochIsPositive, - Fn.Ref(ResourceConstants.PARAMETERS.APIKeyExpirationEpoch), - nowEpochTime + expirationDateInSeconds - ), - }).condition(ResourceConstants.CONDITIONS.ShouldCreateAPIKey) - } - /** - * Outputs - */ - public makeApiKeyOutput(): any { - return { - Description: "Your GraphQL API key. Provide via 'x-api-key' header.", - Value: Fn.GetAtt(ResourceConstants.RESOURCES.APIKeyLogicalID, 'ApiKey'), - Export: { - Name: Fn.Join(':', [Refs.StackName, "GraphQLApiKey"]) - }, - Condition: ResourceConstants.CONDITIONS.ShouldCreateAPIKey - }; + properties.OpenIDConnectConfig = this.assignOpenIDConnectConfig(authConfig.defaultAuthentication.openIDConnectConfig); + break; } - public updateGraphQLAPIWithAuth(apiRecord: GraphQLAPI, authConfig: Transformer.AppSyncAuthConfiguration) { - let properties: GraphQLApiProperties = { - ...apiRecord.Properties, - Name: apiRecord.Properties.Name, - AuthenticationType: authConfig.defaultAuthentication.authenticationType, - UserPoolConfig: undefined, - OpenIDConnectConfig: undefined - }; - - switch (authConfig.defaultAuthentication.authenticationType) { - case 'AMAZON_COGNITO_USER_POOLS': - properties.UserPoolConfig = new UserPoolConfig({ - UserPoolId: Fn.Ref(ResourceConstants.PARAMETERS.AuthCognitoUserPoolId), - AwsRegion: Refs.Region, - DefaultAction: 'ALLOW' - }); - break; - case 'OPENID_CONNECT': - if (!authConfig.defaultAuthentication.openIDConnectConfig) { - throw new Error('openIDConnectConfig is not configured for defaultAuthentication'); - } - - properties.OpenIDConnectConfig = this.assignOpenIDConnectConfig(authConfig.defaultAuthentication.openIDConnectConfig); - break; - } - - // Configure additional authentication providers - if (authConfig.additionalAuthenticationProviders && authConfig.additionalAuthenticationProviders.length > 0) { - const additionalAuthenticationProviders = new Array(); - - for (const sourceProvider of authConfig.additionalAuthenticationProviders) { - let provider: AdditionalAuthenticationProvider; - - switch (sourceProvider.authenticationType) { - case 'AMAZON_COGNITO_USER_POOLS': - provider = { - AuthenticationType: 'AMAZON_COGNITO_USER_POOLS', - UserPoolConfig: new UserPoolConfig({ - UserPoolId: Fn.Ref(ResourceConstants.PARAMETERS.AuthCognitoUserPoolId), - AwsRegion: Refs.Region - }) - }; - break; - case 'API_KEY': - provider = { - AuthenticationType: 'API_KEY', - }; - break; - case 'AWS_IAM': - provider = { - AuthenticationType: 'AWS_IAM', - }; - break; - case 'OPENID_CONNECT': - if (!sourceProvider.openIDConnectConfig) { - throw new Error('openIDConnectConfig is not configured for provider'); - } - - provider = { - AuthenticationType: 'OPENID_CONNECT', - OpenIDConnectConfig: this.assignOpenIDConnectConfig(sourceProvider.openIDConnectConfig) - }; - break; - } - - additionalAuthenticationProviders.push(provider); + // Configure additional authentication providers + if (authConfig.additionalAuthenticationProviders && authConfig.additionalAuthenticationProviders.length > 0) { + const additionalAuthenticationProviders = new Array(); + + for (const sourceProvider of authConfig.additionalAuthenticationProviders) { + let provider: AdditionalAuthenticationProvider; + + switch (sourceProvider.authenticationType) { + case 'AMAZON_COGNITO_USER_POOLS': + provider = { + AuthenticationType: 'AMAZON_COGNITO_USER_POOLS', + UserPoolConfig: new UserPoolConfig({ + UserPoolId: Fn.Ref(ResourceConstants.PARAMETERS.AuthCognitoUserPoolId), + AwsRegion: Refs.Region, + }), + }; + break; + case 'API_KEY': + provider = { + AuthenticationType: 'API_KEY', + }; + break; + case 'AWS_IAM': + provider = { + AuthenticationType: 'AWS_IAM', + }; + break; + case 'OPENID_CONNECT': + if (!sourceProvider.openIDConnectConfig) { + throw new Error('openIDConnectConfig is not configured for provider'); } - properties.AdditionalAuthenticationProviders = additionalAuthenticationProviders; + provider = { + AuthenticationType: 'OPENID_CONNECT', + OpenIDConnectConfig: this.assignOpenIDConnectConfig(sourceProvider.openIDConnectConfig), + }; + break; } - return new GraphQLAPI(properties); - } + additionalAuthenticationProviders.push(provider); + } - private assignOpenIDConnectConfig(config: Transformer.OpenIDConnectConfig) { - return new OpenIDConnectConfig({ - Issuer: config.issuerUrl, - ClientId: config.clientId, - IatTTL: config.iatTTL, - AuthTTL: config.authTTL - }); + properties.AdditionalAuthenticationProviders = additionalAuthenticationProviders; } - public blankResolver(type: string, field: string) { - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: 'NONE', - FieldName: field, - TypeName: type, - RequestMappingTemplate: print(obj({ - "version": str("2017-02-28"), - "payload": obj({}) - })), - ResponseMappingTemplate: print(ref(`util.toJson($context.source.${field})`)) + return new GraphQLAPI(properties); + } + + private assignOpenIDConnectConfig(config: Transformer.OpenIDConnectConfig) { + return new OpenIDConnectConfig({ + Issuer: config.issuerUrl, + ClientId: config.clientId, + IatTTL: config.iatTTL, + AuthTTL: config.authTTL, + }); + } + + public blankResolver(type: string, field: string) { + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: 'NONE', + FieldName: field, + TypeName: type, + RequestMappingTemplate: print( + obj({ + version: str('2017-02-28'), + payload: obj({}), }) + ), + ResponseMappingTemplate: print(ref(`util.toJson($context.source.${field})`)), + }); + } + + public noneDataSource() { + return new AppSync.DataSource({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Name: 'NONE', + Type: 'NONE', + }); + } + + /** + * Builds a VTL expression that will set the + * ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable variable to + * true if the user is static group authorized. + * @param rules The list of static group authorization rules. + */ + public staticGroupAuthorizationExpression(rules: AuthRule[], field?: FieldDefinitionNode): Expression { + if (!rules || rules.length === 0) { + return comment(`No Static Group Authorization Rules`); } - - public noneDataSource() { - return new AppSync.DataSource({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Name: 'NONE', - Type: 'NONE' - }) + const variableToSet = this.getStaticAuthorizationVariable(field); + let groupAuthorizationExpressions = []; + for (const rule of rules) { + const groups = rule.groups; + const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM; + + if (groups) { + groupAuthorizationExpressions = groupAuthorizationExpressions.concat( + comment(`Authorization rule: { allow: groups, groups: ${JSON.stringify(groups)}, groupClaim: "${groupClaimAttribute}" }`), + this.setUserGroups(rule.groupClaim), + set(ref('allowedGroups'), list(groups.map(s => str(s)))), + forEach(ref('userGroup'), ref('userGroups'), [ + iff(raw(`$allowedGroups.contains($userGroup)`), compoundExpression([set(ref(variableToSet), raw('true')), raw('#break')])), + ]) + ); + } } + const staticGroupAuthorizedVariable = this.getStaticAuthorizationVariable(field); - /** - * Builds a VTL expression that will set the - * ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable variable to - * true if the user is static group authorized. - * @param rules The list of static group authorization rules. - */ - public staticGroupAuthorizationExpression(rules: AuthRule[], field?: FieldDefinitionNode ): Expression { - if (!rules || rules.length === 0) { - return comment(`No Static Group Authorization Rules`) - } - const variableToSet = this.getStaticAuthorizationVariable(field); - let groupAuthorizationExpressions = [] - for (const rule of rules) { - const groups = rule.groups; - const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM - - if (groups) { - groupAuthorizationExpressions = groupAuthorizationExpressions.concat( - comment(`Authorization rule: { allow: groups, groups: ${JSON.stringify(groups)}, groupClaim: "${groupClaimAttribute}" }`), - this.setUserGroups(rule.groupClaim), - set(ref('allowedGroups'), list(groups.map(s => str(s)))), - forEach(ref('userGroup'), ref('userGroups'), [ - iff( - raw(`$allowedGroups.contains($userGroup)`), - compoundExpression([ - set(ref(variableToSet), raw('true')), - raw('#break') - ]) - ) - ]) - ); - } - } - const staticGroupAuthorizedVariable = this.getStaticAuthorizationVariable(field); - - // tslint:disable-next-line - return block('Static Group Authorization Checks', [ - raw(`#set($${staticGroupAuthorizedVariable} = $util.defaultIfNull( + // tslint:disable-next-line + return block('Static Group Authorization Checks', [ + raw(`#set($${staticGroupAuthorizedVariable} = $util.defaultIfNull( $${staticGroupAuthorizedVariable}, false))`), - ...groupAuthorizationExpressions - ]) + ...groupAuthorizationExpressions, + ]); + } + + /** + * Given a set of dynamic group authorization rules verifies that input + * value satisfies at least one dynamic group authorization rule. + * @param rules The list of authorization rules. + * @param variableToCheck The name of the value containing the input. + * @param variableToSet The name of the variable to set when auth is satisfied. + */ + public dynamicGroupAuthorizationExpressionForCreateOperations( + rules: AuthRule[], + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable + ): Expression { + if (!rules || rules.length === 0) { + return comment(`No Dynamic Group Authorization Rules`); } - - - - /** - * Given a set of dynamic group authorization rules verifies that input - * value satisfies at least one dynamic group authorization rule. - * @param rules The list of authorization rules. - * @param variableToCheck The name of the value containing the input. - * @param variableToSet The name of the variable to set when auth is satisfied. - */ - public dynamicGroupAuthorizationExpressionForCreateOperations( - rules: AuthRule[], - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable, - ): Expression { - if (!rules || rules.length === 0) { - return comment(`No Dynamic Group Authorization Rules`) - } - return block('Dynamic Group Authorization Checks', [ - this.dynamicAuthorizationExpressionForCreate(rules, variableToCheck, variableToSet) - ]) + return block('Dynamic Group Authorization Checks', [ + this.dynamicAuthorizationExpressionForCreate(rules, variableToCheck, variableToSet), + ]); + } + + /** + * Given a set of dynamic group authorization rules verifies that input + * value satisfies at least one dynamic group authorization rule. + * @param rules The list of authorization rules. + * @param variableToCheck The name of the value containing the input. + * @param variableToSet The name of the variable to set when auth is satisfied. + */ + public dynamicGroupAuthorizationExpressionForCreateOperationsByField( + rules: AuthRule[], + fieldToCheck: string, + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable + ): Expression { + if (!rules || rules.length === 0) { + return comment(`No dynamic group authorization rules for field "${fieldToCheck}"`); } - - /** - * Given a set of dynamic group authorization rules verifies that input - * value satisfies at least one dynamic group authorization rule. - * @param rules The list of authorization rules. - * @param variableToCheck The name of the value containing the input. - * @param variableToSet The name of the variable to set when auth is satisfied. - */ - public dynamicGroupAuthorizationExpressionForCreateOperationsByField( - rules: AuthRule[], - fieldToCheck: string, - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable, - ): Expression { - if (!rules || rules.length === 0) { - return comment(`No dynamic group authorization rules for field "${fieldToCheck}"`); - } - let groupAuthorizationExpression: Expression = this.dynamicAuthorizationExpressionForCreate( - rules, variableToCheck, variableToSet, - rule => `Authorization rule on field "${fieldToCheck}": { allow: ${rule.allow}, \ + let groupAuthorizationExpression: Expression = this.dynamicAuthorizationExpressionForCreate( + rules, + variableToCheck, + variableToSet, + rule => `Authorization rule on field "${fieldToCheck}": { allow: ${rule.allow}, \ groupsField: "${rule.groupsField || DEFAULT_GROUPS_FIELD}", groupClaim: "${rule.groupClaim || DEFAULT_GROUP_CLAIM}" }` - ) - return block(`Dynamic group authorization rules for field "${fieldToCheck}"`, [ - groupAuthorizationExpression + ); + return block(`Dynamic group authorization rules for field "${fieldToCheck}"`, [groupAuthorizationExpression]); + } + + private dynamicAuthorizationExpressionForCreate( + rules: AuthRule[], + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable, + formatComment?: (rule: AuthRule) => string + ) { + let groupAuthorizationExpressions = []; + for (const rule of rules) { + // for loop do check of rules here + const groupsAttribute = rule.groupsField || DEFAULT_GROUPS_FIELD; + const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM; + groupAuthorizationExpressions = groupAuthorizationExpressions.concat( + formatComment + ? comment(formatComment(rule)) + : comment(`Authorization rule: { allow: ${rule.allow}, groupsField: "${groupsAttribute}", groupClaim: "${groupClaimAttribute}"`), + this.setUserGroups(rule.groupClaim), + set(ref(variableToSet), raw(`$util.defaultIfNull($${variableToSet}, false)`)), + forEach(ref('userGroup'), ref('userGroups'), [ + iff( + raw(`$util.isList($ctx.args.input.${groupsAttribute})`), + iff(ref(`${variableToCheck}.${groupsAttribute}.contains($userGroup)`), set(ref(variableToSet), raw('true'))) + ), + iff( + raw(`$util.isString($ctx.args.input.${groupsAttribute})`), + iff(raw(`$ctx.args.input.${groupsAttribute} == $userGroup`), set(ref(variableToSet), raw('true'))) + ), ]) + ); } - private dynamicAuthorizationExpressionForCreate( - rules: AuthRule[], - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable, - formatComment?: (rule: AuthRule) => string, - ) { - let groupAuthorizationExpressions = [] - for (const rule of rules) { - // for loop do check of rules here - const groupsAttribute = rule.groupsField || DEFAULT_GROUPS_FIELD - const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM - groupAuthorizationExpressions = groupAuthorizationExpressions.concat( - formatComment ? - comment(formatComment(rule)) : - comment(`Authorization rule: { allow: ${rule.allow}, groupsField: "${groupsAttribute}", groupClaim: "${groupClaimAttribute}"`), - this.setUserGroups(rule.groupClaim), - set( - ref(variableToSet), - raw(`$util.defaultIfNull($${variableToSet}, false)`) - ), - forEach(ref('userGroup'), ref('userGroups'), [ - iff( - raw(`$util.isList($ctx.args.input.${groupsAttribute})`), - iff( - ref(`${variableToCheck}.${groupsAttribute}.contains($userGroup)`), - set(ref(variableToSet), raw('true')) - ), - ), - iff( - raw(`$util.isString($ctx.args.input.${groupsAttribute})`), - iff( - raw(`$ctx.args.input.${groupsAttribute} == $userGroup`), - set(ref(variableToSet), raw('true')) - ), - ) - ]) - ) - } - - return compoundExpression(groupAuthorizationExpressions) - } - - /** - * Given a set of owner authorization rules verifies that input - * value satisfies at least one rule. - * @param rules The list of authorization rules. - * @param variableToCheck The name of the value containing the input. - * @param variableToSet The name of the variable to set when auth is satisfied. - */ - public ownerAuthorizationExpressionForCreateOperations( - rules: AuthRule[], - fieldIsList: (fieldName: string) => boolean, - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, - ): Expression { - if (!rules || rules.length === 0) { - return comment(`No Owner Authorization Rules`) - } - return block('Owner Authorization Checks', [ - this.ownershipAuthorizationExpressionForCreate(rules, fieldIsList, variableToCheck, variableToSet) - ]) + return compoundExpression(groupAuthorizationExpressions); + } + + /** + * Given a set of owner authorization rules verifies that input + * value satisfies at least one rule. + * @param rules The list of authorization rules. + * @param variableToCheck The name of the value containing the input. + * @param variableToSet The name of the variable to set when auth is satisfied. + */ + public ownerAuthorizationExpressionForCreateOperations( + rules: AuthRule[], + fieldIsList: (fieldName: string) => boolean, + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable + ): Expression { + if (!rules || rules.length === 0) { + return comment(`No Owner Authorization Rules`); } - - public ownerAuthorizationExpressionForSubscriptions( - rules: AuthRule[], - variableToCheck: string = 'ctx.args', - variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, - ): Expression { - if (!rules || rules.length === 0) { - return comment(`No Owner Authorization Rules`) - } - return block('Owner Authorization Checks', [ - this.ownershipAuthorizationExpressionForSubscriptions(rules, variableToCheck, variableToSet) - ]) + return block('Owner Authorization Checks', [ + this.ownershipAuthorizationExpressionForCreate(rules, fieldIsList, variableToCheck, variableToSet), + ]); + } + + public ownerAuthorizationExpressionForSubscriptions( + rules: AuthRule[], + variableToCheck: string = 'ctx.args', + variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable + ): Expression { + if (!rules || rules.length === 0) { + return comment(`No Owner Authorization Rules`); } - public ownershipAuthorizationExpressionForSubscriptions( - rules: AuthRule[], - variableToCheck: string = 'ctx.args', - variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, - formatComment?: (rule: AuthRule) => string, - ) { - let ownershipAuthorizationExpressions = [] - let ruleNumber = 0; - for (const rule of rules) { - const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD - const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD - const isUser = isUsername(rawUsername) - const identityAttribute = replaceIfUsername(rawUsername) - const allowedOwnersVariable = `allowedOwners${ruleNumber}` - ownershipAuthorizationExpressions = ownershipAuthorizationExpressions.concat( - formatComment ? - comment(formatComment(rule)) : - comment(`Authorization rule: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }`), - set(ref(allowedOwnersVariable), raw(`$util.defaultIfNull($${variableToCheck}.${ownerAttribute}, null)`)), - isUser ? - // tslint:disable-next-line - set( - ref('identityValue'), - raw(`$util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), + return block('Owner Authorization Checks', [ + this.ownershipAuthorizationExpressionForSubscriptions(rules, variableToCheck, variableToSet), + ]); + } + public ownershipAuthorizationExpressionForSubscriptions( + rules: AuthRule[], + variableToCheck: string = 'ctx.args', + variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, + formatComment?: (rule: AuthRule) => string + ) { + let ownershipAuthorizationExpressions = []; + let ruleNumber = 0; + for (const rule of rules) { + const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD; + const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD; + const isUser = isUsername(rawUsername); + const identityAttribute = replaceIfUsername(rawUsername); + const allowedOwnersVariable = `allowedOwners${ruleNumber}`; + ownershipAuthorizationExpressions = ownershipAuthorizationExpressions.concat( + formatComment + ? comment(formatComment(rule)) + : comment(`Authorization rule: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }`), + set(ref(allowedOwnersVariable), raw(`$util.defaultIfNull($${variableToCheck}.${ownerAttribute}, null)`)), + isUser + ? // tslint:disable-next-line + set( + ref('identityValue'), + raw(`$util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), $util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}"))`) - ) - : set( - ref('identityValue'), - raw(`$util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")`) - ), - // If a list of owners check for at least one. - iff( - raw(`$util.isList($${allowedOwnersVariable})`), - forEach(ref('allowedOwner'), ref(allowedOwnersVariable), [ - iff( - raw(`$allowedOwner == $identityValue`), - set(ref(variableToSet), raw('true'))), - ]) - ), - // If a single owner check for at least one. - iff( - raw(`$util.isString($${allowedOwnersVariable})`), - iff( - raw(`$${allowedOwnersVariable} == $identityValue`), - set(ref(variableToSet), raw('true'))), - ) ) - ruleNumber++ - } - return compoundExpression([ - set(ref(variableToSet), raw(`false`)), - ...ownershipAuthorizationExpressions, - ]); + : set(ref('identityValue'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")`)), + // If a list of owners check for at least one. + iff( + raw(`$util.isList($${allowedOwnersVariable})`), + forEach(ref('allowedOwner'), ref(allowedOwnersVariable), [ + iff(raw(`$allowedOwner == $identityValue`), set(ref(variableToSet), raw('true'))), + ]) + ), + // If a single owner check for at least one. + iff( + raw(`$util.isString($${allowedOwnersVariable})`), + iff(raw(`$${allowedOwnersVariable} == $identityValue`), set(ref(variableToSet), raw('true'))) + ) + ); + ruleNumber++; } - - /** - * Given a set of owner authorization rules verifies that if the input - * specifies the given input field, the value satisfies at least one rule. - * @param rules The list of authorization rules. - * @param variableToCheck The name of the value containing the input. - * @param variableToSet The name of the variable to set when auth is satisfied. - */ - public ownerAuthorizationExpressionForCreateOperationsByField( - rules: AuthRule[], - fieldToCheck: string, - fieldIsList: (fieldName: string) => boolean, - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, - ): Expression { - if (!rules || rules.length === 0) { - return comment(`No Owner Authorization Rules`) - } - return block(`Owner authorization rules for field "${fieldToCheck}"`, [ - this.ownershipAuthorizationExpressionForCreate( - rules, fieldIsList, variableToCheck, variableToSet, - rule => `Authorization rule: { allow: ${rule.allow}, \ + return compoundExpression([set(ref(variableToSet), raw(`false`)), ...ownershipAuthorizationExpressions]); + } + + /** + * Given a set of owner authorization rules verifies that if the input + * specifies the given input field, the value satisfies at least one rule. + * @param rules The list of authorization rules. + * @param variableToCheck The name of the value containing the input. + * @param variableToSet The name of the variable to set when auth is satisfied. + */ + public ownerAuthorizationExpressionForCreateOperationsByField( + rules: AuthRule[], + fieldToCheck: string, + fieldIsList: (fieldName: string) => boolean, + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable + ): Expression { + if (!rules || rules.length === 0) { + return comment(`No Owner Authorization Rules`); + } + return block(`Owner authorization rules for field "${fieldToCheck}"`, [ + this.ownershipAuthorizationExpressionForCreate( + rules, + fieldIsList, + variableToCheck, + variableToSet, + rule => `Authorization rule: { allow: ${rule.allow}, \ ownerField: "${rule.ownerField || DEFAULT_OWNER_FIELD}", \ identityClaim: "${rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD}" }` + ), + ]); + } + + public ownershipAuthorizationExpressionForCreate( + rules: AuthRule[], + fieldIsList: (fieldName: string) => boolean, + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, + formatComment?: (rule: AuthRule) => string + ) { + let ownershipAuthorizationExpressions = []; + let ruleNumber = 0; + for (const rule of rules) { + const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD; + const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD; + const isUser = isUsername(rawUsername); + const identityAttribute = replaceIfUsername(rawUsername); + const ownerFieldIsList = fieldIsList(ownerAttribute); + const allowedOwnersVariable = `allowedOwners${ruleNumber}`; + ownershipAuthorizationExpressions = ownershipAuthorizationExpressions.concat( + formatComment + ? comment(formatComment(rule)) + : comment(`Authorization rule: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }`), + set(ref(allowedOwnersVariable), raw(`$util.defaultIfNull($${variableToCheck}.${ownerAttribute}, null)`)), + isUser + ? // tslint:disable-next-line + set( + ref('identityValue'), + raw( + `$util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), $util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}"))` + ) ) - ]) + : set(ref('identityValue'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")`)), + // If a list of owners check for at least one. + iff( + raw(`$util.isList($${allowedOwnersVariable})`), + forEach(ref('allowedOwner'), ref(allowedOwnersVariable), [ + iff(raw(`$allowedOwner == $identityValue`), set(ref(variableToSet), raw('true'))), + ]) + ), + // If a single owner check for at least one. + iff( + raw(`$util.isString($${allowedOwnersVariable})`), + iff(raw(`$${allowedOwnersVariable} == $identityValue`), set(ref(variableToSet), raw('true'))) + ) + ); + // If the owner field is not a list and the user does not + // provide a value for the owner, set the owner automatically. + if (!ownerFieldIsList) { + ownershipAuthorizationExpressions.push( + // If the owner is not provided set it automatically. + // If the user explicitly provides null this will be false and we leave it null. + iff( + and([raw(`$util.isNull($${allowedOwnersVariable})`), parens(raw(`! $${variableToCheck}.containsKey("${ownerAttribute}")`))]), + compoundExpression([qref(`$${variableToCheck}.put("${ownerAttribute}", $identityValue)`), set(ref(variableToSet), raw('true'))]) + ) + ); + } else { + // If the owner field is a list and the user does not + // provide a list of values for the owner, set the list with + // the owner as the sole member. + ownershipAuthorizationExpressions.push( + // If the owner is not provided set it automatically. + // If the user explicitly provides null this will be false and we leave it null. + iff( + and([raw(`$util.isNull($${allowedOwnersVariable})`), parens(raw(`! $${variableToCheck}.containsKey("${ownerAttribute}")`))]), + compoundExpression([ + qref(`$${variableToCheck}.put("${ownerAttribute}", ["$identityValue"])`), + set(ref(variableToSet), raw('true')), + ]) + ) + ); + } + ruleNumber++; } - - public ownershipAuthorizationExpressionForCreate( - rules: AuthRule[], - fieldIsList: (fieldName: string) => boolean, - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, - formatComment?: (rule: AuthRule) => string, - ) { - let ownershipAuthorizationExpressions = [] - let ruleNumber = 0; - for (const rule of rules) { - const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD - const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD - const isUser = isUsername(rawUsername) - const identityAttribute = replaceIfUsername(rawUsername) - const ownerFieldIsList = fieldIsList(ownerAttribute) - const allowedOwnersVariable = `allowedOwners${ruleNumber}` - ownershipAuthorizationExpressions = ownershipAuthorizationExpressions.concat( - formatComment ? - comment(formatComment(rule)) : - comment(`Authorization rule: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }`), - set(ref(allowedOwnersVariable), raw(`$util.defaultIfNull($${variableToCheck}.${ownerAttribute}, null)`)), - isUser ? - // tslint:disable-next-line - set(ref('identityValue'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), $util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}"))`)) : - set(ref('identityValue'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")`)), - // If a list of owners check for at least one. - iff( - raw(`$util.isList($${allowedOwnersVariable})`), - forEach(ref('allowedOwner'), ref(allowedOwnersVariable), [ - iff( - raw(`$allowedOwner == $identityValue`), - set(ref(variableToSet), raw('true'))), - ]) - ), - // If a single owner check for at least one. - iff( - raw(`$util.isString($${allowedOwnersVariable})`), - iff( - raw(`$${allowedOwnersVariable} == $identityValue`), - set(ref(variableToSet), raw('true'))), - ) - ) - // If the owner field is not a list and the user does not - // provide a value for the owner, set the owner automatically. - if (!ownerFieldIsList) { - ownershipAuthorizationExpressions.push( - // If the owner is not provided set it automatically. - // If the user explicitly provides null this will be false and we leave it null. - iff( - and([ - raw(`$util.isNull($${allowedOwnersVariable})`), - parens(raw(`! $${variableToCheck}.containsKey("${ownerAttribute}")`)), - ]), - compoundExpression([ - qref(`$${variableToCheck}.put("${ownerAttribute}", $identityValue)`), - set(ref(variableToSet), raw('true')) - ]) - ) - ) - } else { - // If the owner field is a list and the user does not - // provide a list of values for the owner, set the list with - // the owner as the sole member. - ownershipAuthorizationExpressions.push( - // If the owner is not provided set it automatically. - // If the user explicitly provides null this will be false and we leave it null. - iff( - and([ - raw(`$util.isNull($${allowedOwnersVariable})`), - parens(raw(`! $${variableToCheck}.containsKey("${ownerAttribute}")`)), - ]), - compoundExpression([ - qref(`$${variableToCheck}.put("${ownerAttribute}", ["$identityValue"])`), - set(ref(variableToSet), raw('true')) - ]) - ) - ) - } - ruleNumber++ - } - return compoundExpression([ - set(ref(variableToSet), raw(`false`)), - ...ownershipAuthorizationExpressions, - ]); + return compoundExpression([set(ref(variableToSet), raw(`false`)), ...ownershipAuthorizationExpressions]); + } + + /** + * Given a set of dynamic group authorization rules verifies w/ a conditional + * expression that the existing object has the correct group expression. + * @param rules The list of authorization rules. + * @param variableToCheck The name of the value containing the input. + * @param variableToSet The name of the variable to set when auth is satisfied. + */ + public dynamicGroupAuthorizationExpressionForUpdateOrDeleteOperations( + rules: AuthRule[], + fieldBeingProtected?: string, + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable + ): Expression { + const fieldMention = fieldBeingProtected ? ` for field "${fieldBeingProtected}"` : ''; + if (!rules || rules.length === 0) { + return comment(`No dynamic group authorization rules${fieldMention}`); } - /** - * Given a set of dynamic group authorization rules verifies w/ a conditional - * expression that the existing object has the correct group expression. - * @param rules The list of authorization rules. - * @param variableToCheck The name of the value containing the input. - * @param variableToSet The name of the variable to set when auth is satisfied. - */ - public dynamicGroupAuthorizationExpressionForUpdateOrDeleteOperations( - rules: AuthRule[], - fieldBeingProtected?: string, - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable, - ): Expression { - const fieldMention = fieldBeingProtected ? ` for field "${fieldBeingProtected}"` : ''; - if (!rules || rules.length === 0) { - return comment(`No dynamic group authorization rules${fieldMention}`) - } - - let groupAuthorizationExpressions = [] - let ruleNumber = 0 - for (const rule of rules) { - const groupsAttribute = rule.groupsField || DEFAULT_GROUPS_FIELD - const groupsAttributeName = fieldBeingProtected ? `${fieldBeingProtected}_groupsAttribute${ruleNumber}` : `groupsAttribute${ruleNumber}` - const groupName = fieldBeingProtected ? `${fieldBeingProtected}_group${ruleNumber}` : `group${ruleNumber}` - const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM - groupAuthorizationExpressions = groupAuthorizationExpressions.concat( - comment(`Authorization rule${fieldMention}: { allow: ${rule.allow}, groupsField: "${groupsAttribute}", groupClaim: "${groupClaimAttribute}"}`), - // Add the new auth expression and values - this.setUserGroups(rule.groupClaim), - forEach(ref('userGroup'), ref('userGroups'), [ - raw(`$util.qr($groupAuthExpressions.add("contains(#${groupsAttributeName}, :${groupName}$foreach.count)"))`), - raw(`$util.qr($groupAuthExpressionValues.put(":${groupName}$foreach.count", { "S": $userGroup }))`), - ]), - iff(raw('$userGroups.size() > 0'), raw(`$util.qr($groupAuthExpressionNames.put("#${groupsAttributeName}", "${groupsAttribute}"))`)), - ) - ruleNumber++ - } - // check for groupclaim here - return block('Dynamic group authorization checks', [ - set(ref('groupAuthExpressions'), list([])), - set(ref('groupAuthExpressionValues'), obj({})), - set(ref('groupAuthExpressionNames'), obj({})), - ...groupAuthorizationExpressions, - ]) + let groupAuthorizationExpressions = []; + let ruleNumber = 0; + for (const rule of rules) { + const groupsAttribute = rule.groupsField || DEFAULT_GROUPS_FIELD; + const groupsAttributeName = fieldBeingProtected + ? `${fieldBeingProtected}_groupsAttribute${ruleNumber}` + : `groupsAttribute${ruleNumber}`; + const groupName = fieldBeingProtected ? `${fieldBeingProtected}_group${ruleNumber}` : `group${ruleNumber}`; + const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM; + groupAuthorizationExpressions = groupAuthorizationExpressions.concat( + comment( + `Authorization rule${fieldMention}: { allow: ${rule.allow}, groupsField: "${groupsAttribute}", groupClaim: "${groupClaimAttribute}"}` + ), + // Add the new auth expression and values + this.setUserGroups(rule.groupClaim), + forEach(ref('userGroup'), ref('userGroups'), [ + raw(`$util.qr($groupAuthExpressions.add("contains(#${groupsAttributeName}, :${groupName}$foreach.count)"))`), + raw(`$util.qr($groupAuthExpressionValues.put(":${groupName}$foreach.count", { "S": $userGroup }))`), + ]), + iff(raw('$userGroups.size() > 0'), raw(`$util.qr($groupAuthExpressionNames.put("#${groupsAttributeName}", "${groupsAttribute}"))`)) + ); + ruleNumber++; } - - /** - * Given a set of owner authorization rules verifies with a conditional - * expression that the existing object is owned. - * @param rules The list of authorization rules. - * @param variableToCheck The name of the value containing the input. - * @param variableToSet The name of the variable to set when auth is satisfied. - */ - public ownerAuthorizationExpressionForUpdateOrDeleteOperations( - rules: AuthRule[], - fieldIsList: (fieldName: string) => boolean, - fieldBeingProtected?: string, - variableToCheck: string = 'ctx.args.input', - variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, - ): Expression { - const fieldMention = fieldBeingProtected ? ` for field "${fieldBeingProtected}"` : ''; - if (!rules || rules.length === 0) { - return comment(`No owner authorization rules${fieldMention}`) - } - let ownerAuthorizationExpressions = [] - let ruleNumber = 0; - for (const rule of rules) { - const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD - const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD - const isUser = isUsername(rawUsername) - const identityAttribute = replaceIfUsername(rawUsername) - const ownerFieldIsList = fieldIsList(ownerAttribute) - const ownerName = fieldBeingProtected ? `${fieldBeingProtected}_owner${ruleNumber}` : `owner${ruleNumber}` - const identityName = fieldBeingProtected ? `${fieldBeingProtected}_identity${ruleNumber}` : `identity${ruleNumber}` - - ownerAuthorizationExpressions.push( - // tslint:disable:max-line-length - comment(`Authorization rule${fieldMention}: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }`), + // check for groupclaim here + return block('Dynamic group authorization checks', [ + set(ref('groupAuthExpressions'), list([])), + set(ref('groupAuthExpressionValues'), obj({})), + set(ref('groupAuthExpressionNames'), obj({})), + ...groupAuthorizationExpressions, + ]); + } + + /** + * Given a set of owner authorization rules verifies with a conditional + * expression that the existing object is owned. + * @param rules The list of authorization rules. + * @param variableToCheck The name of the value containing the input. + * @param variableToSet The name of the variable to set when auth is satisfied. + */ + public ownerAuthorizationExpressionForUpdateOrDeleteOperations( + rules: AuthRule[], + fieldIsList: (fieldName: string) => boolean, + fieldBeingProtected?: string, + variableToCheck: string = 'ctx.args.input', + variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable + ): Expression { + const fieldMention = fieldBeingProtected ? ` for field "${fieldBeingProtected}"` : ''; + if (!rules || rules.length === 0) { + return comment(`No owner authorization rules${fieldMention}`); + } + let ownerAuthorizationExpressions = []; + let ruleNumber = 0; + for (const rule of rules) { + const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD; + const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD; + const isUser = isUsername(rawUsername); + const identityAttribute = replaceIfUsername(rawUsername); + const ownerFieldIsList = fieldIsList(ownerAttribute); + const ownerName = fieldBeingProtected ? `${fieldBeingProtected}_owner${ruleNumber}` : `owner${ruleNumber}`; + const identityName = fieldBeingProtected ? `${fieldBeingProtected}_identity${ruleNumber}` : `identity${ruleNumber}`; + + ownerAuthorizationExpressions.push( + // tslint:disable:max-line-length + comment( + `Authorization rule${fieldMention}: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }` + ) + ); + if (ownerFieldIsList) { + ownerAuthorizationExpressions.push(raw(`$util.qr($ownerAuthExpressions.add("contains(#${ownerName}, :${identityName})"))`)); + } else { + ownerAuthorizationExpressions.push(raw(`$util.qr($ownerAuthExpressions.add("#${ownerName} = :${identityName}"))`)); + } + ownerAuthorizationExpressions = ownerAuthorizationExpressions.concat( + raw(`$util.qr($ownerAuthExpressionNames.put("#${ownerName}", "${ownerAttribute}"))`), + // tslint:disable + isUser + ? raw( + `$util.qr($ownerAuthExpressionValues.put(":${identityName}", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), $util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")))))` ) - if (ownerFieldIsList) { - ownerAuthorizationExpressions.push( - raw(`$util.qr($ownerAuthExpressions.add("contains(#${ownerName}, :${identityName})"))`) - ) - } else { - ownerAuthorizationExpressions.push( - raw(`$util.qr($ownerAuthExpressions.add("#${ownerName} = :${identityName}"))`) - ) - } - ownerAuthorizationExpressions = ownerAuthorizationExpressions.concat( - raw(`$util.qr($ownerAuthExpressionNames.put("#${ownerName}", "${ownerAttribute}"))`), - // tslint:disable - isUser ? - raw(`$util.qr($ownerAuthExpressionValues.put(":${identityName}", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), $util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")))))`) : - raw(`$util.qr($ownerAuthExpressionValues.put(":${identityName}", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}"))))`) - // tslint:enable + : raw( + `$util.qr($ownerAuthExpressionValues.put(":${identityName}", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}"))))` ) - ruleNumber++ - } - return block('Owner Authorization Checks', [ - set(ref('ownerAuthExpressions'), list([])), - set(ref('ownerAuthExpressionValues'), obj({})), - set(ref('ownerAuthExpressionNames'), obj({})), - ...ownerAuthorizationExpressions, - ]) + // tslint:enable + ); + ruleNumber++; } - - /** - * Given a list of rules return a VTL expression that checks if the given variableToCheck - * statisies at least one of the auth rules. - * @param rules The list of dynamic group authorization rules. - */ - public dynamicGroupAuthorizationExpressionForReadOperations( - rules: AuthRule[], - variableToCheck: string = 'ctx.result', - variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable, - defaultValue: Expression = raw(`$util.defaultIfNull($${variableToSet}, false)`) - ): Expression { - if (!rules || rules.length === 0) { - return comment(`No Dynamic Group Authorization Rules`) - } - let groupAuthorizationExpressions = []; - for (const rule of rules) { - const groupsAttribute = rule.groupsField || DEFAULT_GROUPS_FIELD - const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM - groupAuthorizationExpressions = groupAuthorizationExpressions.concat( - comment(`Authorization rule: { allow: ${rule.allow}, groupsField: "${groupsAttribute}", groupClaim: "${groupClaimAttribute}" }`), - set(ref('allowedGroups'), ref(`util.defaultIfNull($${variableToCheck}.${groupsAttribute}, [])`)), - this.setUserGroups(rule.groupClaim), - forEach(ref('userGroup'), ref('userGroups'), [ - iff( - raw('$util.isList($allowedGroups)'), - iff( - raw(`$allowedGroups.contains($userGroup)`), - set(ref(variableToSet), raw('true'))), - ), - iff( - raw(`$util.isString($allowedGroups)`), - iff( - raw(`$allowedGroups == $userGroup`), - set(ref(variableToSet), raw('true'))), - ) - ]) - ) - } - // check for group claim here - return block('Dynamic Group Authorization Checks', [ - set(ref(variableToSet), defaultValue), - ...groupAuthorizationExpressions, - ]) + return block('Owner Authorization Checks', [ + set(ref('ownerAuthExpressions'), list([])), + set(ref('ownerAuthExpressionValues'), obj({})), + set(ref('ownerAuthExpressionNames'), obj({})), + ...ownerAuthorizationExpressions, + ]); + } + + /** + * Given a list of rules return a VTL expression that checks if the given variableToCheck + * statisies at least one of the auth rules. + * @param rules The list of dynamic group authorization rules. + */ + public dynamicGroupAuthorizationExpressionForReadOperations( + rules: AuthRule[], + variableToCheck: string = 'ctx.result', + variableToSet: string = ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable, + defaultValue: Expression = raw(`$util.defaultIfNull($${variableToSet}, false)`) + ): Expression { + if (!rules || rules.length === 0) { + return comment(`No Dynamic Group Authorization Rules`); } - - /** - * Given a list of rules return a VTL expression that checks if the given variableToCheck - * statisies at least one of the auth rules. - * @param rules The list of dynamic group authorization rules. - */ - public ownerAuthorizationExpressionForReadOperations( - rules: AuthRule[], - variableToCheck: string = 'ctx.result', - variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, - defaultValue: Expression = raw(`$util.defaultIfNull($${variableToSet}, false)`) - ): Expression { - if (!rules || rules.length === 0) { - return comment(`No Owner Authorization Rules`) - } - let ownerAuthorizationExpressions = []; - let ruleNumber = 0; - for (const rule of rules) { - const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD - const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD - const isUser = isUsername(rawUsername) - const identityAttribute = replaceIfUsername(rawUsername) - const allowedOwnersVariable = `allowedOwners${ruleNumber}` - ownerAuthorizationExpressions = ownerAuthorizationExpressions.concat( - comment(`Authorization rule: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }`), - set(ref(allowedOwnersVariable), ref(`${variableToCheck}.${ownerAttribute}`)), - isUser ? - // tslint:disable-next-line - set(ref('identityValue'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), $util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}"))`)) : - set(ref('identityValue'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")`)), - iff( - raw(`$util.isList($${allowedOwnersVariable})`), - forEach(ref('allowedOwner'), ref(allowedOwnersVariable), [ - iff( - raw(`$allowedOwner == $identityValue`), - set(ref(variableToSet), raw('true'))), - ]) - ), - iff( - raw(`$util.isString($${allowedOwnersVariable})`), - iff( - raw(`$${allowedOwnersVariable} == $identityValue`), - set(ref(variableToSet), raw('true'))), - ) - ) - ruleNumber++ - } - return block('Owner Authorization Checks', [ - set(ref(variableToSet), defaultValue), - ...ownerAuthorizationExpressions + let groupAuthorizationExpressions = []; + for (const rule of rules) { + const groupsAttribute = rule.groupsField || DEFAULT_GROUPS_FIELD; + const groupClaimAttribute = rule.groupClaim || DEFAULT_GROUP_CLAIM; + groupAuthorizationExpressions = groupAuthorizationExpressions.concat( + comment(`Authorization rule: { allow: ${rule.allow}, groupsField: "${groupsAttribute}", groupClaim: "${groupClaimAttribute}" }`), + set(ref('allowedGroups'), ref(`util.defaultIfNull($${variableToCheck}.${groupsAttribute}, [])`)), + this.setUserGroups(rule.groupClaim), + forEach(ref('userGroup'), ref('userGroups'), [ + iff(raw('$util.isList($allowedGroups)'), iff(raw(`$allowedGroups.contains($userGroup)`), set(ref(variableToSet), raw('true')))), + iff(raw(`$util.isString($allowedGroups)`), iff(raw(`$allowedGroups == $userGroup`), set(ref(variableToSet), raw('true')))), ]) + ); } - - public throwIfSubscriptionUnauthorized(): Expression { - const ifUnauthThrow = iff( - not(parens( - or([ - equals(ref(ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable), raw('true')), - equals(ref(ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable), raw('true')) - ]) - )), raw('$util.unauthorized()') - ) - return block('Throw if unauthorized', [ - ifUnauthThrow, - ]) + // check for group claim here + return block('Dynamic Group Authorization Checks', [set(ref(variableToSet), defaultValue), ...groupAuthorizationExpressions]); + } + + /** + * Given a list of rules return a VTL expression that checks if the given variableToCheck + * statisies at least one of the auth rules. + * @param rules The list of dynamic group authorization rules. + */ + public ownerAuthorizationExpressionForReadOperations( + rules: AuthRule[], + variableToCheck: string = 'ctx.result', + variableToSet: string = ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable, + defaultValue: Expression = raw(`$util.defaultIfNull($${variableToSet}, false)`) + ): Expression { + if (!rules || rules.length === 0) { + return comment(`No Owner Authorization Rules`); } - - public throwIfUnauthorized(field?: FieldDefinitionNode): Expression { - const staticGroupAuthorizedVariable = this.getStaticAuthorizationVariable(field); - const ifUnauthThrow = iff( - not(parens( - or([ - equals(ref(staticGroupAuthorizedVariable), raw('true')), - equals(ref(ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable), raw('true')), - equals(ref(ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable), raw('true')) - ]) - )), raw('$util.unauthorized()') + let ownerAuthorizationExpressions = []; + let ruleNumber = 0; + for (const rule of rules) { + const ownerAttribute = rule.ownerField || DEFAULT_OWNER_FIELD; + const rawUsername = rule.identityField || rule.identityClaim || DEFAULT_IDENTITY_FIELD; + const isUser = isUsername(rawUsername); + const identityAttribute = replaceIfUsername(rawUsername); + const allowedOwnersVariable = `allowedOwners${ruleNumber}`; + ownerAuthorizationExpressions = ownerAuthorizationExpressions.concat( + comment(`Authorization rule: { allow: ${rule.allow}, ownerField: "${ownerAttribute}", identityClaim: "${identityAttribute}" }`), + set(ref(allowedOwnersVariable), ref(`${variableToCheck}.${ownerAttribute}`)), + isUser + ? // tslint:disable-next-line + set( + ref('identityValue'), + raw( + `$util.defaultIfNull($ctx.identity.claims.get("${rawUsername}"), $util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}"))` + ) + ) + : set(ref('identityValue'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${identityAttribute}"), "${NONE_VALUE}")`)), + iff( + raw(`$util.isList($${allowedOwnersVariable})`), + forEach(ref('allowedOwner'), ref(allowedOwnersVariable), [ + iff(raw(`$allowedOwner == $identityValue`), set(ref(variableToSet), raw('true'))), + ]) + ), + iff( + raw(`$util.isString($${allowedOwnersVariable})`), + iff(raw(`$${allowedOwnersVariable} == $identityValue`), set(ref(variableToSet), raw('true'))) ) - return block('Throw if unauthorized', [ - ifUnauthThrow, - ]) + ); + ruleNumber++; } - - // A = IsStaticallyAuthed - // B = AuthConditionIsNotNull - // ! (A OR B) == (!A AND !B) - public throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty(field?: FieldDefinitionNode): Expression { - const staticGroupAuthorizedVariable = this.getStaticAuthorizationVariable(field); - const ifUnauthThrow = iff( - not(parens( - or([ - equals(ref(staticGroupAuthorizedVariable), raw('true')), - parens(raw('$totalAuthExpression != ""')) - ]) - )), raw('$util.unauthorized()') + return block('Owner Authorization Checks', [set(ref(variableToSet), defaultValue), ...ownerAuthorizationExpressions]); + } + + public throwIfSubscriptionUnauthorized(): Expression { + const ifUnauthThrow = iff( + not( + parens( + or([ + equals(ref(ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable), raw('true')), + equals(ref(ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable), raw('true')), + ]) + ) + ), + raw('$util.unauthorized()') + ); + return block('Throw if unauthorized', [ifUnauthThrow]); + } + + public throwIfUnauthorized(field?: FieldDefinitionNode): Expression { + const staticGroupAuthorizedVariable = this.getStaticAuthorizationVariable(field); + const ifUnauthThrow = iff( + not( + parens( + or([ + equals(ref(staticGroupAuthorizedVariable), raw('true')), + equals(ref(ResourceConstants.SNIPPETS.IsDynamicGroupAuthorizedVariable), raw('true')), + equals(ref(ResourceConstants.SNIPPETS.IsOwnerAuthorizedVariable), raw('true')), + ]) ) - return block('Throw if unauthorized', [ - ifUnauthThrow, + ), + raw('$util.unauthorized()') + ); + return block('Throw if unauthorized', [ifUnauthThrow]); + } + + // A = IsStaticallyAuthed + // B = AuthConditionIsNotNull + // ! (A OR B) == (!A AND !B) + public throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty(field?: FieldDefinitionNode): Expression { + const staticGroupAuthorizedVariable = this.getStaticAuthorizationVariable(field); + const ifUnauthThrow = iff( + not(parens(or([equals(ref(staticGroupAuthorizedVariable), raw('true')), parens(raw('$totalAuthExpression != ""'))]))), + raw('$util.unauthorized()') + ); + return block('Throw if unauthorized', [ifUnauthThrow]); + } + + public collectAuthCondition(): Expression { + return block('Collect Auth Condition', [ + set( + ref(ResourceConstants.SNIPPETS.AuthCondition), + raw( + `$util.defaultIfNull($authCondition, ${print( + obj({ + expression: str(''), + expressionNames: obj({}), + expressionValues: obj({}), + }) + )})` + ) + ), + set(ref('totalAuthExpression'), str('')), + comment('Add dynamic group auth conditions if they exist'), + iff( + ref('groupAuthExpressions'), + forEach(ref('authExpr'), ref('groupAuthExpressions'), [ + set(ref('totalAuthExpression'), str(`$totalAuthExpression $authExpr`)), + iff(ref('foreach.hasNext'), set(ref('totalAuthExpression'), str(`$totalAuthExpression OR`))), ]) - } - - public collectAuthCondition(): Expression { - return block('Collect Auth Condition', [ - set( - ref(ResourceConstants.SNIPPETS.AuthCondition), - raw(`$util.defaultIfNull($authCondition, ${print(obj({ - expression: str(""), - expressionNames: obj({}), - expressionValues: obj({}) - }))})`) - ), - set(ref('totalAuthExpression'), str('')), - comment('Add dynamic group auth conditions if they exist'), - iff( - ref('groupAuthExpressions'), - forEach(ref('authExpr'), ref('groupAuthExpressions'), [ - set(ref('totalAuthExpression'), str(`$totalAuthExpression $authExpr`)), - iff(ref('foreach.hasNext'), set(ref('totalAuthExpression'), str(`$totalAuthExpression OR`))) - ]) - ), - iff( - ref('groupAuthExpressionNames'), - raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionNames.putAll($groupAuthExpressionNames))`)), - iff( - ref('groupAuthExpressionValues'), - raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionValues.putAll($groupAuthExpressionValues))`)), - - comment('Add owner auth conditions if they exist'), - iff( - raw(`$totalAuthExpression != "" && $ownerAuthExpressions && $ownerAuthExpressions.size() > 0`), - set(ref('totalAuthExpression'), str(`$totalAuthExpression OR`)) - ), - iff( - ref('ownerAuthExpressions'), - forEach(ref('authExpr'), ref('ownerAuthExpressions'), [ - set(ref('totalAuthExpression'), str(`$totalAuthExpression $authExpr`)), - iff(ref('foreach.hasNext'), set(ref('totalAuthExpression'), str(`$totalAuthExpression OR`))) - ])), - iff( - ref('ownerAuthExpressionNames'), - raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionNames.putAll($ownerAuthExpressionNames))`)), - - iff( - ref('ownerAuthExpressionValues'), - raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionValues.putAll($ownerAuthExpressionValues))`)), - - comment('Set final expression if it has changed.'), - iff( - raw(`$totalAuthExpression != ""`), - ifElse( - raw(`$util.isNullOrEmpty($${ResourceConstants.SNIPPETS.AuthCondition}.expression)`), - set(ref(`${ResourceConstants.SNIPPETS.AuthCondition}.expression`), str(`($totalAuthExpression)`)), - set(ref(`${ResourceConstants.SNIPPETS.AuthCondition}.expression`), str(`$${ResourceConstants.SNIPPETS.AuthCondition}.expression AND ($totalAuthExpression)`)) - ) - ) + ), + iff( + ref('groupAuthExpressionNames'), + raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionNames.putAll($groupAuthExpressionNames))`) + ), + iff( + ref('groupAuthExpressionValues'), + raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionValues.putAll($groupAuthExpressionValues))`) + ), + + comment('Add owner auth conditions if they exist'), + iff( + raw(`$totalAuthExpression != "" && $ownerAuthExpressions && $ownerAuthExpressions.size() > 0`), + set(ref('totalAuthExpression'), str(`$totalAuthExpression OR`)) + ), + iff( + ref('ownerAuthExpressions'), + forEach(ref('authExpr'), ref('ownerAuthExpressions'), [ + set(ref('totalAuthExpression'), str(`$totalAuthExpression $authExpr`)), + iff(ref('foreach.hasNext'), set(ref('totalAuthExpression'), str(`$totalAuthExpression OR`))), ]) - } - - public appendItemIfLocallyAuthorized(): Expression { - return iff( - parens( - or([ - equals(ref(ResourceConstants.SNIPPETS.IsLocalDynamicGroupAuthorizedVariable), raw('true')), - equals(ref(ResourceConstants.SNIPPETS.IsLocalOwnerAuthorizedVariable), raw('true')) - ]) - ), qref('$items.add($item)') + ), + iff( + ref('ownerAuthExpressionNames'), + raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionNames.putAll($ownerAuthExpressionNames))`) + ), + + iff( + ref('ownerAuthExpressionValues'), + raw(`$util.qr($${ResourceConstants.SNIPPETS.AuthCondition}.expressionValues.putAll($ownerAuthExpressionValues))`) + ), + + comment('Set final expression if it has changed.'), + iff( + raw(`$totalAuthExpression != ""`), + ifElse( + raw(`$util.isNullOrEmpty($${ResourceConstants.SNIPPETS.AuthCondition}.expression)`), + set(ref(`${ResourceConstants.SNIPPETS.AuthCondition}.expression`), str(`($totalAuthExpression)`)), + set( + ref(`${ResourceConstants.SNIPPETS.AuthCondition}.expression`), + str(`$${ResourceConstants.SNIPPETS.AuthCondition}.expression AND ($totalAuthExpression)`) + ) ) + ), + ]); + } + + public appendItemIfLocallyAuthorized(): Expression { + return iff( + parens( + or([ + equals(ref(ResourceConstants.SNIPPETS.IsLocalDynamicGroupAuthorizedVariable), raw('true')), + equals(ref(ResourceConstants.SNIPPETS.IsLocalOwnerAuthorizedVariable), raw('true')), + ]) + ), + qref('$items.add($item)') + ); + } + + public setUserGroups(customGroup?: string): Expression { + if (customGroup) { + return compoundExpression([ + set(ref('userGroups'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${customGroup}"), [])`)), + iff( + raw('$util.isString($userGroups)'), + ifElse( + raw('$util.isList($util.parseJson($userGroups))'), + set(ref('userGroups'), raw('$util.parseJson($userGroups)')), + set(ref('userGroups'), raw('[$userGroups]')) + ) + ), + ]); } - - public setUserGroups(customGroup?: string): Expression { - if (customGroup) { - return compoundExpression([ - set(ref('userGroups'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${customGroup}"), [])`)), - iff( - raw('$util.isString($userGroups)'), - ifElse(raw('$util.isList($util.parseJson($userGroups))'), - set(ref('userGroups'), raw('$util.parseJson($userGroups)')), - set(ref('userGroups'), raw('[$userGroups]')) - ) - ), - ]); - } - return set(ref('userGroups'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${DEFAULT_GROUP_CLAIM}"), [])`)); - } - - public generateSubscriptionResolver(fieldName: string, subscriptionTypeName: string = 'Subscription') { - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: "NONE", - FieldName: fieldName, - TypeName: subscriptionTypeName, - RequestMappingTemplate: print( - raw(`{ + return set(ref('userGroups'), raw(`$util.defaultIfNull($ctx.identity.claims.get("${DEFAULT_GROUP_CLAIM}"), [])`)); + } + + public generateSubscriptionResolver(fieldName: string, subscriptionTypeName: string = 'Subscription') { + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: 'NONE', + FieldName: fieldName, + TypeName: subscriptionTypeName, + RequestMappingTemplate: print( + raw(`{ "version": "2018-05-29", "payload": {} }`) - ), - ResponseMappingTemplate: print( - raw(`$util.toJson(null)`) - ) - }); + ), + ResponseMappingTemplate: print(raw(`$util.toJson(null)`)), + }); + } + + public operationCheckExpression(operation: string, field: string) { + return block('Checking for allowed operations which can return this field', [ + set(ref('operation'), raw('$util.defaultIfNull($context.source.operation, "null")')), + ifElse(raw(`$operation == "${operation}"`), ref('util.toJson(null)'), ref(`util.toJson($context.source.${field})`)), + ]); + } + + public setOperationExpression(operation: string): string { + return print(block('Setting the operation', [set(ref('context.result.operation'), str(operation))])); + } + + public getAuthModeCheckWrappedExpression(expectedAuthModes: Set, expression: Expression): Expression { + if (!expectedAuthModes || expectedAuthModes.size === 0) { + return expression; } - public operationCheckExpression(operation: string, field: string) { - return block('Checking for allowed operations which can return this field', [ - set(ref('operation'), raw('$util.defaultIfNull($context.source.operation, "null")')), - ifElse( - raw(`$operation == "${operation}"`), - ref('util.toJson(null)'), - ref(`util.toJson($context.source.${field})`), - ) - ]) - } + const conditions = []; - public setOperationExpression(operation: string): string { - return print(block('Setting the operation', [ - set(ref('context.result.operation'), str(operation)) - ])) + for (const expectedAuthMode of expectedAuthModes) { + conditions.push(equals(ref(ResourceConstants.SNIPPETS.AuthMode), str(`${expectedAuthMode}`))); } - public getAuthModeCheckWrappedExpression(expectedAuthModes: Set, expression: Expression): Expression { - if (!expectedAuthModes || expectedAuthModes.size === 0) { - return expression; - } + return block('Check authMode and execute owner/group checks', [ + iff(conditions.length === 1 ? conditions[0] : or(conditions), expression), + ]); + } - const conditions = []; + public getAuthModeDeterminationExpression(authProviders: Set): Expression { + if (!authProviders || authProviders.size === 0) { + return comment(`No authentication mode determination needed`); + } - for (const expectedAuthMode of expectedAuthModes) { - conditions.push(equals( - ref(ResourceConstants.SNIPPETS.AuthMode), - str(`${expectedAuthMode}`) - )); + const expressions = []; + + for (const authProvider of authProviders) { + if (authProvider === 'userPools') { + const userPoolsExpression = iff( + and([ + raw(`$util.isNullOrEmpty($${ResourceConstants.SNIPPETS.AuthMode})`), + not(raw(`$util.isNull($ctx.identity)`)), + not(raw(`$util.isNull($ctx.identity.sub)`)), + not(raw(`$util.isNull($ctx.identity.issuer)`)), + not(raw(`$util.isNull($ctx.identity.username)`)), + not(raw(`$util.isNull($ctx.identity.claims)`)), + not(raw(`$util.isNull($ctx.identity.sourceIp)`)), + not(raw(`$util.isNull($ctx.identity.defaultAuthStrategy)`)), + ]), + set(ref(ResourceConstants.SNIPPETS.AuthMode), str(`userPools`)) + ); + + expressions.push(userPoolsExpression); + } else if (authProvider === 'oidc') { + const oidcExpression = iff( + and([ + raw(`$util.isNullOrEmpty($${ResourceConstants.SNIPPETS.AuthMode})`), + not(raw(`$util.isNull($ctx.identity)`)), + not(raw(`$util.isNull($ctx.identity.sub)`)), + not(raw(`$util.isNull($ctx.identity.issuer)`)), + not(raw(`$util.isNull($ctx.identity.claims)`)), + raw(`$util.isNull($ctx.identity.username)`), + raw(`$util.isNull($ctx.identity.sourceIp)`), + raw(`$util.isNull($ctx.identity.defaultAuthStrategy)`), + ]), + set(ref(ResourceConstants.SNIPPETS.AuthMode), str(`oidc`)) + ); + + if (expressions.length > 0) { + expressions.push(newline()); } - return block("Check authMode and execute owner/group checks", [ - iff( - conditions.length === 1 ? conditions[0] : or (conditions), - expression - ) - ]); + expressions.push(oidcExpression); + } } - public getAuthModeDeterminationExpression(authProviders: Set): Expression { - if (!authProviders || authProviders.size === 0) { - return comment(`No authentication mode determination needed`); - } - - const expressions = []; - - for (const authProvider of authProviders) { - - if (authProvider === 'userPools') { - const userPoolsExpression = iff( - and([ - raw(`$util.isNullOrEmpty($${ResourceConstants.SNIPPETS.AuthMode})`), - not(raw(`$util.isNull($ctx.identity)`)), - not(raw(`$util.isNull($ctx.identity.sub)`)), - not(raw(`$util.isNull($ctx.identity.issuer)`)), - not(raw(`$util.isNull($ctx.identity.username)`)), - not(raw(`$util.isNull($ctx.identity.claims)`)), - not(raw(`$util.isNull($ctx.identity.sourceIp)`)), - not(raw(`$util.isNull($ctx.identity.defaultAuthStrategy)`)), - ]), - set(ref(ResourceConstants.SNIPPETS.AuthMode), str(`userPools`)) - ); - - expressions.push(userPoolsExpression); - - } else if (authProvider === 'oidc') { - const oidcExpression = iff( - and([ - raw(`$util.isNullOrEmpty($${ResourceConstants.SNIPPETS.AuthMode})`), - not(raw(`$util.isNull($ctx.identity)`)), - not(raw(`$util.isNull($ctx.identity.sub)`)), - not(raw(`$util.isNull($ctx.identity.issuer)`)), - not(raw(`$util.isNull($ctx.identity.claims)`)), - raw(`$util.isNull($ctx.identity.username)`), - raw(`$util.isNull($ctx.identity.sourceIp)`), - raw(`$util.isNull($ctx.identity.defaultAuthStrategy)`), - ]), - set(ref(ResourceConstants.SNIPPETS.AuthMode), str(`oidc`)) - ); - - if (expressions.length > 0) { - expressions.push(newline()); - } - - expressions.push(oidcExpression); - } - } + return block('Determine request authentication mode', expressions); + } - return block("Determine request authentication mode", expressions); - } + public getStaticAuthorizationVariable(field: FieldDefinitionNode): string { + return field + ? `${field.name.value}_${ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable}` + : ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable; + } - public getStaticAuthorizationVariable(field: FieldDefinitionNode): string { - return field ? `${field.name.value}_${ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable}` : - ResourceConstants.SNIPPETS.IsStaticGroupAuthorizedVariable; - } + public makeIAMPolicyForRole(isAuthPolicy: Boolean, resources: Set): Policy { + const authPiece = isAuthPolicy ? 'auth' : 'unauth'; + const policyResources: object[] = []; - public makeIAMPolicyForRole(isAuthPolicy: Boolean, resources: Set): Policy { - const authPiece = isAuthPolicy ? "auth" : "unauth"; - const policyResources: object[] = []; - - for (const resource of resources) { - // We always have 2 parts, no need to check - const resourceParts = resource.split("/"); - - if (resourceParts[1] !== "null") { - policyResources.push ( - Fn.Sub( - 'arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/fields/${fieldName}', - { - apiId: { - "Fn::GetAtt": [ - "GraphQLAPI", - "ApiId" - ] - }, - typeName: resourceParts[0], - fieldName: resourceParts[1] - } - ) - ); - } else { - policyResources.push ( - Fn.Sub( - 'arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/*', - { - apiId: { - "Fn::GetAtt": [ - "GraphQLAPI", - "ApiId" - ] - }, - typeName: resourceParts[0] - } - ) - ); - } - } + for (const resource of resources) { + // We always have 2 parts, no need to check + const resourceParts = resource.split('/'); - return new IAM.Policy({ - PolicyName: `appsync-${authPiece}role-policy`, - Roles: [ - //HACK double casting needed because it cannot except Ref - { Ref: `${authPiece}RoleName` } as unknown as Value - ], - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'appsync:GraphQL' - ], - Resource: policyResources, - }], + if (resourceParts[1] !== 'null') { + policyResources.push( + Fn.Sub('arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/fields/${fieldName}', { + apiId: { + 'Fn::GetAtt': ['GraphQLAPI', 'ApiId'], }, - }); - } - - /** - * ES EXPRESSIONS - */ - - public makeESItemsExpression() { - // generate es expresion to appsync - return compoundExpression([ - set(ref('es_items'), list([])), - forEach( - ref('entry'), - ref('context.result.hits.hits'), - [ - iff( - raw('!$foreach.hasNext'), - set(ref('nextToken'), ref('entry.sort.get(0)')) - ), - qref('$es_items.add($entry.get("_source"))') - ] - ), - ]) + typeName: resourceParts[0], + fieldName: resourceParts[1], + }) + ); + } else { + policyResources.push( + Fn.Sub('arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/*', { + apiId: { + 'Fn::GetAtt': ['GraphQLAPI', 'ApiId'], + }, + typeName: resourceParts[0], + }) + ); + } } - public makeESToGQLExpression() { - return compoundExpression([ - set(ref('es_response'), obj({ - "items": ref('es_items'), - "total": ref('ctx.result.hits.total') - })), - iff( - raw('$es_items.size() > 0'), - qref('$es_response.put("nextToken", $nextToken)') - ), - toJson(ref('es_response')) - ]) - } + return new IAM.Policy({ + PolicyName: `appsync-${authPiece}role-policy`, + Roles: [ + //HACK double casting needed because it cannot except Ref + ({ Ref: `${authPiece}RoleName` } as unknown) as Value, + ], + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['appsync:GraphQL'], + Resource: policyResources, + }, + ], + }, + }); + } + + /** + * ES EXPRESSIONS + */ + + public makeESItemsExpression() { + // generate es expresion to appsync + return compoundExpression([ + set(ref('es_items'), list([])), + forEach(ref('entry'), ref('context.result.hits.hits'), [ + iff(raw('!$foreach.hasNext'), set(ref('nextToken'), ref('entry.sort.get(0)'))), + qref('$es_items.add($entry.get("_source"))'), + ]), + ]); + } + + public makeESToGQLExpression() { + return compoundExpression([ + set( + ref('es_response'), + obj({ + items: ref('es_items'), + total: ref('ctx.result.hits.total'), + }) + ), + iff(raw('$es_items.size() > 0'), qref('$es_response.put("nextToken", $nextToken)')), + toJson(ref('es_response')), + ]); + } } diff --git a/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts b/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts index 4d2d4e3dda..876d9f3c88 100644 --- a/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts +++ b/packages/graphql-connection-transformer/src/__tests__/ModelConnectionTransformer.test.ts @@ -1,15 +1,20 @@ import { - ObjectTypeDefinitionNode, parse, FieldDefinitionNode, DocumentNode, - DefinitionNode, Kind, InputObjectTypeDefinitionNode, - InputValueDefinitionNode -} from 'graphql' -import GraphQLTransform, { InvalidDirectiveError } from 'graphql-transformer-core' -import { ResourceConstants, ResolverResourceIDs, ModelResourceIDs } from 'graphql-transformer-common' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import { ModelConnectionTransformer } from '../ModelConnectionTransformer' + ObjectTypeDefinitionNode, + parse, + FieldDefinitionNode, + DocumentNode, + DefinitionNode, + Kind, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, +} from 'graphql'; +import GraphQLTransform, { InvalidDirectiveError } from 'graphql-transformer-core'; +import { ResourceConstants, ResolverResourceIDs, ModelResourceIDs } from 'graphql-transformer-common'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import { ModelConnectionTransformer } from '../ModelConnectionTransformer'; test('Test ModelConnectionTransformer simple one to many happy case', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -19,40 +24,37 @@ test('Test ModelConnectionTransformer simple one to many happy case', () => { id: ID! content: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - - // Post.comments field - const postType = getObjectType(schemaDoc, 'Post') - expectFields(postType, ['comments']) - const commentField = postType.fields.find(f => f.name.value === 'comments') - expect(commentField.arguments.length).toEqual(4) - expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']) - expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE) - expect((commentField.type as any).name.value).toEqual('ModelCommentConnection') - - // Check the Comment.commentPostId - // Check the Comment.commentPostId inputs - const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')) - const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postCommentsId') - expect(connectionId).toBeTruthy() - - const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')) - const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postCommentsId') - expect(connectionUpdateId).toBeTruthy() + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); + const schemaDoc = parse(out.schema); + + // Post.comments field + const postType = getObjectType(schemaDoc, 'Post'); + expectFields(postType, ['comments']); + const commentField = postType.fields.find(f => f.name.value === 'comments'); + expect(commentField.arguments.length).toEqual(4); + expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']); + expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE); + expect((commentField.type as any).name.value).toEqual('ModelCommentConnection'); + + // Check the Comment.commentPostId + // Check the Comment.commentPostId inputs + const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')); + const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postCommentsId'); + expect(connectionId).toBeTruthy(); + + const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')); + const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postCommentsId'); + expect(connectionUpdateId).toBeTruthy(); }); test('Test ModelConnectionTransformer simple one to many happy case with custom keyField', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -62,40 +64,37 @@ test('Test ModelConnectionTransformer simple one to many happy case with custom id: ID! content: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - - // Post.comments field - const postType = getObjectType(schemaDoc, 'Post') - expectFields(postType, ['comments']) - const commentField = postType.fields.find(f => f.name.value === 'comments') - expect(commentField.arguments.length).toEqual(4) - expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']) - expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE) - expect((commentField.type as any).name.value).toEqual('ModelCommentConnection') - - // Check the Comment.commentPostId - // Check the Comment.commentPostId inputs - const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')) - const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postId') - expect(connectionId).toBeTruthy() - - const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')) - const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postId') - expect(connectionUpdateId).toBeTruthy() + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); + const schemaDoc = parse(out.schema); + + // Post.comments field + const postType = getObjectType(schemaDoc, 'Post'); + expectFields(postType, ['comments']); + const commentField = postType.fields.find(f => f.name.value === 'comments'); + expect(commentField.arguments.length).toEqual(4); + expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']); + expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE); + expect((commentField.type as any).name.value).toEqual('ModelCommentConnection'); + + // Check the Comment.commentPostId + // Check the Comment.commentPostId inputs + const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')); + const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postId'); + expect(connectionId).toBeTruthy(); + + const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')); + const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postId'); + expect(connectionUpdateId).toBeTruthy(); }); test('Test ModelConnectionTransformer simple one to many happy case with custom keyField', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -108,42 +107,39 @@ test('Test ModelConnectionTransformer simple one to many happy case with custom content: String! post: Post! @connection(name: "PostComments", keyField: "postId") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - - // Post.comments field - const postType = getObjectType(schemaDoc, 'Post') - expectFields(postType, ['comments']) - const commentField = postType.fields.find(f => f.name.value === 'comments') - expect(commentField.arguments.length).toEqual(4) - expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']) - expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE) - expect((commentField.type as any).name.value).toEqual('ModelCommentConnection') - - // Check the Comment.commentPostId - // Check the Comment.commentPostId inputs - const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')) - const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postId') - expect(connectionId).toBeTruthy() - expect(connectionId.type.kind).toEqual(Kind.NON_NULL_TYPE) - - const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')) - const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postId') - expect(connectionUpdateId).toBeTruthy() - expect(connectionUpdateId.type.kind).toEqual(Kind.NAMED_TYPE) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); + const schemaDoc = parse(out.schema); + + // Post.comments field + const postType = getObjectType(schemaDoc, 'Post'); + expectFields(postType, ['comments']); + const commentField = postType.fields.find(f => f.name.value === 'comments'); + expect(commentField.arguments.length).toEqual(4); + expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']); + expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE); + expect((commentField.type as any).name.value).toEqual('ModelCommentConnection'); + + // Check the Comment.commentPostId + // Check the Comment.commentPostId inputs + const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')); + const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postId'); + expect(connectionId).toBeTruthy(); + expect(connectionId.type.kind).toEqual(Kind.NON_NULL_TYPE); + + const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')); + const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postId'); + expect(connectionUpdateId).toBeTruthy(); + expect(connectionUpdateId.type.kind).toEqual(Kind.NAMED_TYPE); }); test('Test ModelConnectionTransformer complex one to many happy case', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -154,47 +150,44 @@ test('Test ModelConnectionTransformer complex one to many happy case', () => { content: String post: Post @connection(name: "PostComments") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'post')]).toBeTruthy() - const schemaDoc = parse(out.schema) - const postType = getObjectType(schemaDoc, 'Post') - const commentType = getObjectType(schemaDoc, 'Comment') - - // Check Post.comments field - expectFields(postType, ['comments']) - const commentField = postType.fields.find(f => f.name.value === 'comments') - expect(commentField.arguments.length).toEqual(4) - expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']) - expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE) - expect((commentField.type as any).name.value).toEqual('ModelCommentConnection') - - // Check the Comment.commentPostId inputs - const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')) - const connectionId = commentCreateInput.fields.find(f => f.name.value === 'commentPostId') - expect(connectionId).toBeTruthy() - - const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')) - const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'commentPostId') - expect(connectionUpdateId).toBeTruthy() - - // Check Comment.post field - const postField = commentType.fields.find(f => f.name.value === 'post') - expect(postField.arguments.length).toEqual(0) - expect(postField.type.kind).toEqual(Kind.NAMED_TYPE) - expect((postField.type as any).name.value).toEqual('Post') + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'post')]).toBeTruthy(); + const schemaDoc = parse(out.schema); + const postType = getObjectType(schemaDoc, 'Post'); + const commentType = getObjectType(schemaDoc, 'Comment'); + + // Check Post.comments field + expectFields(postType, ['comments']); + const commentField = postType.fields.find(f => f.name.value === 'comments'); + expect(commentField.arguments.length).toEqual(4); + expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']); + expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE); + expect((commentField.type as any).name.value).toEqual('ModelCommentConnection'); + + // Check the Comment.commentPostId inputs + const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')); + const connectionId = commentCreateInput.fields.find(f => f.name.value === 'commentPostId'); + expect(connectionId).toBeTruthy(); + + const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')); + const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'commentPostId'); + expect(connectionUpdateId).toBeTruthy(); + + // Check Comment.post field + const postField = commentType.fields.find(f => f.name.value === 'post'); + expect(postField.arguments.length).toEqual(0); + expect(postField.type.kind).toEqual(Kind.NAMED_TYPE); + expect((postField.type as any).name.value).toEqual('Post'); }); test('Test ModelConnectionTransformer many to many should fail', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -205,25 +198,22 @@ test('Test ModelConnectionTransformer many to many should fail', () => { content: String posts: [Post] @connection(name: "ManyToMany") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - try { - transformer.transform(validSchema); - expect(true).toEqual(false) - } catch (e) { - // Should throw bc we don't let support many to many - expect(e).toBeTruthy() - expect(e.name).toEqual('InvalidDirectiveError') - } + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + try { + transformer.transform(validSchema); + expect(true).toEqual(false); + } catch (e) { + // Should throw bc we don't let support many to many + expect(e).toBeTruthy(); + expect(e.name).toEqual('InvalidDirectiveError'); + } }); test('Test ModelConnectionTransformer many to many should fail due to missing other "name"', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -236,25 +226,22 @@ test('Test ModelConnectionTransformer many to many should fail due to missing ot # This is meant to be the other half of "ManyToMany" but I forgot. posts: [Post] @connection } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - try { - transformer.transform(validSchema); - expect(true).toEqual(false) - } catch (e) { - // Should throw bc we check both halves when name is given - expect(e).toBeTruthy() - expect(e.name).toEqual('InvalidDirectiveError') - } + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + try { + transformer.transform(validSchema); + expect(true).toEqual(false); + } catch (e) { + // Should throw bc we check both halves when name is given + expect(e).toBeTruthy(); + expect(e.name).toEqual('InvalidDirectiveError'); + } }); test('Test ModelConnectionTransformer many to many should fail due to missing other "name"', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! things: [Thing!] @connection @@ -263,29 +250,26 @@ test('Test ModelConnectionTransformer many to many should fail due to missing ot type Thing @model(queries: null, mutations: null) { id: ID! } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'things')]).toBeTruthy() - const schemaDoc = parse(out.schema) - const postType = getObjectType(schemaDoc, 'Post') - const postConnection = getObjectType(schemaDoc, 'ModelPostConnection') - const thingConnection = getObjectType(schemaDoc, 'ModelThingConnection') - const thingFilterInput = getInputType(schemaDoc, 'ModelThingFilterInput') - expect(thingFilterInput).toBeDefined() - expect(postType).toBeDefined() - expect(thingConnection).toBeDefined() - expect(postConnection).toBeDefined() + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'things')]).toBeTruthy(); + const schemaDoc = parse(out.schema); + const postType = getObjectType(schemaDoc, 'Post'); + const postConnection = getObjectType(schemaDoc, 'ModelPostConnection'); + const thingConnection = getObjectType(schemaDoc, 'ModelThingConnection'); + const thingFilterInput = getInputType(schemaDoc, 'ModelThingFilterInput'); + expect(thingFilterInput).toBeDefined(); + expect(postType).toBeDefined(); + expect(thingConnection).toBeDefined(); + expect(postConnection).toBeDefined(); }); test('Test ModelConnectionTransformer with non null @connections', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -308,53 +292,49 @@ test('Test ModelConnectionTransformer with non null @connections', () => { # A non-null on the one in 1-M again enforces a non null. post: Post! @connection(name: "PostComments", keyField: "postId") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - - // Post.comments field - const postType = getObjectType(schemaDoc, 'Post') - expectFields(postType, ['comments']) - const commentField = postType.fields.find(f => f.name.value === 'comments') - expect(commentField.arguments.length).toEqual(4) - expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']) - expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE) - expect((commentField.type as any).name.value).toEqual('ModelCommentConnection') - - // Check the Comment.commentPostId - // Check the Comment.commentPostId inputs - const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')) - const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postId') - expect(connectionId).toBeTruthy() - expect(connectionId.type.kind).toEqual(Kind.NON_NULL_TYPE) - - const manyCommentId = commentCreateInput.fields.find(f => f.name.value === 'postManyCommentsId') - expect(manyCommentId).toBeTruthy() - expect(manyCommentId.type.kind).toEqual(Kind.NAMED_TYPE) - - const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')) - const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postId') - expect(connectionUpdateId).toBeTruthy() - expect(connectionUpdateId.type.kind).toEqual(Kind.NAMED_TYPE) - - - // Check the post create type - const postCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Post')) - const postConnectionId = postCreateInput.fields.find(f => f.name.value === 'postSingleCommentId') - expect(postConnectionId).toBeTruthy() - expect(postConnectionId.type.kind).toEqual(Kind.NON_NULL_TYPE) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); + const schemaDoc = parse(out.schema); + + // Post.comments field + const postType = getObjectType(schemaDoc, 'Post'); + expectFields(postType, ['comments']); + const commentField = postType.fields.find(f => f.name.value === 'comments'); + expect(commentField.arguments.length).toEqual(4); + expectArguments(commentField, ['filter', 'limit', 'nextToken', 'sortDirection']); + expect(commentField.type.kind).toEqual(Kind.NAMED_TYPE); + expect((commentField.type as any).name.value).toEqual('ModelCommentConnection'); + + // Check the Comment.commentPostId + // Check the Comment.commentPostId inputs + const commentCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Comment')); + const connectionId = commentCreateInput.fields.find(f => f.name.value === 'postId'); + expect(connectionId).toBeTruthy(); + expect(connectionId.type.kind).toEqual(Kind.NON_NULL_TYPE); + + const manyCommentId = commentCreateInput.fields.find(f => f.name.value === 'postManyCommentsId'); + expect(manyCommentId).toBeTruthy(); + expect(manyCommentId.type.kind).toEqual(Kind.NAMED_TYPE); + + const commentUpdateInput = getInputType(schemaDoc, ModelResourceIDs.ModelUpdateInputObjectName('Comment')); + const connectionUpdateId = commentUpdateInput.fields.find(f => f.name.value === 'postId'); + expect(connectionUpdateId).toBeTruthy(); + expect(connectionUpdateId.type.kind).toEqual(Kind.NAMED_TYPE); + + // Check the post create type + const postCreateInput = getInputType(schemaDoc, ModelResourceIDs.ModelCreateInputObjectName('Post')); + const postConnectionId = postCreateInput.fields.find(f => f.name.value === 'postSingleCommentId'); + expect(postConnectionId).toBeTruthy(); + expect(postConnectionId.type.kind).toEqual(Kind.NON_NULL_TYPE); }); test('Test ModelConnectionTransformer with sortField fails if not specified in associated type', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -365,18 +345,17 @@ test('Test ModelConnectionTransformer with sortField fails if not specified in a content: String post: Post @connection(name: "PostComments") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - expect(() => { transformer.transform(validSchema) }).toThrowError() + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + expect(() => { + transformer.transform(validSchema); + }).toThrowError(); }); test('Test ModelConnectionTransformer with sortField creates a connection resolver with a sort key condition.', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -388,35 +367,29 @@ test('Test ModelConnectionTransformer with sortField creates a connection resolv post: Post @connection(name: "PostComments", sortField: "createdAt") createdAt: AWSDateTime } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - - // Post.comments field - const postType = getObjectType(schemaDoc, 'Post') - expectFields(postType, ['comments']) - const commentField = postType.fields.find(f => f.name.value === 'comments') - expect(commentField.arguments.length).toEqual(5) - expectArguments(commentField, ['createdAt', 'filter', 'limit', 'nextToken', 'sortDirection']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); + const schemaDoc = parse(out.schema); + + // Post.comments field + const postType = getObjectType(schemaDoc, 'Post'); + expectFields(postType, ['comments']); + const commentField = postType.fields.find(f => f.name.value === 'comments'); + expect(commentField.arguments.length).toEqual(5); + expectArguments(commentField, ['createdAt', 'filter', 'limit', 'nextToken', 'sortDirection']); }); test('Test ModelConnectionTransformer throws with invalid key fields', () => { - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - - const invalidSchema = ` + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + + const invalidSchema = ` type Post @model { id: ID! title: String! @@ -429,10 +402,10 @@ test('Test ModelConnectionTransformer throws with invalid key fields', () => { # Key fields must be String or ID. postId: [String] } - ` - expect(() => transformer.transform(invalidSchema)).toThrow(); + `; + expect(() => transformer.transform(invalidSchema)).toThrow(); - const invalidSchema2 = ` + const invalidSchema2 = ` type Post @model { id: ID! title: String! @@ -447,10 +420,10 @@ test('Test ModelConnectionTransformer throws with invalid key fields', () => { post: Post @connection(name: "PostComments", keyField: "postId") } - ` - expect(() => transformer.transform(invalidSchema2)).toThrow(); + `; + expect(() => transformer.transform(invalidSchema2)).toThrow(); - const invalidSchema3 = ` + const invalidSchema3 = ` type Post @model { id: ID! title: String! @@ -464,19 +437,16 @@ test('Test ModelConnectionTransformer throws with invalid key fields', () => { post: Post @connection(keyField: "postId") } - ` - expect(() => transformer.transform(invalidSchema3)).toThrow(); -}) + `; + expect(() => transformer.transform(invalidSchema3)).toThrow(); +}); test('Test ModelConnectionTransformer does not throw with valid key fields', () => { - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - - const validSchema = ` + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + + const validSchema = ` type Post @model { id: ID! title: String! @@ -489,10 +459,10 @@ test('Test ModelConnectionTransformer does not throw with valid key fields', () # Key fields must be String or ID. postId: String } - ` - expect(() => transformer.transform(validSchema)).toBeTruthy(); + `; + expect(() => transformer.transform(validSchema)).toBeTruthy(); - const validSchema2 = ` + const validSchema2 = ` type Post @model { id: ID! title: String! @@ -507,10 +477,10 @@ test('Test ModelConnectionTransformer does not throw with valid key fields', () post: Post @connection(name: "PostComments", keyField: "postId") } - ` - expect(() => transformer.transform(validSchema2)).toBeTruthy(); + `; + expect(() => transformer.transform(validSchema2)).toBeTruthy(); - const validSchema3 = ` + const validSchema3 = ` type Post @model { id: ID! title: String! @@ -524,12 +494,12 @@ test('Test ModelConnectionTransformer does not throw with valid key fields', () post: Post @connection(keyField: "postId") } - ` - expect(() => transformer.transform(validSchema3)).toBeTruthy(); -}) + `; + expect(() => transformer.transform(validSchema3)).toBeTruthy(); +}); test('Test ModelConnectionTransformer sortField with missing @key should fail', () => { - const validSchema = ` + const validSchema = ` type Model1 @model(subscriptions: null) { id: ID! @@ -542,26 +512,23 @@ test('Test ModelConnectionTransformer sortField with missing @key should fail', connection: Model1 @connection(sortField: "modelOneSort") modelOneSort: Int! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - - try { - transformer.transform(validSchema); - expect(true).toEqual(false) - } catch (e) { - expect(e).toBeTruthy() - expect(e.name).toEqual('InvalidDirectiveError') - } + `; + + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + + try { + transformer.transform(validSchema); + expect(true).toEqual(false); + } catch (e) { + expect(e).toBeTruthy(); + expect(e.name).toEqual('InvalidDirectiveError'); + } }); test('Test ModelConnectionTransformer overrides the default limit', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -571,25 +538,22 @@ test('Test ModelConnectionTransformer overrides the default limit', () => { id: ID! content: String } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); - // Post.comments field - expect(out.resolvers['Post.comments.req.vtl']).toContain('#set( $limit = $util.defaultIfNull($context.args.limit, 50) )'); + // Post.comments field + expect(out.resolvers['Post.comments.req.vtl']).toContain('#set( $limit = $util.defaultIfNull($context.args.limit, 50) )'); }); test('Test ModelConnectionTransformer uses the default limit', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -599,55 +563,50 @@ test('Test ModelConnectionTransformer uses the default limit', () => { id: ID! content: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - - // Post.comments field - expect(out.resolvers['Post.comments.req.vtl']).toContain('#set( $limit = $util.defaultIfNull($context.args.limit, 10) )'); + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy(); + + // Post.comments field + expect(out.resolvers['Post.comments.req.vtl']).toContain('#set( $limit = $util.defaultIfNull($context.args.limit, 10) )'); }); function expectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } function expectArguments(field: FieldDefinitionNode, args: string[]) { - for (const argName of args) { - const foundArg = field.arguments.find((a: InputValueDefinitionNode) => a.name.value === argName) - expect(foundArg).toBeDefined() - } + for (const argName of args) { + const foundArg = field.arguments.find((a: InputValueDefinitionNode) => a.name.value === argName); + expect(foundArg).toBeDefined(); + } } function doNotExpectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - expect( - type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - ).toBeUndefined() - } + for (const fieldName of fields) { + expect(type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName)).toBeUndefined(); + } } function getObjectType(doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type - ) as ObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; } function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type - ) as InputObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type) as + | InputObjectTypeDefinitionNode + | undefined; } function verifyInputCount(doc: DocumentNode, type: string, count: number): boolean { - return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length === count; + return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length === count; } diff --git a/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts b/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts index 6ffa5f2a0d..59e37a17d8 100644 --- a/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts +++ b/packages/graphql-connection-transformer/src/__tests__/NewConnectionTransformer.test.ts @@ -1,16 +1,21 @@ import { - ObjectTypeDefinitionNode, parse, FieldDefinitionNode, DocumentNode, - DefinitionNode, Kind, InputObjectTypeDefinitionNode, - InputValueDefinitionNode -} from 'graphql' -import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core' -import { ResourceConstants, ResolverResourceIDs, ModelResourceIDs } from 'graphql-transformer-common' -import { ModelConnectionTransformer } from '../ModelConnectionTransformer' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import KeyTransformer from 'graphql-key-transformer' + ObjectTypeDefinitionNode, + parse, + FieldDefinitionNode, + DocumentNode, + DefinitionNode, + Kind, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, +} from 'graphql'; +import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core'; +import { ResourceConstants, ResolverResourceIDs, ModelResourceIDs } from 'graphql-transformer-common'; +import { ModelConnectionTransformer } from '../ModelConnectionTransformer'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import KeyTransformer from 'graphql-key-transformer'; test('ModelConnectionTransformer should fail if connection was called on an object that is not a Model type.', () => { - const validSchema = ` + const validSchema = ` type Test { id: ID! email: String! @@ -21,20 +26,17 @@ test('ModelConnectionTransformer should fail if connection was called on an obje id: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError(`@connection must be on an @model object type field.`); -}) + expect(() => transformer.transform(validSchema)).toThrowError(`@connection must be on an @model object type field.`); +}); test('ModelConnectionTransformer should fail if connection was with an object that is not a Model type.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -45,20 +47,17 @@ test('ModelConnectionTransformer should fail if connection was with an object th id: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError(`Object type Test1 must be annotated with @model.`); -}) + expect(() => transformer.transform(validSchema)).toThrowError(`Object type Test1 must be annotated with @model.`); +}); test('ModelConnectionTransformer should fail if the field type where the directive is called is incorrect.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -69,20 +68,17 @@ test('ModelConnectionTransformer should fail if the field type where the directi id: iD! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError('Type "Test2" not found in document.'); -}) + expect(() => transformer.transform(validSchema)).toThrowError('Type "Test2" not found in document.'); +}); test('ModelConnectionTransformer should fail if an empty list of fields is passed in.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String @@ -93,20 +89,17 @@ test('ModelConnectionTransformer should fail if an empty list of fields is passe id: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError('No fields passed in to @connection directive.'); -}) + expect(() => transformer.transform(validSchema)).toThrowError('No fields passed in to @connection directive.'); +}); test('ModelConnectionTransformer should fail if any of the fields passed in are not in the Parent model.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String @@ -121,22 +114,17 @@ test('ModelConnectionTransformer should fail if any of the fields passed in are friendID: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError('name is not a field in Test'); -}) + expect(() => transformer.transform(validSchema)).toThrowError('name is not a field in Test'); +}); -test('ModelConnectionTransformer should fail if the query is not run on the default table when connection is trying to connect a single object.', - () => { - const validSchema = ` +test('ModelConnectionTransformer should fail if the query is not run on the default table when connection is trying to connect a single object.', () => { + const validSchema = ` type Test @model { id: ID! email: String @@ -151,24 +139,19 @@ test('ModelConnectionTransformer should fail if the query is not run on the defa friendID: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - expect(() => - transformer.transform(validSchema)).toThrowError( - 'Connection is to a single object but the keyName notDefault was provided which does not reference the default table.' - ) -}) + expect(() => transformer.transform(validSchema)).toThrowError( + 'Connection is to a single object but the keyName notDefault was provided which does not reference the default table.' + ); +}); test('ModelConnectionTransformer should fail if keyName provided does not exist.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String @@ -180,21 +163,17 @@ test('ModelConnectionTransformer should fail if keyName provided does not exist. friendID: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); - expect(() => - transformer.transform(validSchema)).toThrowError('Key notDefault does not exist for model Test1') -}) + expect(() => transformer.transform(validSchema)).toThrowError('Key notDefault does not exist for model Test1'); +}); test('ModelConnectionTransformer should fail if first field does not match PK of table. (When using default table)', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -206,21 +185,17 @@ test('ModelConnectionTransformer should fail if first field does not match PK of friendID: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer()], + }); - expect(() => - transformer.transform(validSchema)).toThrowError('email field is not of type ID') -}) + expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); +}); test('ModelConnectionTransformer should fail if sort key type passed in does not match default table sort key type.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -235,21 +210,17 @@ test('ModelConnectionTransformer should fail if sort key type passed in does not friendID: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); -}) + expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); +}); test('ModelConnectionTransformer should fail if sort key type passed in does not match custom index sort key type.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -264,21 +235,17 @@ test('ModelConnectionTransformer should fail if sort key type passed in does not friendID: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); -}) + expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); +}); test('ModelConnectionTransformer should fail if partition key type passed in does not match custom index partition key type.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -293,21 +260,17 @@ test('ModelConnectionTransformer should fail if partition key type passed in doe friendID: ID! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); -}) + expect(() => transformer.transform(validSchema)).toThrowError('email field is not of type ID'); +}); test('Test ModelConnectionTransformer for One-to-One getItem case.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -322,29 +285,25 @@ test('Test ModelConnectionTransformer for One-to-One getItem case.', () => { friendID: ID! email: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - const out = transformer.transform(validSchema); - expect(out).toBeDefined(); - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherHalf')]).toBeTruthy(); - const schemaDoc = parse(out.schema); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherHalf')]).toBeTruthy(); + const schemaDoc = parse(out.schema); - const testObjType = getObjectType(schemaDoc, 'Test'); - expectFields(testObjType, ['otherHalf']); - const relatedField = testObjType.fields.find(f => f.name.value === 'otherHalf'); - expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); -}) + const testObjType = getObjectType(schemaDoc, 'Test'); + expectFields(testObjType, ['otherHalf']); + const relatedField = testObjType.fields.find(f => f.name.value === 'otherHalf'); + expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); +}); test('Test ModelConnectionTransformer for One-to-Many query case.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -359,35 +318,30 @@ test('Test ModelConnectionTransformer for One-to-Many query case.', () => { friendID: ID! email: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - const out = transformer.transform(validSchema); - expect(out).toBeDefined(); - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherParts')]).toBeTruthy(); - const schemaDoc = parse(out.schema); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherParts')]).toBeTruthy(); + const schemaDoc = parse(out.schema); - const testObjType = getObjectType(schemaDoc, 'Test'); - expectFields(testObjType, ['otherParts']); - const relatedField = testObjType.fields.find(f => f.name.value === 'otherParts'); + const testObjType = getObjectType(schemaDoc, 'Test'); + expectFields(testObjType, ['otherParts']); + const relatedField = testObjType.fields.find(f => f.name.value === 'otherParts'); - expect(relatedField.arguments.length).toEqual(4); - expectArguments(relatedField, ['filter', 'limit', 'nextToken', 'sortDirection']); - expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); + expect(relatedField.arguments.length).toEqual(4); + expectArguments(relatedField, ['filter', 'limit', 'nextToken', 'sortDirection']); + expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); - expect((relatedField.type as any).name.value).toEqual('ModelTest1Connection'); - -}) + expect((relatedField.type as any).name.value).toEqual('ModelTest1Connection'); +}); test('Test ModelConnectionTransformer for bidirectional One-to-Many query case.', () => { - const validSchema = ` + const validSchema = ` type Post @model @key(name: "byOwner", fields: ["owner", "id"]) @@ -401,41 +355,36 @@ test('Test ModelConnectionTransformer for bidirectional One-to-Many query case.' id: ID! posts: [Post] @connection(keyName: "byOwner", fields: ["id"]) } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + `; - const out = transformer.transform(validSchema); - expect(out).toBeDefined(); - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'author')]).toBeTruthy(); - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('User', 'posts')]).toBeTruthy(); - const schemaDoc = parse(out.schema); + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - const userType = getObjectType(schemaDoc, 'User'); - expectFields(userType, ['posts']); - const postsField = userType.fields.find(f => f.name.value === 'posts'); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'author')]).toBeTruthy(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('User', 'posts')]).toBeTruthy(); + const schemaDoc = parse(out.schema); - expect(postsField.arguments.length).toEqual(5); - expectArguments(postsField, ['id', 'filter', 'limit', 'nextToken', 'sortDirection']); - expect(postsField.type.kind).toEqual(Kind.NAMED_TYPE); + const userType = getObjectType(schemaDoc, 'User'); + expectFields(userType, ['posts']); + const postsField = userType.fields.find(f => f.name.value === 'posts'); - expect((postsField.type as any).name.value).toEqual('ModelPostConnection'); + expect(postsField.arguments.length).toEqual(5); + expectArguments(postsField, ['id', 'filter', 'limit', 'nextToken', 'sortDirection']); + expect(postsField.type.kind).toEqual(Kind.NAMED_TYPE); - const postType = getObjectType(schemaDoc, 'Post'); - expectFields(postType, ['author']); - const userField = postType.fields.find(f => f.name.value === 'author'); - expect(userField.type.kind).toEqual(Kind.NAMED_TYPE); + expect((postsField.type as any).name.value).toEqual('ModelPostConnection'); -}) + const postType = getObjectType(schemaDoc, 'Post'); + expectFields(postType, ['author']); + const userField = postType.fields.find(f => f.name.value === 'author'); + expect(userField.type.kind).toEqual(Kind.NAMED_TYPE); +}); test('Test ModelConnectionTransformer for One-to-Many query with a composite sort key.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -452,34 +401,29 @@ test('Test ModelConnectionTransformer for One-to-Many query with a composite sor email: String! name: String! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + `; - const out = transformer.transform(validSchema); - expect(out).toBeDefined(); - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherParts')]).toBeTruthy(); - const schemaDoc = parse(out.schema); + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - const testObjType = getObjectType(schemaDoc, 'Test'); - expectFields(testObjType, ['otherParts']); - const relatedField = testObjType.fields.find(f => f.name.value === 'otherParts'); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherParts')]).toBeTruthy(); + const schemaDoc = parse(out.schema); - expect(relatedField.arguments.length).toEqual(4); - expectArguments(relatedField, ['filter', 'limit', 'nextToken', 'sortDirection']); - expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); + const testObjType = getObjectType(schemaDoc, 'Test'); + expectFields(testObjType, ['otherParts']); + const relatedField = testObjType.fields.find(f => f.name.value === 'otherParts'); - expect((relatedField.type as any).name.value).toEqual('ModelTest1Connection'); + expect(relatedField.arguments.length).toEqual(4); + expectArguments(relatedField, ['filter', 'limit', 'nextToken', 'sortDirection']); + expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); -}) + expect((relatedField.type as any).name.value).toEqual('ModelTest1Connection'); +}); test('Test ModelConnectionTransformer for One-to-Many query with a composite sort key passed in as an argument.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -496,35 +440,30 @@ test('Test ModelConnectionTransformer for One-to-Many query with a composite sor email: String! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - const out = transformer.transform(validSchema); - expect(out).toBeDefined(); - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherParts')]).toBeTruthy(); - const schemaDoc = parse(out.schema); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherParts')]).toBeTruthy(); + const schemaDoc = parse(out.schema); - const testObjType = getObjectType(schemaDoc, 'Test'); - expectFields(testObjType, ['otherParts']); - const relatedField = testObjType.fields.find(f => f.name.value === 'otherParts'); + const testObjType = getObjectType(schemaDoc, 'Test'); + expectFields(testObjType, ['otherParts']); + const relatedField = testObjType.fields.find(f => f.name.value === 'otherParts'); - expect(relatedField.arguments.length).toEqual(5); - expectArguments(relatedField, ['emailName', 'filter', 'limit', 'nextToken', 'sortDirection']); - expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); + expect(relatedField.arguments.length).toEqual(5); + expectArguments(relatedField, ['emailName', 'filter', 'limit', 'nextToken', 'sortDirection']); + expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); - expect((relatedField.type as any).name.value).toEqual('ModelTest1Connection'); - -}) + expect((relatedField.type as any).name.value).toEqual('ModelTest1Connection'); +}); test('Test ModelConnectionTransformer for One-to-One getItem with composite sort key.', () => { - const validSchema = ` + const validSchema = ` type Test @model { id: ID! email: String! @@ -541,50 +480,46 @@ test('Test ModelConnectionTransformer for One-to-One getItem with composite sort email: String! name: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new KeyTransformer(), new ModelConnectionTransformer()], + }); - const out = transformer.transform(validSchema); - expect(out).toBeDefined(); - expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherHalf')]).toBeTruthy(); - const schemaDoc = parse(out.schema); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(out.stacks.ConnectionStack.Resources[ResolverResourceIDs.ResolverResourceID('Test', 'otherHalf')]).toBeTruthy(); + const schemaDoc = parse(out.schema); - const testObjType = getObjectType(schemaDoc, 'Test'); - expectFields(testObjType, ['otherHalf']); - const relatedField = testObjType.fields.find(f => f.name.value === 'otherHalf'); - expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); -}) + const testObjType = getObjectType(schemaDoc, 'Test'); + expectFields(testObjType, ['otherHalf']); + const relatedField = testObjType.fields.find(f => f.name.value === 'otherHalf'); + expect(relatedField.type.kind).toEqual(Kind.NAMED_TYPE); +}); function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type - ) as InputObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type) as + | InputObjectTypeDefinitionNode + | undefined; } // Taken from ModelConnectionTransformer.test.ts function getObjectType(doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type - ) as ObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; } function expectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } function expectArguments(field: FieldDefinitionNode, args: string[]) { - for (const argName of args) { - const foundArg = field.arguments.find((a: InputValueDefinitionNode) => a.name.value === argName) - expect(foundArg).toBeDefined() - } + for (const argName of args) { + const foundArg = field.arguments.find((a: InputValueDefinitionNode) => a.name.value === argName); + expect(foundArg).toBeDefined(); + } } diff --git a/packages/graphql-connection-transformer/src/definitions.ts b/packages/graphql-connection-transformer/src/definitions.ts index 8a5be9bbec..9db7222978 100644 --- a/packages/graphql-connection-transformer/src/definitions.ts +++ b/packages/graphql-connection-transformer/src/definitions.ts @@ -1,43 +1,40 @@ -import { InputObjectTypeDefinitionNode } from 'graphql' +import { InputObjectTypeDefinitionNode } from 'graphql'; import { makeInputValueDefinition, makeNonNullType, makeNamedType } from 'graphql-transformer-common'; export function updateCreateInputWithConnectionField( - input: InputObjectTypeDefinitionNode, - connectionFieldName: string, - nonNull: boolean = false + input: InputObjectTypeDefinitionNode, + connectionFieldName: string, + nonNull: boolean = false ): InputObjectTypeDefinitionNode { - const keyFieldExists = Boolean(input.fields.find(f => f.name.value === connectionFieldName)) - // If the key field already exists then do not change the input. - // The @connection field will validate that the key field is valid. - if (keyFieldExists) { - return input; - } - const updatedFields = [ - ...input.fields, - makeInputValueDefinition(connectionFieldName, nonNull ? makeNonNullType(makeNamedType('ID')) : makeNamedType('ID')) - ] - return { - ...input, - fields: updatedFields - } + const keyFieldExists = Boolean(input.fields.find(f => f.name.value === connectionFieldName)); + // If the key field already exists then do not change the input. + // The @connection field will validate that the key field is valid. + if (keyFieldExists) { + return input; + } + const updatedFields = [ + ...input.fields, + makeInputValueDefinition(connectionFieldName, nonNull ? makeNonNullType(makeNamedType('ID')) : makeNamedType('ID')), + ]; + return { + ...input, + fields: updatedFields, + }; } export function updateUpdateInputWithConnectionField( - input: InputObjectTypeDefinitionNode, - connectionFieldName: string + input: InputObjectTypeDefinitionNode, + connectionFieldName: string ): InputObjectTypeDefinitionNode { - const keyFieldExists = Boolean(input.fields.find(f => f.name.value === connectionFieldName)) - // If the key field already exists then do not change the input. - // The @connection field will validate that the key field is valid. - if (keyFieldExists) { - return input; - } - const updatedFields = [ - ...input.fields, - makeInputValueDefinition(connectionFieldName, makeNamedType('ID')) - ] - return { - ...input, - fields: updatedFields - } + const keyFieldExists = Boolean(input.fields.find(f => f.name.value === connectionFieldName)); + // If the key field already exists then do not change the input. + // The @connection field will validate that the key field is valid. + if (keyFieldExists) { + return input; + } + const updatedFields = [...input.fields, makeInputValueDefinition(connectionFieldName, makeNamedType('ID'))]; + return { + ...input, + fields: updatedFields, + }; } diff --git a/packages/graphql-connection-transformer/src/index.ts b/packages/graphql-connection-transformer/src/index.ts index a683413d04..251779a9c3 100644 --- a/packages/graphql-connection-transformer/src/index.ts +++ b/packages/graphql-connection-transformer/src/index.ts @@ -1,3 +1,3 @@ -import { ModelConnectionTransformer } from './ModelConnectionTransformer' -export * from './ModelConnectionTransformer' -export default ModelConnectionTransformer +import { ModelConnectionTransformer } from './ModelConnectionTransformer'; +export * from './ModelConnectionTransformer'; +export default ModelConnectionTransformer; diff --git a/packages/graphql-connection-transformer/src/resources.ts b/packages/graphql-connection-transformer/src/resources.ts index 3e6c1eff88..a741e2c2e7 100644 --- a/packages/graphql-connection-transformer/src/resources.ts +++ b/packages/graphql-connection-transformer/src/resources.ts @@ -1,435 +1,427 @@ -import Table, { GlobalSecondaryIndex, KeySchema, Projection, AttributeDefinition } from 'cloudform-types/types/dynamoDb/table' -import Resolver from 'cloudform-types/types/appSync/resolver' -import Template from 'cloudform-types/types/template' -import { Fn, Refs } from 'cloudform-types' -import { ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode } from 'graphql' +import Table, { GlobalSecondaryIndex, KeySchema, Projection, AttributeDefinition } from 'cloudform-types/types/dynamoDb/table'; +import Resolver from 'cloudform-types/types/appSync/resolver'; +import Template from 'cloudform-types/types/template'; +import { Fn, Refs } from 'cloudform-types'; +import { ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode } from 'graphql'; import { - DynamoDBMappingTemplate, str, print, - ref, obj, set, nul, ObjectNode, - ifElse, compoundExpression, bool, equals, iff, raw, comment, qref, Expression, block -} from 'graphql-mapping-template' -import { ResourceConstants, ModelResourceIDs, DEFAULT_SCALARS, NONE_VALUE, NONE_INT_VALUE, applyKeyConditionExpression, - attributeTypeFromScalar, toCamelCase, applyCompositeKeyConditionExpression } from 'graphql-transformer-common' + DynamoDBMappingTemplate, + str, + print, + ref, + obj, + set, + nul, + ObjectNode, + ifElse, + compoundExpression, + bool, + equals, + iff, + raw, + comment, + qref, + Expression, + block, +} from 'graphql-mapping-template'; +import { + ResourceConstants, + ModelResourceIDs, + DEFAULT_SCALARS, + NONE_VALUE, + NONE_INT_VALUE, + applyKeyConditionExpression, + attributeTypeFromScalar, + toCamelCase, + applyCompositeKeyConditionExpression, +} from 'graphql-transformer-common'; import { InvalidDirectiveError } from 'graphql-transformer-core'; - - export class ResourceFactory { - - public makeParams() { - return {} + public makeParams() { + return {}; + } + + /** + * Creates the barebones template for an application. + */ + public initTemplate(): Template { + return { + Parameters: this.makeParams(), + Resources: {}, + Outputs: {}, + }; + } + + /** + * Add a GSI for the connection if one does not already exist. + * @param table The table to add the GSI to. + */ + public updateTableForConnection( + table: Table, + connectionName: string, + connectionAttributeName: string, + sortField: { name: string; type: string } = null + ): Table { + const gsis = table.Properties.GlobalSecondaryIndexes || ([] as GlobalSecondaryIndex[]); + if (gsis.length >= 20) { + throw new InvalidDirectiveError( + `Cannot create connection ${connectionName}. Table ${table.Properties.TableName} out of GSI capacity.` + ); } - - /** - * Creates the barebones template for an application. - */ - public initTemplate(): Template { - return { - Parameters: this.makeParams(), - Resources: {}, - Outputs: {} - } + const connectionGSIName = `gsi-${connectionName}`; + + // If the GSI does not exist yet then add it. + const existingGSI = gsis.find(gsi => gsi.IndexName === connectionGSIName); + if (!existingGSI) { + const keySchema = [new KeySchema({ AttributeName: connectionAttributeName, KeyType: 'HASH' })]; + if (sortField) { + keySchema.push(new KeySchema({ AttributeName: sortField.name, KeyType: 'RANGE' })); + } + gsis.push( + new GlobalSecondaryIndex({ + IndexName: connectionGSIName, + KeySchema: keySchema, + Projection: new Projection({ + ProjectionType: 'ALL', + }), + ProvisionedThroughput: Fn.If(ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, Refs.NoValue, { + ReadCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS), + WriteCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS), + }) as any, + }) + ); } - /** - * Add a GSI for the connection if one does not already exist. - * @param table The table to add the GSI to. - */ - public updateTableForConnection( - table: Table, - connectionName: string, - connectionAttributeName: string, - sortField: { name: string, type: string } = null - ): Table { - const gsis = table.Properties.GlobalSecondaryIndexes || [] as GlobalSecondaryIndex[] - if (gsis.length >= 20) { - throw new InvalidDirectiveError( - `Cannot create connection ${connectionName}. Table ${table.Properties.TableName} out of GSI capacity.` - ) - } - const connectionGSIName = `gsi-${connectionName}` - - // If the GSI does not exist yet then add it. - const existingGSI = gsis.find(gsi => gsi.IndexName === connectionGSIName) - if (!existingGSI) { - const keySchema = [new KeySchema({ AttributeName: connectionAttributeName, KeyType: 'HASH' })] - if (sortField) { - keySchema.push(new KeySchema({ AttributeName: sortField.name, KeyType: 'RANGE' })) - } - gsis.push(new GlobalSecondaryIndex({ - IndexName: connectionGSIName, - KeySchema: keySchema, - Projection: new Projection({ - ProjectionType: 'ALL' - }), - ProvisionedThroughput: Fn.If( - ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, - Refs.NoValue, - { - ReadCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS), - WriteCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS) - } - ) as any, - })) - } - - // If the attribute definition does not exist yet, add it. - const attributeDefinitions = table.Properties.AttributeDefinitions as AttributeDefinition[] - const existingAttribute = attributeDefinitions.find(attr => attr.AttributeName === connectionAttributeName) - if (!existingAttribute) { - attributeDefinitions.push(new AttributeDefinition({ - AttributeName: connectionAttributeName, - AttributeType: 'S' - })) - } - - // If the attribute definition does not exist yet, add it. - if (sortField) { - const existingSortAttribute = attributeDefinitions.find(attr => attr.AttributeName === sortField.name) - if (!existingSortAttribute) { - const scalarType = DEFAULT_SCALARS[sortField.type] - const attributeType = scalarType === 'String' ? 'S' : 'N' - attributeDefinitions.push(new AttributeDefinition({ AttributeName: sortField.name, AttributeType: attributeType })) - } - } - - table.Properties.GlobalSecondaryIndexes = gsis - table.Properties.AttributeDefinitions = attributeDefinitions - return table + // If the attribute definition does not exist yet, add it. + const attributeDefinitions = table.Properties.AttributeDefinitions as AttributeDefinition[]; + const existingAttribute = attributeDefinitions.find(attr => attr.AttributeName === connectionAttributeName); + if (!existingAttribute) { + attributeDefinitions.push( + new AttributeDefinition({ + AttributeName: connectionAttributeName, + AttributeType: 'S', + }) + ); } - /** - * Create a get item resolver for singular connections. - * @param type The parent type name. - * @param field The connection field name. - * @param relatedType The name of the related type to fetch from. - * @param connectionAttribute The name of the underlying attribute containing the id. - * @param idFieldName The name of the field within the type that serve as the id. - * @param sortFieldInfo The info about the sort field if specified. - */ - public makeGetItemConnectionResolver(type: string, field: string, relatedType: string, connectionAttribute: string, - idFieldName: string, sortFieldInfo?: {primarySortFieldName: string, sortFieldName: string, sortFieldIsStringLike: boolean}): Resolver { - let keyObj : ObjectNode = obj({ - [`${idFieldName}`]: - ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${connectionAttribute}, "${NONE_VALUE}"))`) - }) - - if (sortFieldInfo) { - if (sortFieldInfo.sortFieldIsStringLike) { - keyObj.attributes.push([ - sortFieldInfo.primarySortFieldName, - ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${sortFieldInfo.sortFieldName}, "${NONE_VALUE}"))`) - ]); - } else { - // Use Int minvalue as default - keyObj.attributes.push([ - sortFieldInfo.primarySortFieldName, - ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNull($ctx.source.${sortFieldInfo.sortFieldName}, "${NONE_INT_VALUE}"))`) - ]); - } - - } - - return new Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: print( - DynamoDBMappingTemplate.getItem({ - key: keyObj - }) - ), - ResponseMappingTemplate: print( - ref('util.toJson($context.result)') - ) - }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + // If the attribute definition does not exist yet, add it. + if (sortField) { + const existingSortAttribute = attributeDefinitions.find(attr => attr.AttributeName === sortField.name); + if (!existingSortAttribute) { + const scalarType = DEFAULT_SCALARS[sortField.type]; + const attributeType = scalarType === 'String' ? 'S' : 'N'; + attributeDefinitions.push(new AttributeDefinition({ AttributeName: sortField.name, AttributeType: attributeType })); + } } - /** - * Create a resolver that queries an item in DynamoDB. - * @param type - */ - public makeQueryConnectionResolver( - type: string, field: string, relatedType: string, - connectionAttribute: string, connectionName: string, - idFieldName: string, - sortKeyInfo?: { fieldName: string, attributeType: 'S' | 'B' | 'N' }, - limit?: number - ) { - const defaultPageLimit = 10 - const pageLimit = limit || defaultPageLimit - const setup: Expression[] = [ - set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${pageLimit})`)), - set(ref('query'), obj({ - 'expression': str('#connectionAttribute = :connectionAttribute'), - 'expressionNames': obj({ - '#connectionAttribute': str(connectionAttribute) - }), - 'expressionValues': obj({ - ':connectionAttribute': obj({ - 'S': str(`$context.source.${idFieldName}`) - }) - }) - })) - ]; - if (sortKeyInfo) { - setup.push(applyKeyConditionExpression(sortKeyInfo.fieldName, sortKeyInfo.attributeType, 'query')); - } - return new Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: print( - compoundExpression([ - ...setup, - DynamoDBMappingTemplate.query({ - query: raw('$util.toJson($query)'), - scanIndexForward: ifElse( - ref('context.args.sortDirection'), - ifElse( - equals(ref('context.args.sortDirection'), str('ASC')), - bool(true), - bool(false) - ), - bool(true) - ), - filter: ifElse( - ref('context.args.filter'), - ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), - nul() - ), - limit: ref('limit'), - nextToken: ifElse( - ref('context.args.nextToken'), - str('$context.args.nextToken'), - nul() - ), - index: str(`gsi-${connectionName}`) - }) - ]) - ), - ResponseMappingTemplate: print( - compoundExpression([ - iff(raw('!$result'), set(ref('result'), ref('ctx.result'))), - raw('$util.toJson($result)') - ]) - ) - }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + table.Properties.GlobalSecondaryIndexes = gsis; + table.Properties.AttributeDefinitions = attributeDefinitions; + return table; + } + + /** + * Create a get item resolver for singular connections. + * @param type The parent type name. + * @param field The connection field name. + * @param relatedType The name of the related type to fetch from. + * @param connectionAttribute The name of the underlying attribute containing the id. + * @param idFieldName The name of the field within the type that serve as the id. + * @param sortFieldInfo The info about the sort field if specified. + */ + public makeGetItemConnectionResolver( + type: string, + field: string, + relatedType: string, + connectionAttribute: string, + idFieldName: string, + sortFieldInfo?: { primarySortFieldName: string; sortFieldName: string; sortFieldIsStringLike: boolean } + ): Resolver { + let keyObj: ObjectNode = obj({ + [`${idFieldName}`]: ref( + `util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${connectionAttribute}, "${NONE_VALUE}"))` + ), + }); + + if (sortFieldInfo) { + if (sortFieldInfo.sortFieldIsStringLike) { + keyObj.attributes.push([ + sortFieldInfo.primarySortFieldName, + ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${sortFieldInfo.sortFieldName}, "${NONE_VALUE}"))`), + ]); + } else { + // Use Int minvalue as default + keyObj.attributes.push([ + sortFieldInfo.primarySortFieldName, + ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNull($ctx.source.${sortFieldInfo.sortFieldName}, "${NONE_INT_VALUE}"))`), + ]); + } } - // Resources for new way to parameterize @connection - - /** - * Create a get item resolver for singular connections. - * @param type The parent type name. - * @param field The connection field name. - * @param relatedType The name of the related type to fetch from. - * @param connectionAttributes The names of the underlying attributes containing the fields to query by. - * @param keySchema Key schema of the index or table being queried. - */ - public makeGetItemConnectionWithKeyResolver( - type: string, - field: string, - relatedType: string, - connectionAttributes: string[], - keySchema: KeySchema[]): Resolver { - - const partitionKeyName = keySchema[0].AttributeName as string - - let keyObj : ObjectNode = obj({ - [partitionKeyName] : - ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${connectionAttributes[0]}, "${NONE_VALUE}"))`) - }) - - // Add a composite sort key or simple sort key if there is one. - if (connectionAttributes.length > 2) { - const rangeKeyFields = connectionAttributes.slice(1); - const sortKeyName = keySchema[1].AttributeName as string - const condensedSortKeyValue = this.condenseRangeKey( - rangeKeyFields.map(keyField => `\${ctx.source.${keyField}}`) - ) - - keyObj.attributes.push([ - sortKeyName, - ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank("${condensedSortKeyValue}", "${NONE_VALUE}"))`) - ]); - } else if (connectionAttributes[1]) { - const sortKeyName = keySchema[1].AttributeName as string - keyObj.attributes.push([ - sortKeyName, - ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${connectionAttributes[1]}, "${NONE_VALUE}"))`) - ]); - } - - return new Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: print( - compoundExpression([ - DynamoDBMappingTemplate.getItem({ - key: keyObj - }) - ]) - ), - ResponseMappingTemplate: print( - ref('util.toJson($context.result)') - ) - }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + return new Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: print( + DynamoDBMappingTemplate.getItem({ + key: keyObj, + }) + ), + ResponseMappingTemplate: print(ref('util.toJson($context.result)')), + }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID); + } + + /** + * Create a resolver that queries an item in DynamoDB. + * @param type + */ + public makeQueryConnectionResolver( + type: string, + field: string, + relatedType: string, + connectionAttribute: string, + connectionName: string, + idFieldName: string, + sortKeyInfo?: { fieldName: string; attributeType: 'S' | 'B' | 'N' }, + limit?: number + ) { + const defaultPageLimit = 10; + const pageLimit = limit || defaultPageLimit; + const setup: Expression[] = [ + set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${pageLimit})`)), + set( + ref('query'), + obj({ + expression: str('#connectionAttribute = :connectionAttribute'), + expressionNames: obj({ + '#connectionAttribute': str(connectionAttribute), + }), + expressionValues: obj({ + ':connectionAttribute': obj({ + S: str(`$context.source.${idFieldName}`), + }), + }), + }) + ), + ]; + if (sortKeyInfo) { + setup.push(applyKeyConditionExpression(sortKeyInfo.fieldName, sortKeyInfo.attributeType, 'query')); } - - /** - * Create a resolver that queries an item in DynamoDB. - * @param type The parent type name. - * @param field The connection field name. - * @param relatedType The related type to fetch from. - * @param connectionAttributes The names of the underlying attributes containing the fields to query by. - * @param keySchema The keySchema for the table or index being queried. - * @param indexName The index to run the query on. - */ - public makeQueryConnectionWithKeyResolver( - type: string, - field: string, - relatedType: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - connectionAttributes: string[], - keySchema: KeySchema[], - indexName: string - ) { - const defaultPageLimit = 10 - const setup: Expression[] = [ - set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), - set(ref('query'), this.makeExpression(keySchema, connectionAttributes)) - ]; - - // If the key schema has a sort key but one is not provided for the query, let a sort key be - // passed in via $ctx.args. - if (keySchema[1] && !connectionAttributes[1]) { - const sortKeyField = relatedType.fields.find(f => f.name.value === keySchema[1].AttributeName); - - if (sortKeyField) { - setup.push(applyKeyConditionExpression(String(keySchema[1].AttributeName), - attributeTypeFromScalar(sortKeyField.type), 'query')); - } else { - setup.push(applyCompositeKeyConditionExpression(this.getSortKeyNames(String(keySchema[1].AttributeName)), - 'query', - this.makeCompositeSortKeyName(String(keySchema[1].AttributeName)), - String(keySchema[1].AttributeName))); - } - } - - let queryArguments = { + return new Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: print( + compoundExpression([ + ...setup, + DynamoDBMappingTemplate.query({ query: raw('$util.toJson($query)'), scanIndexForward: ifElse( - ref('context.args.sortDirection'), - ifElse( - equals(ref('context.args.sortDirection'), str('ASC')), - bool(true), - bool(false) - ), - bool(true) - ), - filter: ifElse( - ref('context.args.filter'), - ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), - nul() + ref('context.args.sortDirection'), + ifElse(equals(ref('context.args.sortDirection'), str('ASC')), bool(true), bool(false)), + bool(true) ), + filter: ifElse(ref('context.args.filter'), ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), nul()), limit: ref('limit'), - nextToken: ifElse( - ref('context.args.nextToken'), - str('$context.args.nextToken'), - nul() - ), - index: indexName ? str(indexName) : undefined - } - - if (!indexName) { - const indexArg = 'index' - delete queryArguments[indexArg]; - } - - const queryObj = DynamoDBMappingTemplate.query(queryArguments); - - return new Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType.name.value), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: print( - compoundExpression([ - ...setup, - queryObj - ]) - ), - ResponseMappingTemplate: print( - compoundExpression([ - iff(raw('!$result'), set(ref('result'), ref('ctx.result'))), - raw('$util.toJson($result)') - ]) - ) - }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) - } - - /** - * Makes the query expression based on whether there is a sort key to be used for the query - * or not. - * @param keySchema The key schema for the table or index being queried. - * @param connectionAttributes The names of the underlying attributes containing the fields to query by. - */ - public makeExpression(keySchema: KeySchema[], connectionAttributes: string[]) : ObjectNode { - if (keySchema[1] && connectionAttributes[1]) { - - let condensedSortKeyValue : string = undefined; - if (connectionAttributes.length > 2) { - const rangeKeyFields = connectionAttributes.slice(1); - condensedSortKeyValue = this.condenseRangeKey( - rangeKeyFields.map(keyField => `\${context.source.${keyField}}`) - ) - } - - return obj({ - 'expression': str('#partitionKey = :partitionKey AND #sortKey = :sortKey'), - 'expressionNames': obj({ - '#partitionKey': str(String(keySchema[0].AttributeName)), - '#sortKey': str(String(keySchema[1].AttributeName)), - }), - 'expressionValues': obj({ - ':partitionKey': obj({ - 'S': str(`$context.source.${connectionAttributes[0]}`) - }), - ':sortKey': obj({ - 'S': str(condensedSortKeyValue || `$context.source.${connectionAttributes[1]}`) - }), - }) - }) - } - - return obj({ - 'expression': str('#partitionKey = :partitionKey'), - 'expressionNames': obj({ - '#partitionKey': str(String(keySchema[0].AttributeName)) - }), - 'expressionValues': obj({ - ':partitionKey': obj({ - 'S': str(`$context.source.${connectionAttributes[0]}`) - }) - }) - }) + nextToken: ifElse(ref('context.args.nextToken'), str('$context.args.nextToken'), nul()), + index: str(`gsi-${connectionName}`), + }), + ]) + ), + ResponseMappingTemplate: print( + compoundExpression([iff(raw('!$result'), set(ref('result'), ref('ctx.result'))), raw('$util.toJson($result)')]) + ), + }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID); + } + + // Resources for new way to parameterize @connection + + /** + * Create a get item resolver for singular connections. + * @param type The parent type name. + * @param field The connection field name. + * @param relatedType The name of the related type to fetch from. + * @param connectionAttributes The names of the underlying attributes containing the fields to query by. + * @param keySchema Key schema of the index or table being queried. + */ + public makeGetItemConnectionWithKeyResolver( + type: string, + field: string, + relatedType: string, + connectionAttributes: string[], + keySchema: KeySchema[] + ): Resolver { + const partitionKeyName = keySchema[0].AttributeName as string; + + let keyObj: ObjectNode = obj({ + [partitionKeyName]: ref( + `util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${connectionAttributes[0]}, "${NONE_VALUE}"))` + ), + }); + + // Add a composite sort key or simple sort key if there is one. + if (connectionAttributes.length > 2) { + const rangeKeyFields = connectionAttributes.slice(1); + const sortKeyName = keySchema[1].AttributeName as string; + const condensedSortKeyValue = this.condenseRangeKey(rangeKeyFields.map(keyField => `\${ctx.source.${keyField}}`)); + + keyObj.attributes.push([ + sortKeyName, + ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank("${condensedSortKeyValue}", "${NONE_VALUE}"))`), + ]); + } else if (connectionAttributes[1]) { + const sortKeyName = keySchema[1].AttributeName as string; + keyObj.attributes.push([ + sortKeyName, + ref(`util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.${connectionAttributes[1]}, "${NONE_VALUE}"))`), + ]); } - private condenseRangeKey(fields: string[]) { - return fields.join(ModelResourceIDs.ModelCompositeKeySeparator()); + return new Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: print( + compoundExpression([ + DynamoDBMappingTemplate.getItem({ + key: keyObj, + }), + ]) + ), + ResponseMappingTemplate: print(ref('util.toJson($context.result)')), + }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID); + } + + /** + * Create a resolver that queries an item in DynamoDB. + * @param type The parent type name. + * @param field The connection field name. + * @param relatedType The related type to fetch from. + * @param connectionAttributes The names of the underlying attributes containing the fields to query by. + * @param keySchema The keySchema for the table or index being queried. + * @param indexName The index to run the query on. + */ + public makeQueryConnectionWithKeyResolver( + type: string, + field: string, + relatedType: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + connectionAttributes: string[], + keySchema: KeySchema[], + indexName: string + ) { + const defaultPageLimit = 10; + const setup: Expression[] = [ + set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), + set(ref('query'), this.makeExpression(keySchema, connectionAttributes)), + ]; + + // If the key schema has a sort key but one is not provided for the query, let a sort key be + // passed in via $ctx.args. + if (keySchema[1] && !connectionAttributes[1]) { + const sortKeyField = relatedType.fields.find(f => f.name.value === keySchema[1].AttributeName); + + if (sortKeyField) { + setup.push(applyKeyConditionExpression(String(keySchema[1].AttributeName), attributeTypeFromScalar(sortKeyField.type), 'query')); + } else { + setup.push( + applyCompositeKeyConditionExpression( + this.getSortKeyNames(String(keySchema[1].AttributeName)), + 'query', + this.makeCompositeSortKeyName(String(keySchema[1].AttributeName)), + String(keySchema[1].AttributeName) + ) + ); + } } - public makeCompositeSortKeyName(sortKeyName: string) { - const attributeNames = sortKeyName.split(ModelResourceIDs.ModelCompositeKeySeparator()); - return toCamelCase(attributeNames) + let queryArguments = { + query: raw('$util.toJson($query)'), + scanIndexForward: ifElse( + ref('context.args.sortDirection'), + ifElse(equals(ref('context.args.sortDirection'), str('ASC')), bool(true), bool(false)), + bool(true) + ), + filter: ifElse(ref('context.args.filter'), ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), nul()), + limit: ref('limit'), + nextToken: ifElse(ref('context.args.nextToken'), str('$context.args.nextToken'), nul()), + index: indexName ? str(indexName) : undefined, + }; + + if (!indexName) { + const indexArg = 'index'; + delete queryArguments[indexArg]; } - private getSortKeyNames(compositeSK: string) { - return compositeSK.split(ModelResourceIDs.ModelCompositeKeySeparator()); + const queryObj = DynamoDBMappingTemplate.query(queryArguments); + + return new Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(relatedType.name.value), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: print(compoundExpression([...setup, queryObj])), + ResponseMappingTemplate: print( + compoundExpression([iff(raw('!$result'), set(ref('result'), ref('ctx.result'))), raw('$util.toJson($result)')]) + ), + }).dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID); + } + + /** + * Makes the query expression based on whether there is a sort key to be used for the query + * or not. + * @param keySchema The key schema for the table or index being queried. + * @param connectionAttributes The names of the underlying attributes containing the fields to query by. + */ + public makeExpression(keySchema: KeySchema[], connectionAttributes: string[]): ObjectNode { + if (keySchema[1] && connectionAttributes[1]) { + let condensedSortKeyValue: string = undefined; + if (connectionAttributes.length > 2) { + const rangeKeyFields = connectionAttributes.slice(1); + condensedSortKeyValue = this.condenseRangeKey(rangeKeyFields.map(keyField => `\${context.source.${keyField}}`)); + } + + return obj({ + expression: str('#partitionKey = :partitionKey AND #sortKey = :sortKey'), + expressionNames: obj({ + '#partitionKey': str(String(keySchema[0].AttributeName)), + '#sortKey': str(String(keySchema[1].AttributeName)), + }), + expressionValues: obj({ + ':partitionKey': obj({ + S: str(`$context.source.${connectionAttributes[0]}`), + }), + ':sortKey': obj({ + S: str(condensedSortKeyValue || `$context.source.${connectionAttributes[1]}`), + }), + }), + }); } + return obj({ + expression: str('#partitionKey = :partitionKey'), + expressionNames: obj({ + '#partitionKey': str(String(keySchema[0].AttributeName)), + }), + expressionValues: obj({ + ':partitionKey': obj({ + S: str(`$context.source.${connectionAttributes[0]}`), + }), + }), + }); + } + + private condenseRangeKey(fields: string[]) { + return fields.join(ModelResourceIDs.ModelCompositeKeySeparator()); + } + + public makeCompositeSortKeyName(sortKeyName: string) { + const attributeNames = sortKeyName.split(ModelResourceIDs.ModelCompositeKeySeparator()); + return toCamelCase(attributeNames); + } + + private getSortKeyNames(compositeSK: string) { + return compositeSK.split(ModelResourceIDs.ModelCompositeKeySeparator()); + } } diff --git a/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts b/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts index 08e5300184..2eedc0815e 100644 --- a/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts +++ b/packages/graphql-dynamodb-transformer/src/DynamoDBModelTransformer.ts @@ -1,24 +1,26 @@ -import { Transformer, TransformerContext, getDirectiveArguments, gql } from 'graphql-transformer-core' +import { Transformer, TransformerContext, getDirectiveArguments, gql } from 'graphql-transformer-core'; +import { DirectiveNode, ObjectTypeDefinitionNode, InputObjectTypeDefinitionNode, print } from 'graphql'; +import { ResourceFactory } from './resources'; import { - DirectiveNode, ObjectTypeDefinitionNode, InputObjectTypeDefinitionNode, print -} from 'graphql' -import { ResourceFactory } from './resources' -import { - makeCreateInputObject, makeUpdateInputObject, makeDeleteInputObject, - makeModelXFilterInputObject, makeModelSortDirectionEnumObject, makeModelConnectionType, - makeScalarFilterInputs, makeSubscriptionField, getNonModelObjectArray, - makeNonModelInputObject, makeEnumFilterInputObjects -} from './definitions' -import { - blankObject, makeField, makeInputValueDefinition, makeNamedType, - makeNonNullType -} from 'graphql-transformer-common' -import { ResolverResourceIDs, ModelResourceIDs, makeConnectionField } from 'graphql-transformer-common' + makeCreateInputObject, + makeUpdateInputObject, + makeDeleteInputObject, + makeModelXFilterInputObject, + makeModelSortDirectionEnumObject, + makeModelConnectionType, + makeScalarFilterInputs, + makeSubscriptionField, + getNonModelObjectArray, + makeNonModelInputObject, + makeEnumFilterInputObjects, +} from './definitions'; +import { blankObject, makeField, makeInputValueDefinition, makeNamedType, makeNonNullType } from 'graphql-transformer-common'; +import { ResolverResourceIDs, ModelResourceIDs, makeConnectionField } from 'graphql-transformer-common'; import { DeletionPolicy } from 'cloudform-types'; import { ModelDirectiveArgs } from './ModelDirectiveArgs'; export interface DynamoDBModelTransformerOptions { - EnableDeletionProtection?: boolean + EnableDeletionProtection?: boolean; } /** @@ -38,446 +40,419 @@ export interface DynamoDBModelTransformerOptions { */ export class DynamoDBModelTransformer extends Transformer { - - resources: ResourceFactory - opts: DynamoDBModelTransformerOptions - - constructor(opts: DynamoDBModelTransformerOptions = {}) { - super( - 'DynamoDBModelTransformer', - gql` - directive @model( - queries: ModelQueryMap, - mutations: ModelMutationMap, - subscriptions: ModelSubscriptionMap - ) on OBJECT - input ModelMutationMap { create: String, update: String, delete: String } - input ModelQueryMap { get: String, list: String } - input ModelSubscriptionMap { - onCreate: [String] - onUpdate: [String] - onDelete: [String] - level: ModelSubscriptionLevel - } - enum ModelSubscriptionLevel { off public on } - ` - ) - this.opts = this.getOpts(opts); - this.resources = new ResourceFactory(); - } - - public before = (ctx: TransformerContext): void => { - const template = this.resources.initTemplate(); - ctx.mergeResources(template.Resources) - ctx.mergeParameters(template.Parameters) - ctx.mergeOutputs(template.Outputs) - ctx.mergeConditions(template.Conditions) + resources: ResourceFactory; + opts: DynamoDBModelTransformerOptions; + + constructor(opts: DynamoDBModelTransformerOptions = {}) { + super( + 'DynamoDBModelTransformer', + gql` + directive @model(queries: ModelQueryMap, mutations: ModelMutationMap, subscriptions: ModelSubscriptionMap) on OBJECT + input ModelMutationMap { + create: String + update: String + delete: String + } + input ModelQueryMap { + get: String + list: String + } + input ModelSubscriptionMap { + onCreate: [String] + onUpdate: [String] + onDelete: [String] + level: ModelSubscriptionLevel + } + enum ModelSubscriptionLevel { + off + public + on + } + ` + ); + this.opts = this.getOpts(opts); + this.resources = new ResourceFactory(); + } + + public before = (ctx: TransformerContext): void => { + const template = this.resources.initTemplate(); + ctx.mergeResources(template.Resources); + ctx.mergeParameters(template.Parameters); + ctx.mergeOutputs(template.Outputs); + ctx.mergeConditions(template.Conditions); + }; + + /** + * Given the initial input and context manipulate the context to handle this object directive. + * @param initial The input passed to the transform. + * @param ctx The accumulated context for the transform. + */ + public object = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext): void => { + // Add a stack mapping so that all model resources are pulled + // into their own stack at the end of the transformation. + const stackName = def.name.value; + + let nonModelArray: ObjectTypeDefinitionNode[] = getNonModelObjectArray(def, ctx, new Map()); + + nonModelArray.forEach((value: ObjectTypeDefinitionNode) => { + let nonModelObject = makeNonModelInputObject(value, nonModelArray, ctx); + if (!this.typeExist(nonModelObject.name.value, ctx)) { + ctx.addInput(nonModelObject); + } + }); + + // Create the dynamodb table to hold the @model type + // TODO: Handle types with more than a single "id" hash key + const typeName = def.name.value; + const tableLogicalID = ModelResourceIDs.ModelTableResourceID(typeName); + const iamRoleLogicalID = ModelResourceIDs.ModelTableIAMRoleID(typeName); + const dataSourceRoleLogicalID = ModelResourceIDs.ModelTableDataSourceID(typeName); + const deletionPolicy = this.opts.EnableDeletionProtection ? DeletionPolicy.Retain : DeletionPolicy.Delete; + ctx.setResource(tableLogicalID, this.resources.makeModelTable(typeName, undefined, undefined, deletionPolicy)); + ctx.mapResourceToStack(stackName, tableLogicalID); + + ctx.setResource(iamRoleLogicalID, this.resources.makeIAMRole(typeName)); + ctx.mapResourceToStack(stackName, iamRoleLogicalID); + + ctx.setResource(dataSourceRoleLogicalID, this.resources.makeDynamoDBDataSource(tableLogicalID, iamRoleLogicalID, typeName)); + ctx.mapResourceToStack(stackName, dataSourceRoleLogicalID); + + const streamArnOutputId = `GetAtt${ModelResourceIDs.ModelTableStreamArn(typeName)}`; + ctx.setOutput( + // "GetAtt" is a backward compatibility addition to prevent breaking current deploys. + streamArnOutputId, + this.resources.makeTableStreamArnOutput(tableLogicalID) + ); + ctx.mapResourceToStack(stackName, streamArnOutputId); + + const datasourceOutputId = `GetAtt${dataSourceRoleLogicalID}Name`; + ctx.setOutput(datasourceOutputId, this.resources.makeDataSourceOutput(dataSourceRoleLogicalID)); + ctx.mapResourceToStack(stackName, datasourceOutputId); + + const tableNameOutputId = `GetAtt${tableLogicalID}Name`; + ctx.setOutput(tableNameOutputId, this.resources.makeTableNameOutput(tableLogicalID)); + ctx.mapResourceToStack(stackName, tableNameOutputId); + + this.createQueries(def, directive, ctx); + this.createMutations(def, directive, ctx, nonModelArray); + this.createSubscriptions(def, directive, ctx); + }; + + private createMutations = ( + def: ObjectTypeDefinitionNode, + directive: DirectiveNode, + ctx: TransformerContext, + nonModelArray: ObjectTypeDefinitionNode[] + ) => { + const typeName = def.name.value; + + const mutationFields = []; + // Get any name overrides provided by the user. If an empty map it provided + // then we do not generate those fields. + const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive); + + // Configure mutations based on *mutations* argument + let shouldMakeCreate = true; + let shouldMakeUpdate = true; + let shouldMakeDelete = true; + let createFieldNameOverride = undefined; + let updateFieldNameOverride = undefined; + let deleteFieldNameOverride = undefined; + + // Figure out which mutations to make and if they have name overrides + if (directiveArguments.mutations === null) { + shouldMakeCreate = false; + shouldMakeUpdate = false; + shouldMakeDelete = false; + } else if (directiveArguments.mutations) { + if (!directiveArguments.mutations.create) { + shouldMakeCreate = false; + } else { + createFieldNameOverride = directiveArguments.mutations.create; + } + if (!directiveArguments.mutations.update) { + shouldMakeUpdate = false; + } else { + updateFieldNameOverride = directiveArguments.mutations.update; + } + if (!directiveArguments.mutations.delete) { + shouldMakeDelete = false; + } else { + deleteFieldNameOverride = directiveArguments.mutations.delete; + } } - /** - * Given the initial input and context manipulate the context to handle this object directive. - * @param initial The input passed to the transform. - * @param ctx The accumulated context for the transform. - */ - public object = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext): void => { - // Add a stack mapping so that all model resources are pulled - // into their own stack at the end of the transformation. - const stackName = def.name.value; - - let nonModelArray: ObjectTypeDefinitionNode[] = getNonModelObjectArray( - def, - ctx, - new Map() - ) - - nonModelArray.forEach( - (value: ObjectTypeDefinitionNode) => { - let nonModelObject = makeNonModelInputObject(value, nonModelArray, ctx) - if (!this.typeExist(nonModelObject.name.value, ctx)) { - ctx.addInput(nonModelObject) - } - } + // Create the mutations. + if (shouldMakeCreate) { + const createInput = makeCreateInputObject(def, nonModelArray, ctx); + if (!ctx.getType(createInput.name.value)) { + ctx.addInput(createInput); + } + const createResolver = this.resources.makeCreateResolver(def.name.value, createFieldNameOverride); + const resourceId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName); + ctx.setResource(resourceId, createResolver); + ctx.mapResourceToStack(typeName, resourceId); + mutationFields.push( + makeField( + createResolver.Properties.FieldName, + [makeInputValueDefinition('input', makeNonNullType(makeNamedType(createInput.name.value)))], + makeNamedType(def.name.value) ) + ); + } - // Create the dynamodb table to hold the @model type - // TODO: Handle types with more than a single "id" hash key - const typeName = def.name.value - const tableLogicalID = ModelResourceIDs.ModelTableResourceID(typeName) - const iamRoleLogicalID = ModelResourceIDs.ModelTableIAMRoleID(typeName) - const dataSourceRoleLogicalID = ModelResourceIDs.ModelTableDataSourceID(typeName) - const deletionPolicy = this.opts.EnableDeletionProtection ? - DeletionPolicy.Retain : - DeletionPolicy.Delete; - ctx.setResource( - tableLogicalID, - this.resources.makeModelTable(typeName, undefined, undefined, deletionPolicy) - ) - ctx.mapResourceToStack(stackName, tableLogicalID); - - ctx.setResource( - iamRoleLogicalID, - this.resources.makeIAMRole(typeName) - ) - ctx.mapResourceToStack(stackName, iamRoleLogicalID); - - ctx.setResource( - dataSourceRoleLogicalID, - this.resources.makeDynamoDBDataSource(tableLogicalID, iamRoleLogicalID, typeName) + if (shouldMakeUpdate) { + const updateInput = makeUpdateInputObject(def, nonModelArray, ctx); + if (!ctx.getType(updateInput.name.value)) { + ctx.addInput(updateInput); + } + const updateResolver = this.resources.makeUpdateResolver(def.name.value, updateFieldNameOverride); + const resourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(typeName); + ctx.setResource(resourceId, updateResolver); + ctx.mapResourceToStack(typeName, resourceId); + mutationFields.push( + makeField( + updateResolver.Properties.FieldName, + [makeInputValueDefinition('input', makeNonNullType(makeNamedType(updateInput.name.value)))], + makeNamedType(def.name.value) ) - ctx.mapResourceToStack(stackName, dataSourceRoleLogicalID); + ); + } - const streamArnOutputId = `GetAtt${ModelResourceIDs.ModelTableStreamArn(typeName)}`; - ctx.setOutput( - // "GetAtt" is a backward compatibility addition to prevent breaking current deploys. - streamArnOutputId, - this.resources.makeTableStreamArnOutput(tableLogicalID) + if (shouldMakeDelete) { + const deleteInput = makeDeleteInputObject(def); + if (!ctx.getType(deleteInput.name.value)) { + ctx.addInput(deleteInput); + } + const deleteResolver = this.resources.makeDeleteResolver(def.name.value, deleteFieldNameOverride); + const resourceId = ResolverResourceIDs.DynamoDBDeleteResolverResourceID(typeName); + ctx.setResource(resourceId, deleteResolver); + ctx.mapResourceToStack(typeName, resourceId); + mutationFields.push( + makeField( + deleteResolver.Properties.FieldName, + [makeInputValueDefinition('input', makeNonNullType(makeNamedType(deleteInput.name.value)))], + makeNamedType(def.name.value) ) - ctx.mapResourceToStack(stackName, streamArnOutputId); + ); + } + ctx.addMutationFields(mutationFields); + }; + + private createQueries = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const typeName = def.name.value; + const queryFields = []; + const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive); + + // Configure queries based on *queries* argument + let shouldMakeGet = true; + let shouldMakeList = true; + let getFieldNameOverride = undefined; + let listFieldNameOverride = undefined; + + // Figure out which queries to make and if they have name overrides. + // If queries is undefined (default), create all queries + // If queries is explicetly set to null, do not create any + // else if queries is defined, check overrides + if (directiveArguments.queries === null) { + shouldMakeGet = false; + shouldMakeList = false; + } else if (directiveArguments.queries) { + if (!directiveArguments.queries.get) { + shouldMakeGet = false; + } else { + getFieldNameOverride = directiveArguments.queries.get; + } + if (!directiveArguments.queries.list) { + shouldMakeList = false; + } else { + listFieldNameOverride = directiveArguments.queries.list; + } + } - const datasourceOutputId = `GetAtt${dataSourceRoleLogicalID}Name`; - ctx.setOutput( - datasourceOutputId, - this.resources.makeDataSourceOutput(dataSourceRoleLogicalID) - ) - ctx.mapResourceToStack(stackName, datasourceOutputId); + if (shouldMakeList) { + if (!this.typeExist('ModelSortDirection', ctx)) { + const tableSortDirection = makeModelSortDirectionEnumObject(); + ctx.addEnum(tableSortDirection); + } + } - const tableNameOutputId = `GetAtt${tableLogicalID}Name`; - ctx.setOutput( - tableNameOutputId, - this.resources.makeTableNameOutput(tableLogicalID) + // Create get queries + if (shouldMakeGet) { + const getResolver = this.resources.makeGetResolver(def.name.value, getFieldNameOverride, ctx.getQueryTypeName()); + const resourceId = ResolverResourceIDs.DynamoDBGetResolverResourceID(typeName); + ctx.setResource(resourceId, getResolver); + ctx.mapResourceToStack(typeName, resourceId); + + queryFields.push( + makeField( + getResolver.Properties.FieldName, + [makeInputValueDefinition('id', makeNonNullType(makeNamedType('ID')))], + makeNamedType(def.name.value) ) - ctx.mapResourceToStack(stackName, tableNameOutputId); - - this.createQueries(def, directive, ctx) - this.createMutations(def, directive, ctx, nonModelArray) - this.createSubscriptions(def, directive, ctx) + ); } - private createMutations = ( - def: ObjectTypeDefinitionNode, - directive: DirectiveNode, - ctx: TransformerContext, - nonModelArray: ObjectTypeDefinitionNode[] - ) => { - const typeName = def.name.value - - const mutationFields = []; - // Get any name overrides provided by the user. If an empty map it provided - // then we do not generate those fields. - const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive) - - // Configure mutations based on *mutations* argument - let shouldMakeCreate = true; - let shouldMakeUpdate = true; - let shouldMakeDelete = true; - let createFieldNameOverride = undefined; - let updateFieldNameOverride = undefined; - let deleteFieldNameOverride = undefined; - - - // Figure out which mutations to make and if they have name overrides - if (directiveArguments.mutations === null) { - shouldMakeCreate = false - shouldMakeUpdate = false - shouldMakeDelete = false - } else if (directiveArguments.mutations) { - if (!directiveArguments.mutations.create) { - shouldMakeCreate = false; - } else { - createFieldNameOverride = directiveArguments.mutations.create - } - if (!directiveArguments.mutations.update) { - shouldMakeUpdate = false; - } else { - updateFieldNameOverride = directiveArguments.mutations.update - } - if (!directiveArguments.mutations.delete) { - shouldMakeDelete = false; - } else { - deleteFieldNameOverride = directiveArguments.mutations.delete - } - } - - // Create the mutations. - if (shouldMakeCreate) { - const createInput = makeCreateInputObject(def, nonModelArray, ctx) - if (!ctx.getType(createInput.name.value)) { - ctx.addInput(createInput) - } - const createResolver = this.resources.makeCreateResolver(def.name.value, createFieldNameOverride) - const resourceId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName); - ctx.setResource(resourceId, createResolver) - ctx.mapResourceToStack(typeName, resourceId); - mutationFields.push(makeField( - createResolver.Properties.FieldName, - [makeInputValueDefinition('input', makeNonNullType(makeNamedType(createInput.name.value)))], - makeNamedType(def.name.value) - )); - } + if (shouldMakeList) { + this.generateModelXConnectionType(ctx, def); - if (shouldMakeUpdate) { - const updateInput = makeUpdateInputObject(def, nonModelArray, ctx) - if (!ctx.getType(updateInput.name.value)) { - ctx.addInput(updateInput) - } - const updateResolver = this.resources.makeUpdateResolver(def.name.value, updateFieldNameOverride) - const resourceId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(typeName); - ctx.setResource(resourceId, updateResolver); - ctx.mapResourceToStack(typeName, resourceId); - mutationFields.push(makeField( - updateResolver.Properties.FieldName, - [makeInputValueDefinition('input', makeNonNullType(makeNamedType(updateInput.name.value)))], - makeNamedType(def.name.value) - )); - } + // Create the list resolver + const listResolver = this.resources.makeListResolver(def.name.value, listFieldNameOverride, ctx.getQueryTypeName()); + const resourceId = ResolverResourceIDs.DynamoDBListResolverResourceID(typeName); + ctx.setResource(resourceId, listResolver); + ctx.mapResourceToStack(typeName, resourceId); - if (shouldMakeDelete) { - const deleteInput = makeDeleteInputObject(def) - if (!ctx.getType(deleteInput.name.value)) { - ctx.addInput(deleteInput) - } - const deleteResolver = this.resources.makeDeleteResolver(def.name.value, deleteFieldNameOverride) - const resourceId = ResolverResourceIDs.DynamoDBDeleteResolverResourceID(typeName); - ctx.setResource(resourceId, deleteResolver); - ctx.mapResourceToStack(typeName, resourceId); - mutationFields.push(makeField( - deleteResolver.Properties.FieldName, - [makeInputValueDefinition('input', makeNonNullType(makeNamedType(deleteInput.name.value)))], - makeNamedType(def.name.value) - )); - } - ctx.addMutationFields(mutationFields) + queryFields.push(makeConnectionField(listResolver.Properties.FieldName, def.name.value)); } - - private createQueries = ( - def: ObjectTypeDefinitionNode, - directive: DirectiveNode, - ctx: TransformerContext, - ) => { - const typeName = def.name.value - const queryFields = [] - const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive) - - // Configure queries based on *queries* argument - let shouldMakeGet = true; - let shouldMakeList = true; - let getFieldNameOverride = undefined; - let listFieldNameOverride = undefined; - - // Figure out which queries to make and if they have name overrides. - // If queries is undefined (default), create all queries - // If queries is explicetly set to null, do not create any - // else if queries is defined, check overrides - if (directiveArguments.queries === null) { - shouldMakeGet = false; - shouldMakeList = false; - } else if (directiveArguments.queries) { - if (!directiveArguments.queries.get) { - shouldMakeGet = false; - } else { - getFieldNameOverride = directiveArguments.queries.get - } - if (!directiveArguments.queries.list) { - shouldMakeList = false; - } else { - listFieldNameOverride = directiveArguments.queries.list - } + this.generateFilterInputs(ctx, def); + + ctx.addQueryFields(queryFields); + }; + + /** + * Creates subscriptions for a @model object type. By default creates a subscription for + * create, update, and delete mutations. + * + * Subscriptions are one to many in that a subscription may subscribe to multiple mutations. + * You may thus provide multiple names of the subscriptions that will be triggered by each + * mutation. + * + * type Post @model(subscriptions: { onCreate: ["onPostCreated", "onFeedUpdated"] }) { + * id: ID! + * title: String! + * } + * + * will create two subscription fields: + * + * type Subscription { + * onPostCreated: Post @aws_subscribe(mutations: ["createPost"]) + * onFeedUpdated: Post @aws_subscribe(mutations: ["createPost"]) + * } + * Subscription Levels + * subscriptions.level === OFF || subscriptions === null + * Will not create subscription operations + * subcriptions.level === PUBLIC + * Will continue as is creating subscription operations + * subscriptions.level === ON || subscriptions === undefined + * If auth is enabled it will enabled protection on subscription operations and resolvers + */ + private createSubscriptions = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const typeName = def.name.value; + const subscriptionFields = []; + + const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive); + + const subscriptionsArgument = directiveArguments.subscriptions; + const createResolver = ctx.getResource(ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName)); + const updateResolver = ctx.getResource(ResolverResourceIDs.DynamoDBUpdateResolverResourceID(typeName)); + const deleteResolver = ctx.getResource(ResolverResourceIDs.DynamoDBDeleteResolverResourceID(typeName)); + + if (subscriptionsArgument === null) { + return; + } else if (subscriptionsArgument && subscriptionsArgument.level === 'off') { + return; + } else if ( + subscriptionsArgument && + (subscriptionsArgument.onCreate || subscriptionsArgument.onUpdate || subscriptionsArgument.onDelete) + ) { + // Add the custom subscriptions + const subscriptionToMutationsMap: { [subField: string]: string[] } = {}; + const onCreate = subscriptionsArgument.onCreate || []; + const onUpdate = subscriptionsArgument.onUpdate || []; + const onDelete = subscriptionsArgument.onDelete || []; + const subFields = [...onCreate, ...onUpdate, ...onDelete]; + // initialize the reverse lookup + for (const field of subFields) { + subscriptionToMutationsMap[field] = []; + } + // Add the correct mutation to the lookup + for (const field of Object.keys(subscriptionToMutationsMap)) { + if (onCreate.includes(field) && createResolver) { + subscriptionToMutationsMap[field].push(createResolver.Properties.FieldName); } - - if (shouldMakeList) { - if (!this.typeExist('ModelSortDirection', ctx)) { - const tableSortDirection = makeModelSortDirectionEnumObject() - ctx.addEnum(tableSortDirection) - } + if (onUpdate.includes(field) && updateResolver) { + subscriptionToMutationsMap[field].push(updateResolver.Properties.FieldName); } - - // Create get queries - if (shouldMakeGet) { - const getResolver = this.resources.makeGetResolver(def.name.value, getFieldNameOverride, ctx.getQueryTypeName()) - const resourceId = ResolverResourceIDs.DynamoDBGetResolverResourceID(typeName); - ctx.setResource(resourceId, getResolver); - ctx.mapResourceToStack(typeName, resourceId); - - queryFields.push(makeField( - getResolver.Properties.FieldName, - [makeInputValueDefinition('id', makeNonNullType(makeNamedType('ID')))], - makeNamedType(def.name.value) - )) + if (onDelete.includes(field) && deleteResolver) { + subscriptionToMutationsMap[field].push(deleteResolver.Properties.FieldName); } - - if (shouldMakeList) { - - this.generateModelXConnectionType(ctx, def) - - // Create the list resolver - const listResolver = this.resources.makeListResolver(def.name.value, listFieldNameOverride, ctx.getQueryTypeName()) - const resourceId = ResolverResourceIDs.DynamoDBListResolverResourceID(typeName); - ctx.setResource(resourceId, listResolver); - ctx.mapResourceToStack(typeName, resourceId); - - queryFields.push(makeConnectionField(listResolver.Properties.FieldName, def.name.value)) - } - this.generateFilterInputs(ctx, def) - - ctx.addQueryFields(queryFields) + } + for (const subFieldName of Object.keys(subscriptionToMutationsMap)) { + const subField = makeSubscriptionField(subFieldName, typeName, subscriptionToMutationsMap[subFieldName]); + subscriptionFields.push(subField); + } + } else { + // Add the default subscriptions + if (createResolver) { + const onCreateField = makeSubscriptionField(ModelResourceIDs.ModelOnCreateSubscriptionName(typeName), typeName, [ + createResolver.Properties.FieldName, + ]); + subscriptionFields.push(onCreateField); + } + if (updateResolver) { + const onUpdateField = makeSubscriptionField(ModelResourceIDs.ModelOnUpdateSubscriptionName(typeName), typeName, [ + updateResolver.Properties.FieldName, + ]); + subscriptionFields.push(onUpdateField); + } + if (deleteResolver) { + const onDeleteField = makeSubscriptionField(ModelResourceIDs.ModelOnDeleteSubscriptionName(typeName), typeName, [ + deleteResolver.Properties.FieldName, + ]); + subscriptionFields.push(onDeleteField); + } } - /** - * Creates subscriptions for a @model object type. By default creates a subscription for - * create, update, and delete mutations. - * - * Subscriptions are one to many in that a subscription may subscribe to multiple mutations. - * You may thus provide multiple names of the subscriptions that will be triggered by each - * mutation. - * - * type Post @model(subscriptions: { onCreate: ["onPostCreated", "onFeedUpdated"] }) { - * id: ID! - * title: String! - * } - * - * will create two subscription fields: - * - * type Subscription { - * onPostCreated: Post @aws_subscribe(mutations: ["createPost"]) - * onFeedUpdated: Post @aws_subscribe(mutations: ["createPost"]) - * } - * Subscription Levels - * subscriptions.level === OFF || subscriptions === null - * Will not create subscription operations - * subcriptions.level === PUBLIC - * Will continue as is creating subscription operations - * subscriptions.level === ON || subscriptions === undefined - * If auth is enabled it will enabled protection on subscription operations and resolvers - */ - private createSubscriptions = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const typeName = def.name.value - const subscriptionFields = [] - - const directiveArguments: ModelDirectiveArgs = getDirectiveArguments(directive) - - const subscriptionsArgument = directiveArguments.subscriptions - const createResolver = ctx.getResource(ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName)) - const updateResolver = ctx.getResource(ResolverResourceIDs.DynamoDBUpdateResolverResourceID(typeName)) - const deleteResolver = ctx.getResource(ResolverResourceIDs.DynamoDBDeleteResolverResourceID(typeName)) - - if (subscriptionsArgument === null) { - return; - } else if (subscriptionsArgument && - subscriptionsArgument.level === "off") { - return; - } else if (subscriptionsArgument && - (subscriptionsArgument.onCreate || subscriptionsArgument.onUpdate || subscriptionsArgument.onDelete)) { - // Add the custom subscriptions - const subscriptionToMutationsMap: { [subField: string]: string[] } = {} - const onCreate = subscriptionsArgument.onCreate || [] - const onUpdate = subscriptionsArgument.onUpdate || [] - const onDelete = subscriptionsArgument.onDelete || [] - const subFields = [...onCreate, ...onUpdate, ...onDelete] - // initialize the reverse lookup - for (const field of subFields) { - subscriptionToMutationsMap[field] = [] - } - // Add the correct mutation to the lookup - for (const field of Object.keys(subscriptionToMutationsMap)) { - if (onCreate.includes(field) && createResolver) { - subscriptionToMutationsMap[field].push(createResolver.Properties.FieldName) - } - if (onUpdate.includes(field) && updateResolver) { - subscriptionToMutationsMap[field].push(updateResolver.Properties.FieldName) - } - if (onDelete.includes(field) && deleteResolver) { - subscriptionToMutationsMap[field].push(deleteResolver.Properties.FieldName) - } - } - for (const subFieldName of Object.keys(subscriptionToMutationsMap)) { - const subField = makeSubscriptionField( - subFieldName, - typeName, - subscriptionToMutationsMap[subFieldName] - ) - subscriptionFields.push(subField) - } - } else { - // Add the default subscriptions - if (createResolver) { - const onCreateField = makeSubscriptionField( - ModelResourceIDs.ModelOnCreateSubscriptionName(typeName), - typeName, - [createResolver.Properties.FieldName] - ) - subscriptionFields.push(onCreateField) - } - if (updateResolver) { - const onUpdateField = makeSubscriptionField( - ModelResourceIDs.ModelOnUpdateSubscriptionName(typeName), - typeName, - [updateResolver.Properties.FieldName] - ) - subscriptionFields.push(onUpdateField) - } - if (deleteResolver) { - const onDeleteField = makeSubscriptionField( - ModelResourceIDs.ModelOnDeleteSubscriptionName(typeName), - typeName, - [deleteResolver.Properties.FieldName] - ) - subscriptionFields.push(onDeleteField) - } - } + ctx.addSubscriptionFields(subscriptionFields); + }; - ctx.addSubscriptionFields(subscriptionFields) - } + private typeExist(type: string, ctx: TransformerContext): boolean { + return Boolean(type in ctx.nodeMap); + } - private typeExist(type: string, ctx: TransformerContext): boolean { - return Boolean(type in ctx.nodeMap); + private generateModelXConnectionType(ctx: TransformerContext, def: ObjectTypeDefinitionNode): void { + const tableXConnectionName = ModelResourceIDs.ModelConnectionTypeName(def.name.value); + if (this.typeExist(tableXConnectionName, ctx)) { + return; } - private generateModelXConnectionType(ctx: TransformerContext, def: ObjectTypeDefinitionNode): void { - const tableXConnectionName = ModelResourceIDs.ModelConnectionTypeName(def.name.value) - if (this.typeExist(tableXConnectionName, ctx)) { - return - } + // Create the ModelXConnection + const connectionType = blankObject(tableXConnectionName); + ctx.addObject(connectionType); - // Create the ModelXConnection - const connectionType = blankObject(tableXConnectionName) - ctx.addObject(connectionType) + ctx.addObjectExtension(makeModelConnectionType(def.name.value)); + } - ctx.addObjectExtension(makeModelConnectionType(def.name.value)) + private generateFilterInputs(ctx: TransformerContext, def: ObjectTypeDefinitionNode): void { + const scalarFilters = makeScalarFilterInputs(); + for (const filter of scalarFilters) { + if (!this.typeExist(filter.name.value, ctx)) { + ctx.addInput(filter); + } } - private generateFilterInputs( - ctx: TransformerContext, - def: ObjectTypeDefinitionNode, - ): void { - const scalarFilters = makeScalarFilterInputs() - for (const filter of scalarFilters) { - if (!this.typeExist(filter.name.value, ctx)) { - ctx.addInput(filter) - } - } - - // Create the Enum filters - const enumFilters = makeEnumFilterInputObjects(def, ctx); - for (const filter of enumFilters) { - if (!this.typeExist(filter.name.value, ctx)) { - ctx.addInput(filter) - } - } - - // Create the ModelXFilterInput - const tableXQueryFilterInput = makeModelXFilterInputObject(def, ctx) - if (!this.typeExist(tableXQueryFilterInput.name.value, ctx)) { - ctx.addInput(tableXQueryFilterInput) - } + // Create the Enum filters + const enumFilters = makeEnumFilterInputObjects(def, ctx); + for (const filter of enumFilters) { + if (!this.typeExist(filter.name.value, ctx)) { + ctx.addInput(filter); + } } - private getOpts(opts: DynamoDBModelTransformerOptions) { - const defaultOpts = { - EnableDeletionProtection: false - }; - return { - ...defaultOpts, - ...opts - } + // Create the ModelXFilterInput + const tableXQueryFilterInput = makeModelXFilterInputObject(def, ctx); + if (!this.typeExist(tableXQueryFilterInput.name.value, ctx)) { + ctx.addInput(tableXQueryFilterInput); } + } + + private getOpts(opts: DynamoDBModelTransformerOptions) { + const defaultOpts = { + EnableDeletionProtection: false, + }; + return { + ...defaultOpts, + ...opts, + }; + } } diff --git a/packages/graphql-dynamodb-transformer/src/ModelDirectiveArgs.ts b/packages/graphql-dynamodb-transformer/src/ModelDirectiveArgs.ts index 26268bff85..9ea0beccce 100644 --- a/packages/graphql-dynamodb-transformer/src/ModelDirectiveArgs.ts +++ b/packages/graphql-dynamodb-transformer/src/ModelDirectiveArgs.ts @@ -1,26 +1,26 @@ export interface QueryNameMap { - get?: string; - list?: string; - query?: string; + get?: string; + list?: string; + query?: string; } export interface MutationNameMap { - create?: string; - update?: string; - delete?: string; + create?: string; + update?: string; + delete?: string; } -export type ModelSubscriptionLevel = 'off' | 'public' | 'on' +export type ModelSubscriptionLevel = 'off' | 'public' | 'on'; export interface SubscriptionNameMap { - onCreate?: string[]; - onUpdate?: string[]; - onDelete?: string[]; - level?: ModelSubscriptionLevel; + onCreate?: string[]; + onUpdate?: string[]; + onDelete?: string[]; + level?: ModelSubscriptionLevel; } export interface ModelDirectiveArgs { - queries?: QueryNameMap, - mutations?: MutationNameMap, - subscriptions?: SubscriptionNameMap, -} \ No newline at end of file + queries?: QueryNameMap; + mutations?: MutationNameMap; + subscriptions?: SubscriptionNameMap; +} diff --git a/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts b/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts index 803f58f043..3f1affda8b 100644 --- a/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts +++ b/packages/graphql-dynamodb-transformer/src/__tests__/DynamoDBModelTransformer.test.ts @@ -1,115 +1,115 @@ import { - ObjectTypeDefinitionNode, parse, FieldDefinitionNode, DocumentNode, - DefinitionNode, Kind, InputObjectTypeDefinitionNode, ListValueNode, - InputValueDefinitionNode, TypeNode, NamedTypeNode -} from 'graphql' -import GraphQLTransform from 'graphql-transformer-core' -import { ResourceConstants } from 'graphql-transformer-common' -import { DynamoDBModelTransformer } from '../DynamoDBModelTransformer' + ObjectTypeDefinitionNode, + parse, + FieldDefinitionNode, + DocumentNode, + DefinitionNode, + Kind, + InputObjectTypeDefinitionNode, + ListValueNode, + InputValueDefinitionNode, + TypeNode, + NamedTypeNode, +} from 'graphql'; +import GraphQLTransform from 'graphql-transformer-core'; +import { ResourceConstants } from 'graphql-transformer-common'; +import { DynamoDBModelTransformer } from '../DynamoDBModelTransformer'; test('Test DynamoDBModelTransformer validation happy case', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); }); test('Test DynamoDBModelTransformer with query overrides', () => { - const validSchema = `type Post @model(queries: { get: "customGetPost", list: "customListPost" }) { + const validSchema = `type Post @model(queries: { get: "customGetPost", list: "customListPost" }) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const createPostInput = getInputType(parsed, 'CreatePostInput') - expectFieldsOnInputType(createPostInput, ['id', 'title', 'createdAt', 'updatedAt']) - // This id should always be optional. - // aka a named type node aka name.value would not be set if it were a non null node - const idField = createPostInput.fields.find(f => f.name.value === 'id') - expect((idField.type as NamedTypeNode).name.value).toEqual('ID'); - const queryType = getObjectType(parsed, 'Query') - expect(queryType).toBeDefined() - expectFields(queryType, ['customGetPost']) - expectFields(queryType, ['customListPost']) - const subscriptionType = getObjectType(parsed, 'Subscription') - expect(subscriptionType).toBeDefined() - expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost']) - const subField = subscriptionType.fields.find(f => f.name.value === 'onCreatePost') - expect(subField.directives.length).toEqual(1) - expect(subField.directives[0].name.value).toEqual('aws_subscribe') + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const createPostInput = getInputType(parsed, 'CreatePostInput'); + expectFieldsOnInputType(createPostInput, ['id', 'title', 'createdAt', 'updatedAt']); + // This id should always be optional. + // aka a named type node aka name.value would not be set if it were a non null node + const idField = createPostInput.fields.find(f => f.name.value === 'id'); + expect((idField.type as NamedTypeNode).name.value).toEqual('ID'); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).toBeDefined(); + expectFields(queryType, ['customGetPost']); + expectFields(queryType, ['customListPost']); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).toBeDefined(); + expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost']); + const subField = subscriptionType.fields.find(f => f.name.value === 'onCreatePost'); + expect(subField.directives.length).toEqual(1); + expect(subField.directives[0].name.value).toEqual('aws_subscribe'); }); test('Test DynamoDBModelTransformer with mutation overrides', () => { - const validSchema = `type Post @model(mutations: { create: "customCreatePost", update: "customUpdatePost", delete: "customDeletePost" }) { + const validSchema = `type Post @model(mutations: { create: "customCreatePost", update: "customUpdatePost", delete: "customDeletePost" }) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const mutationType = getObjectType(parsed, 'Mutation') - expect(mutationType).toBeDefined() - expectFields(mutationType, ['customCreatePost', 'customUpdatePost', 'customDeletePost']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeDefined(); + expectFields(mutationType, ['customCreatePost', 'customUpdatePost', 'customDeletePost']); }); test('Test DynamoDBModelTransformer with only create mutations', () => { - const validSchema = `type Post @model(mutations: { create: "customCreatePost" }) { + const validSchema = `type Post @model(mutations: { create: "customCreatePost" }) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const mutationType = getObjectType(parsed, 'Mutation') - expect(mutationType).toBeDefined() - expectFields(mutationType, ['customCreatePost']) - doNotExpectFields(mutationType, ['updatePost']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeDefined(); + expectFields(mutationType, ['customCreatePost']); + doNotExpectFields(mutationType, ['updatePost']); }); test('Test DynamoDBModelTransformer with multiple model directives', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -121,169 +121,165 @@ test('Test DynamoDBModelTransformer with multiple model directives', () => { id: ID! name: String! } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query') - expect(queryType).toBeDefined() - expectFields(queryType, ['listPosts']) - expectFields(queryType, ['listUsers']) + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).toBeDefined(); + expectFields(queryType, ['listPosts']); + expectFields(queryType, ['listUsers']); - const stringInputType = getInputType(parsed, 'ModelStringFilterInput') - expect(stringInputType).toBeDefined() - const booleanInputType = getInputType(parsed, 'ModelBooleanFilterInput') - expect(booleanInputType).toBeDefined() - const intInputType = getInputType(parsed, 'ModelIntFilterInput') - expect(intInputType).toBeDefined() - const floatInputType = getInputType(parsed, 'ModelFloatFilterInput') - expect(floatInputType).toBeDefined() - const idInputType = getInputType(parsed, 'ModelIDFilterInput') - expect(idInputType).toBeDefined() - const postInputType = getInputType(parsed, 'ModelPostFilterInput') - expect(postInputType).toBeDefined() - const userInputType = getInputType(parsed, 'ModelUserFilterInput') - expect(userInputType).toBeDefined() + const stringInputType = getInputType(parsed, 'ModelStringFilterInput'); + expect(stringInputType).toBeDefined(); + const booleanInputType = getInputType(parsed, 'ModelBooleanFilterInput'); + expect(booleanInputType).toBeDefined(); + const intInputType = getInputType(parsed, 'ModelIntFilterInput'); + expect(intInputType).toBeDefined(); + const floatInputType = getInputType(parsed, 'ModelFloatFilterInput'); + expect(floatInputType).toBeDefined(); + const idInputType = getInputType(parsed, 'ModelIDFilterInput'); + expect(idInputType).toBeDefined(); + const postInputType = getInputType(parsed, 'ModelPostFilterInput'); + expect(postInputType).toBeDefined(); + const userInputType = getInputType(parsed, 'ModelUserFilterInput'); + expect(userInputType).toBeDefined(); - expect(verifyInputCount(parsed, 'ModelStringFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelBooleanFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelIntFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelFloatFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelIDFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelPostFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelUserFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelStringFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelBooleanFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelIntFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelFloatFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelIDFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelPostFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelUserFilterInput', 1)).toBeTruthy(); }); test('Test DynamoDBModelTransformer with filter', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! createdAt: String updatedAt: String - }` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() + }`; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query') - expect(queryType).toBeDefined() - expectFields(queryType, ['listPosts']) + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).toBeDefined(); + expectFields(queryType, ['listPosts']); - const connectionType = getObjectType(parsed, 'ModelPostConnection') - expect(connectionType).toBeDefined() + const connectionType = getObjectType(parsed, 'ModelPostConnection'); + expect(connectionType).toBeDefined(); - expect(verifyInputCount(parsed, 'ModelStringFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelBooleanFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelIntFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelFloatFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelIDFilterInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'ModelPostFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelStringFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelBooleanFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelIntFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelFloatFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelIDFilterInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'ModelPostFilterInput', 1)).toBeTruthy(); }); test('Test DynamoDBModelTransformer with mutations set to null', () => { - const validSchema = `type Post @model(mutations: null) { + const validSchema = `type Post @model(mutations: null) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer()] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const mutationType = getObjectType(parsed, 'Mutation') - expect(mutationType).not.toBeDefined() -}) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).not.toBeDefined(); +}); test('Test DynamoDBModelTransformer with queries set to null', () => { - const validSchema = `type Post @model(queries: null) { + const validSchema = `type Post @model(queries: null) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer()] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const mutationType = getObjectType(parsed, 'Mutation') - expect(mutationType).toBeDefined() - const queryType = getObjectType(parsed, 'Query') - expect(queryType).not.toBeDefined() -}) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeDefined(); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).not.toBeDefined(); +}); test('Test DynamoDBModelTransformer with subscriptions set to null', () => { - const validSchema = `type Post @model(subscriptions: null) { + const validSchema = `type Post @model(subscriptions: null) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer()] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const mutationType = getObjectType(parsed, 'Mutation') - expect(mutationType).toBeDefined() - const queryType = getObjectType(parsed, 'Query') - expect(queryType).toBeDefined() - const subscriptionType = getObjectType(parsed, 'Subscription') - expect(subscriptionType).not.toBeDefined() -}) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeDefined(); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).toBeDefined(); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).not.toBeDefined(); +}); test('Test DynamoDBModelTransformer with queries and mutations set to null', () => { - const validSchema = `type Post @model(queries: null, mutations: null, subscriptions: null) { + const validSchema = `type Post @model(queries: null, mutations: null, subscriptions: null) { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer()] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const mutationType = getObjectType(parsed, 'Mutation') - expect(mutationType).not.toBeDefined() - const queryType = getObjectType(parsed, 'Query') - expect(queryType).not.toBeDefined() - const subscriptionType = getObjectType(parsed, 'Subscription') - expect(subscriptionType).not.toBeDefined() -}) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).not.toBeDefined(); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).not.toBeDefined(); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).not.toBeDefined(); +}); test('Test DynamoDBModelTransformer with advanced subscriptions', () => { - const validSchema = `type Post @model(subscriptions: { + const validSchema = `type Post @model(subscriptions: { onCreate: ["onFeedUpdated", "onCreatePost"], onUpdate: ["onFeedUpdated"], onDelete: ["onFeedUpdated"] @@ -293,31 +289,31 @@ test('Test DynamoDBModelTransformer with advanced subscriptions', () => { createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [new DynamoDBModelTransformer()] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const subscriptionType = getObjectType(parsed, 'Subscription') - expect(subscriptionType).toBeDefined() - expectFields(subscriptionType, ['onFeedUpdated', 'onCreatePost']) - const subField = subscriptionType.fields.find(f => f.name.value === 'onFeedUpdated') - expect(subField.directives.length).toEqual(1) - expect(subField.directives[0].name.value).toEqual('aws_subscribe') - const mutationsList = subField.directives[0].arguments.find(a => a.name.value === 'mutations').value as ListValueNode - const mutList = mutationsList.values.map((v: any) => v.value) - expect(mutList.length).toEqual(3) - expect(mutList).toContain('createPost') - expect(mutList).toContain('updatePost') - expect(mutList).toContain('deletePost') -}) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).toBeDefined(); + expectFields(subscriptionType, ['onFeedUpdated', 'onCreatePost']); + const subField = subscriptionType.fields.find(f => f.name.value === 'onFeedUpdated'); + expect(subField.directives.length).toEqual(1); + expect(subField.directives[0].name.value).toEqual('aws_subscribe'); + const mutationsList = subField.directives[0].arguments.find(a => a.name.value === 'mutations').value as ListValueNode; + const mutList = mutationsList.values.map((v: any) => v.value); + expect(mutList.length).toEqual(3); + expect(mutList).toContain('createPost'); + expect(mutList).toContain('updatePost'); + expect(mutList).toContain('deletePost'); +}); test('Test DynamoDBModelTransformer with non-model types and enums', () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -338,42 +334,40 @@ test('Test DynamoDBModelTransformer with non-model types and enums', () => { EMPIRE JEDI } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); - const postMetaDataInputType = getInputType(parsed, 'PostMetadataInput') - expect(postMetaDataInputType).toBeDefined() - const tagInputType = getInputType(parsed, 'TagInput') - expect(tagInputType).toBeDefined() - expectFieldsOnInputType(tagInputType, ['metadata']) - const createPostInputType = getInputType(parsed, 'CreatePostInput') - expectFieldsOnInputType(createPostInputType, ['metadata', 'appearsIn']) - const updatePostInputType = getInputType(parsed, 'UpdatePostInput') - expectFieldsOnInputType(updatePostInputType, ['metadata', 'appearsIn']) + const postMetaDataInputType = getInputType(parsed, 'PostMetadataInput'); + expect(postMetaDataInputType).toBeDefined(); + const tagInputType = getInputType(parsed, 'TagInput'); + expect(tagInputType).toBeDefined(); + expectFieldsOnInputType(tagInputType, ['metadata']); + const createPostInputType = getInputType(parsed, 'CreatePostInput'); + expectFieldsOnInputType(createPostInputType, ['metadata', 'appearsIn']); + const updatePostInputType = getInputType(parsed, 'UpdatePostInput'); + expectFieldsOnInputType(updatePostInputType, ['metadata', 'appearsIn']); - const postModelObject = getObjectType(parsed, 'Post') - const postMetaDataInputField = getFieldOnInputType(createPostInputType, 'metadata') - const postMetaDataField = getFieldOnObjectType(postModelObject, 'metadata') - // this checks that the non-model type was properly "unwrapped", renamed, and "rewrapped" - // in the generated CreatePostInput type - its types should be the same as in the Post @model type - verifyMatchingTypes(postMetaDataInputField.type, postMetaDataField.type) + const postModelObject = getObjectType(parsed, 'Post'); + const postMetaDataInputField = getFieldOnInputType(createPostInputType, 'metadata'); + const postMetaDataField = getFieldOnObjectType(postModelObject, 'metadata'); + // this checks that the non-model type was properly "unwrapped", renamed, and "rewrapped" + // in the generated CreatePostInput type - its types should be the same as in the Post @model type + verifyMatchingTypes(postMetaDataInputField.type, postMetaDataField.type); - expect(verifyInputCount(parsed, 'PostMetadataInput', 1)).toBeTruthy(); - expect(verifyInputCount(parsed, 'TagInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'PostMetadataInput', 1)).toBeTruthy(); + expect(verifyInputCount(parsed, 'TagInput', 1)).toBeTruthy(); }); test('Test DynamoDBModelTransformer with mutation input overrides when mutations are disabled', () => { - const validSchema = `type Post @model(mutations: null) { + const validSchema = `type Post @model(mutations: null) { id: ID! title: String! createdAt: String @@ -388,27 +382,25 @@ test('Test DynamoDBModelTransformer with mutation input overrides when mutations input DeletePostInput { different3: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const createPostInput = getInputType(parsed, 'CreatePostInput') - expectFieldsOnInputType(createPostInput, ['different']) - const updatePostInput = getInputType(parsed, 'UpdatePostInput') - expectFieldsOnInputType(updatePostInput, ['different2']) - const deletePostInput = getInputType(parsed, 'DeletePostInput') - expectFieldsOnInputType(deletePostInput, ['different3']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const createPostInput = getInputType(parsed, 'CreatePostInput'); + expectFieldsOnInputType(createPostInput, ['different']); + const updatePostInput = getInputType(parsed, 'UpdatePostInput'); + expectFieldsOnInputType(updatePostInput, ['different2']); + const deletePostInput = getInputType(parsed, 'DeletePostInput'); + expectFieldsOnInputType(deletePostInput, ['different3']); }); test('Test DynamoDBModelTransformer with mutation input overrides when mutations are enabled', () => { - const validSchema = `type Post @model { + const validSchema = `type Post @model { id: ID! title: String! createdAt: String @@ -424,82 +416,75 @@ test('Test DynamoDBModelTransformer with mutation input overrides when mutations input DeletePostInput { different3: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const createPostInput = getInputType(parsed, 'CreatePostInput') - expectFieldsOnInputType(createPostInput, ['different']) - const updatePostInput = getInputType(parsed, 'UpdatePostInput') - expectFieldsOnInputType(updatePostInput, ['different2']) - const deletePostInput = getInputType(parsed, 'DeletePostInput') - expectFieldsOnInputType(deletePostInput, ['different3']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const createPostInput = getInputType(parsed, 'CreatePostInput'); + expectFieldsOnInputType(createPostInput, ['different']); + const updatePostInput = getInputType(parsed, 'UpdatePostInput'); + expectFieldsOnInputType(updatePostInput, ['different2']); + const deletePostInput = getInputType(parsed, 'DeletePostInput'); + expectFieldsOnInputType(deletePostInput, ['different3']); }); function expectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } function expectFieldsOnInputType(type: InputObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } function getFieldOnInputType(type: InputObjectTypeDefinitionNode, field: string): InputValueDefinitionNode { - return type.fields.find((f: InputValueDefinitionNode) => f.name.value === field) + return type.fields.find((f: InputValueDefinitionNode) => f.name.value === field); } function getFieldOnObjectType(type: ObjectTypeDefinitionNode, field: string): FieldDefinitionNode { - return type.fields.find((f: FieldDefinitionNode) => f.name.value === field) + return type.fields.find((f: FieldDefinitionNode) => f.name.value === field); } function doNotExpectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - expect( - type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - ).toBeUndefined() - } + for (const fieldName of fields) { + expect(type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName)).toBeUndefined(); + } } function getObjectType(doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type - ) as ObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; } function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type - ) as InputObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type) as + | InputObjectTypeDefinitionNode + | undefined; } function verifyInputCount(doc: DocumentNode, type: string, count: number): boolean { - return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; + return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; } function verifyMatchingTypes(t1: TypeNode, t2: TypeNode): boolean { - if (t1.kind !== t2.kind) { - return false - } + if (t1.kind !== t2.kind) { + return false; + } - if ( - t1.kind !== Kind.NAMED_TYPE && - t2.kind !== Kind.NAMED_TYPE - ) { - verifyMatchingTypes(t1.type, t2.type) - } else { - return false - } -} \ No newline at end of file + if (t1.kind !== Kind.NAMED_TYPE && t2.kind !== Kind.NAMED_TYPE) { + verifyMatchingTypes(t1.type, t2.type); + } else { + return false; + } +} diff --git a/packages/graphql-dynamodb-transformer/src/definitions.ts b/packages/graphql-dynamodb-transformer/src/definitions.ts index ec582e25b4..c4b7dbb082 100644 --- a/packages/graphql-dynamodb-transformer/src/definitions.ts +++ b/packages/graphql-dynamodb-transformer/src/definitions.ts @@ -1,579 +1,554 @@ import { - ObjectTypeDefinitionNode, InputObjectTypeDefinitionNode, - InputValueDefinitionNode, FieldDefinitionNode, Kind, TypeNode, - EnumTypeDefinitionNode, ObjectTypeExtensionNode, - TypeDefinitionNode, - NamedTypeNode, - DirectiveNode, - InterfaceTypeDefinitionNode, -} from 'graphql' + ObjectTypeDefinitionNode, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + FieldDefinitionNode, + Kind, + TypeNode, + EnumTypeDefinitionNode, + ObjectTypeExtensionNode, + TypeDefinitionNode, + NamedTypeNode, + DirectiveNode, + InterfaceTypeDefinitionNode, +} from 'graphql'; import { - wrapNonNull, unwrapNonNull, makeNamedType, toUpper, graphqlName, makeListType, - isScalar, getBaseType, blankObjectExtension, extensionWithFields, makeField, - makeInputValueDefinition, - ModelResourceIDs, - makeDirective, - makeArgument, - makeValueNode, - withNamedNodeNamed, - isListType -} from 'graphql-transformer-common' + wrapNonNull, + unwrapNonNull, + makeNamedType, + toUpper, + graphqlName, + makeListType, + isScalar, + getBaseType, + blankObjectExtension, + extensionWithFields, + makeField, + makeInputValueDefinition, + ModelResourceIDs, + makeDirective, + makeArgument, + makeValueNode, + withNamedNodeNamed, + isListType, +} from 'graphql-transformer-common'; import { TransformerContext } from 'graphql-transformer-core'; -const STRING_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between', 'beginsWith'] -const ID_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between', 'beginsWith'] -const INT_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between'] -const FLOAT_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between'] -const BOOLEAN_CONDITIONS = ['ne', 'eq'] +const STRING_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between', 'beginsWith']; +const ID_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between', 'beginsWith']; +const INT_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between']; +const FLOAT_CONDITIONS = ['ne', 'eq', 'le', 'lt', 'ge', 'gt', 'contains', 'notContains', 'between']; +const BOOLEAN_CONDITIONS = ['ne', 'eq']; export function getNonModelObjectArray( - obj: ObjectTypeDefinitionNode, - ctx: TransformerContext, - pMap: Map + obj: ObjectTypeDefinitionNode, + ctx: TransformerContext, + pMap: Map ): ObjectTypeDefinitionNode[] { + // loop over all fields in the object, picking out all nonscalars that are not @model types + for (const field of obj.fields) { + if (!isScalar(field.type)) { + const def = ctx.getType(getBaseType(field.type)); - // loop over all fields in the object, picking out all nonscalars that are not @model types - for (const field of obj.fields) { - if (!isScalar(field.type)) { - const def = ctx.getType(getBaseType(field.type)) - - if ( - def && - def.kind === Kind.OBJECT_TYPE_DEFINITION && - !def.directives.find(e => e.name.value === 'model') && - pMap.get(def.name.value) === undefined - ) { - // recursively find any non @model types referenced by the current - // non @model type - pMap.set(def.name.value, def) - getNonModelObjectArray(def, ctx, pMap) - } - } + if ( + def && + def.kind === Kind.OBJECT_TYPE_DEFINITION && + !def.directives.find(e => e.name.value === 'model') && + pMap.get(def.name.value) === undefined + ) { + // recursively find any non @model types referenced by the current + // non @model type + pMap.set(def.name.value, def); + getNonModelObjectArray(def, ctx, pMap); + } } + } - return Array.from(pMap.values()) + return Array.from(pMap.values()); } export function makeNonModelInputObject( - obj: ObjectTypeDefinitionNode, - nonModelTypes: ObjectTypeDefinitionNode[], - ctx: TransformerContext + obj: ObjectTypeDefinitionNode, + nonModelTypes: ObjectTypeDefinitionNode[], + ctx: TransformerContext ): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.NonModelInputObjectName(obj.name.value) - const fields: InputValueDefinitionNode[] = obj.fields - .filter((field: FieldDefinitionNode) => { - const fieldType = ctx.getType(getBaseType(field.type)) - if (field.name.value === 'id') { - return false; - } - if ( - isScalar(field.type) || - nonModelTypes.find(e => e.name.value === getBaseType(field.type)) || - (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) - ) { - return true; - } - return false; - }) - .map( - (field: FieldDefinitionNode) => { - const type = nonModelTypes.find(e => e.name.value === getBaseType(field.type)) ? - withNamedNodeNamed(field.type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))) : - field.type - return { - kind: Kind.INPUT_VALUE_DEFINITION, - name: field.name, - type: type, - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - } - } - ) - return { - kind: 'InputObjectTypeDefinition', + const name = ModelResourceIDs.NonModelInputObjectName(obj.name.value); + const fields: InputValueDefinitionNode[] = obj.fields + .filter((field: FieldDefinitionNode) => { + const fieldType = ctx.getType(getBaseType(field.type)); + if (field.name.value === 'id') { + return false; + } + if ( + isScalar(field.type) || + nonModelTypes.find(e => e.name.value === getBaseType(field.type)) || + (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) + ) { + return true; + } + return false; + }) + .map((field: FieldDefinitionNode) => { + const type = nonModelTypes.find(e => e.name.value === getBaseType(field.type)) + ? withNamedNodeNamed(field.type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))) + : field.type; + return { + kind: Kind.INPUT_VALUE_DEFINITION, + name: field.name, + type: type, // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + // description: field.description, + directives: [], + }; + }); + return { + kind: 'InputObjectTypeDefinition', + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } export function makeCreateInputObject( - obj: ObjectTypeDefinitionNode, - nonModelTypes: ObjectTypeDefinitionNode[], - ctx: TransformerContext + obj: ObjectTypeDefinitionNode, + nonModelTypes: ObjectTypeDefinitionNode[], + ctx: TransformerContext ): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.ModelCreateInputObjectName(obj.name.value) - const fields: InputValueDefinitionNode[] = obj.fields - .filter((field: FieldDefinitionNode) => { - const fieldType = ctx.getType(getBaseType(field.type)) - if ( - isScalar(field.type) || - nonModelTypes.find(e => e.name.value === getBaseType(field.type)) || - (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) - ) { - return true; - } - return false; - }) - .map( - (field: FieldDefinitionNode) => { - let type; - if (field.name.value === 'id') { - // ids are always optional. when provided the value is used. - // when not provided the value is not used. - type = { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: 'ID' - } - } - } else { - type = nonModelTypes.find(e => e.name.value === getBaseType(field.type)) ? - withNamedNodeNamed(field.type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))) : - field.type - } - return { - kind: Kind.INPUT_VALUE_DEFINITION, - name: field.name, - type: type, - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - } - } - ) - return { - kind: 'InputObjectTypeDefinition', + const name = ModelResourceIDs.ModelCreateInputObjectName(obj.name.value); + const fields: InputValueDefinitionNode[] = obj.fields + .filter((field: FieldDefinitionNode) => { + const fieldType = ctx.getType(getBaseType(field.type)); + if ( + isScalar(field.type) || + nonModelTypes.find(e => e.name.value === getBaseType(field.type)) || + (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) + ) { + return true; + } + return false; + }) + .map((field: FieldDefinitionNode) => { + let type; + if (field.name.value === 'id') { + // ids are always optional. when provided the value is used. + // when not provided the value is not used. + type = { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'ID', + }, + }; + } else { + type = nonModelTypes.find(e => e.name.value === getBaseType(field.type)) + ? withNamedNodeNamed(field.type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))) + : field.type; + } + return { + kind: Kind.INPUT_VALUE_DEFINITION, + name: field.name, + type: type, // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + // description: field.description, + directives: [], + }; + }); + return { + kind: 'InputObjectTypeDefinition', + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } export function makeUpdateInputObject( - obj: ObjectTypeDefinitionNode, - nonModelTypes: ObjectTypeDefinitionNode[], - ctx: TransformerContext + obj: ObjectTypeDefinitionNode, + nonModelTypes: ObjectTypeDefinitionNode[], + ctx: TransformerContext ): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.ModelUpdateInputObjectName(obj.name.value) - const fields: InputValueDefinitionNode[] = obj.fields - .filter(f => { - const fieldType = ctx.getType(getBaseType(f.type)) - if ( - isScalar(f.type) || - nonModelTypes.find(e => e.name.value === getBaseType(f.type)) || - (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) - ) { - return true - } else { - return false - } - }) - .map( - (field: FieldDefinitionNode) => { - let type; - if (field.name.value === 'id') { - type = wrapNonNull(field.type) - } else { - type = unwrapNonNull(field.type) - } - type = nonModelTypes.find(e => e.name.value === getBaseType(field.type)) ? - withNamedNodeNamed(type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))) : - type - return { - kind: Kind.INPUT_VALUE_DEFINITION, - name: field.name, - type: type, - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - } - } - ) - return { - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + const name = ModelResourceIDs.ModelUpdateInputObjectName(obj.name.value); + const fields: InputValueDefinitionNode[] = obj.fields + .filter(f => { + const fieldType = ctx.getType(getBaseType(f.type)); + if ( + isScalar(f.type) || + nonModelTypes.find(e => e.name.value === getBaseType(f.type)) || + (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) + ) { + return true; + } else { + return false; + } + }) + .map((field: FieldDefinitionNode) => { + let type; + if (field.name.value === 'id') { + type = wrapNonNull(field.type); + } else { + type = unwrapNonNull(field.type); + } + type = nonModelTypes.find(e => e.name.value === getBaseType(field.type)) + ? withNamedNodeNamed(type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))) + : type; + return { + kind: Kind.INPUT_VALUE_DEFINITION, + name: field.name, + type: type, // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + // description: field.description, + directives: [], + }; + }); + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } export function makeDeleteInputObject(obj: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.ModelDeleteInputObjectName(obj.name.value) - return { - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + const name = ModelResourceIDs.ModelDeleteInputObjectName(obj.name.value); + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} delete mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields: [ + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { kind: 'Name', value: 'id' }, + type: makeNamedType('ID'), // TODO: Service does not support new style descriptions so wait. // description: { // kind: 'StringValue', - // value: `Input type for ${obj.name.value} delete mutations` + // value: `The id of the ${obj.name.value} to delete.` // }, - name: { - kind: 'Name', - value: name - }, - fields: [{ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { kind: 'Name', value: 'id' }, - type: makeNamedType('ID'), - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `The id of the ${obj.name.value} to delete.` - // }, - directives: [] - }], - directives: [] - } + directives: [], + }, + ], + directives: [], + }; } export function makeModelXFilterInputObject( - obj: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - ctx: TransformerContext + obj: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + ctx: TransformerContext ): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.ModelFilterInputTypeName(obj.name.value) - const fields: InputValueDefinitionNode[] = obj.fields - .filter((field: FieldDefinitionNode) => { - const fieldType = ctx.getType(getBaseType(field.type)) - if ( - isScalar(field.type) || - (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) - ) { - return true; - } - }) - .map( - (field: FieldDefinitionNode) => { - const baseType = getBaseType(field.type) - const fieldType = ctx.getType(baseType) - const isList = isListType(field.type) - const isEnumType = fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION; - const filterTypeName = (isEnumType && isList) - ? ModelResourceIDs.ModelFilterListInputTypeName(baseType) - : ModelResourceIDs.ModelFilterInputTypeName(baseType) - - return { - kind: Kind.INPUT_VALUE_DEFINITION, - name: field.name, - type: makeNamedType(filterTypeName), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - } - } - ) + const name = ModelResourceIDs.ModelFilterInputTypeName(obj.name.value); + const fields: InputValueDefinitionNode[] = obj.fields + .filter((field: FieldDefinitionNode) => { + const fieldType = ctx.getType(getBaseType(field.type)); + if (isScalar(field.type) || (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION)) { + return true; + } + }) + .map((field: FieldDefinitionNode) => { + const baseType = getBaseType(field.type); + const fieldType = ctx.getType(baseType); + const isList = isListType(field.type); + const isEnumType = fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION; + const filterTypeName = + isEnumType && isList + ? ModelResourceIDs.ModelFilterListInputTypeName(baseType) + : ModelResourceIDs.ModelFilterInputTypeName(baseType); - fields.push( - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'and' - }, - type: makeListType(makeNamedType(name)), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - }, - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'or' - }, - type: makeListType(makeNamedType(name)), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - }, - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'not' - }, - type: makeNamedType(name), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - } - ) - - return { - kind: 'InputObjectTypeDefinition', + return { + kind: Kind.INPUT_VALUE_DEFINITION, + name: field.name, + type: makeNamedType(filterTypeName), // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] + // description: field.description, + directives: [], + }; + }); + + fields.push( + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'and', + }, + type: makeListType(makeNamedType(name)), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], + }, + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'or', + }, + type: makeListType(makeNamedType(name)), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], + }, + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'not', + }, + type: makeNamedType(name), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], } + ); + + return { + kind: 'InputObjectTypeDefinition', + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } -export function makeEnumFilterInputObjects( - obj: ObjectTypeDefinitionNode, - ctx: TransformerContext -) : InputObjectTypeDefinitionNode[] { - return obj.fields - .filter((field: FieldDefinitionNode) => { - const fieldType = ctx.getType(getBaseType(field.type)) - return (fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION) - }) - .map((enumField: FieldDefinitionNode) => { - const typeName = getBaseType(enumField.type); - const isList = isListType(enumField.type) - const name = isList - ? ModelResourceIDs.ModelFilterListInputTypeName(typeName) - : ModelResourceIDs.ModelFilterInputTypeName(typeName); - const fields = []; +export function makeEnumFilterInputObjects(obj: ObjectTypeDefinitionNode, ctx: TransformerContext): InputObjectTypeDefinitionNode[] { + return obj.fields + .filter((field: FieldDefinitionNode) => { + const fieldType = ctx.getType(getBaseType(field.type)); + return fieldType && fieldType.kind === Kind.ENUM_TYPE_DEFINITION; + }) + .map((enumField: FieldDefinitionNode) => { + const typeName = getBaseType(enumField.type); + const isList = isListType(enumField.type); + const name = isList ? ModelResourceIDs.ModelFilterListInputTypeName(typeName) : ModelResourceIDs.ModelFilterInputTypeName(typeName); + const fields = []; - fields.push({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'eq' - }, - type: isList ? makeListType(makeNamedType(typeName)) : makeNamedType(typeName), - directives: [] - }); + fields.push({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'eq', + }, + type: isList ? makeListType(makeNamedType(typeName)) : makeNamedType(typeName), + directives: [], + }); - fields.push({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'ne' - }, - type: isList ? makeListType(makeNamedType(typeName)) : makeNamedType(typeName), - directives: [] - }); + fields.push({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'ne', + }, + type: isList ? makeListType(makeNamedType(typeName)) : makeNamedType(typeName), + directives: [], + }); - if (isList) { - fields.push({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'contains' - }, - type: makeNamedType(typeName), - directives: [] - }); + if (isList) { + fields.push({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'contains', + }, + type: makeNamedType(typeName), + directives: [], + }); - fields.push({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'notContains' - }, - type: makeNamedType(typeName), - directives: [] - }); - } + fields.push({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'notContains', + }, + type: makeNamedType(typeName), + directives: [], + }); + } - return ({ - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [], - } as InputObjectTypeDefinitionNode) - }) + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + } as InputObjectTypeDefinitionNode; + }); } export function makeModelSortDirectionEnumObject(): EnumTypeDefinitionNode { - const name = graphqlName('ModelSortDirection') - return { - kind: Kind.ENUM_TYPE_DEFINITION, - name: { - kind: 'Name', - value: name - }, - values: [ - { - kind: Kind.ENUM_VALUE_DEFINITION, - name: { kind: 'Name', value: 'ASC' }, - directives: [] - }, - { - kind: Kind.ENUM_VALUE_DEFINITION, - name: { kind: 'Name', value: 'DESC' }, - directives: [] - } - ], - directives: [] - } + const name = graphqlName('ModelSortDirection'); + return { + kind: Kind.ENUM_TYPE_DEFINITION, + name: { + kind: 'Name', + value: name, + }, + values: [ + { + kind: Kind.ENUM_VALUE_DEFINITION, + name: { kind: 'Name', value: 'ASC' }, + directives: [], + }, + { + kind: Kind.ENUM_VALUE_DEFINITION, + name: { kind: 'Name', value: 'DESC' }, + directives: [], + }, + ], + directives: [], + }; } export function makeModelScalarFilterInputObject(type: string): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.ModelFilterInputTypeName(type) - let conditions = getScalarConditions(type) - const fields: InputValueDefinitionNode[] = conditions - .map((condition: string) => ({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { kind: "Name" as "Name", value: condition }, - type: getScalarFilterInputType(condition, type, name), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - })) - return { - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + const name = ModelResourceIDs.ModelFilterInputTypeName(type); + let conditions = getScalarConditions(type); + const fields: InputValueDefinitionNode[] = conditions.map((condition: string) => ({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: { kind: 'Name' as 'Name', value: condition }, + type: getScalarFilterInputType(condition, type, name), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], + })); + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } function getScalarFilterInputType(condition: string, type: string, filterInputName: string): TypeNode { - switch (condition) { - case 'between': - return makeListType(makeNamedType(type)) - case 'and': - case 'or': - return makeNamedType(filterInputName) - default: - return makeNamedType(type) - } + switch (condition) { + case 'between': + return makeListType(makeNamedType(type)); + case 'and': + case 'or': + return makeNamedType(filterInputName); + default: + return makeNamedType(type); + } } function getScalarConditions(type: string): string[] { - switch (type) { - case 'String': - return STRING_CONDITIONS - case 'ID': - return ID_CONDITIONS - case 'Int': - return INT_CONDITIONS - case 'Float': - return FLOAT_CONDITIONS - case 'Boolean': - return BOOLEAN_CONDITIONS - default: - throw 'Valid types are String, ID, Int, Float, Boolean' - } + switch (type) { + case 'String': + return STRING_CONDITIONS; + case 'ID': + return ID_CONDITIONS; + case 'Int': + return INT_CONDITIONS; + case 'Float': + return FLOAT_CONDITIONS; + case 'Boolean': + return BOOLEAN_CONDITIONS; + default: + throw 'Valid types are String, ID, Int, Float, Boolean'; + } } export function makeModelConnectionType(typeName: string): ObjectTypeExtensionNode { - const connectionName = ModelResourceIDs.ModelConnectionTypeName(typeName) - let connectionTypeExtension = blankObjectExtension(connectionName) - connectionTypeExtension = extensionWithFields( - connectionTypeExtension, - [makeField( - 'items', - [], - makeListType(makeNamedType(typeName)) - )] - ) - connectionTypeExtension = extensionWithFields( - connectionTypeExtension, - [makeField( - 'nextToken', - [], - makeNamedType('String') - )] - ) - return connectionTypeExtension + const connectionName = ModelResourceIDs.ModelConnectionTypeName(typeName); + let connectionTypeExtension = blankObjectExtension(connectionName); + connectionTypeExtension = extensionWithFields(connectionTypeExtension, [makeField('items', [], makeListType(makeNamedType(typeName)))]); + connectionTypeExtension = extensionWithFields(connectionTypeExtension, [makeField('nextToken', [], makeNamedType('String'))]); + return connectionTypeExtension; } export function makeSubscriptionField(fieldName: string, returnTypeName: string, mutations: string[]): FieldDefinitionNode { - return makeField( - fieldName, - [], - makeNamedType(returnTypeName), - [ - makeDirective( - 'aws_subscribe', - [makeArgument('mutations', makeValueNode(mutations))] - ) - ] - ) + return makeField(fieldName, [], makeNamedType(returnTypeName), [ + makeDirective('aws_subscribe', [makeArgument('mutations', makeValueNode(mutations))]), + ]); } export type SortKeyFieldInfoTypeName = 'Composite' | string; export interface SortKeyFieldInfo { - // The name of the sort key field. - fieldName: string; - // The GraphQL type of the sort key field. - typeName: SortKeyFieldInfoTypeName; - // Name of the model this field is on. - model?: string; - // The name of the key that this sortKey is on. - keyName?: string; + // The name of the sort key field. + fieldName: string; + // The GraphQL type of the sort key field. + typeName: SortKeyFieldInfoTypeName; + // Name of the model this field is on. + model?: string; + // The name of the key that this sortKey is on. + keyName?: string; } -export function makeModelConnectionField(fieldName: string, returnTypeName: string, sortKeyInfo?: SortKeyFieldInfo, directives?: DirectiveNode[]): FieldDefinitionNode { - const args = [ - makeInputValueDefinition('filter', makeNamedType(ModelResourceIDs.ModelFilterInputTypeName(returnTypeName))), - makeInputValueDefinition('sortDirection', makeNamedType('ModelSortDirection')), - makeInputValueDefinition('limit', makeNamedType('Int')), - makeInputValueDefinition('nextToken', makeNamedType('String')) - ]; - if (sortKeyInfo) { - let namedType : NamedTypeNode; - if (sortKeyInfo.typeName === 'Composite') { - namedType = makeNamedType(ModelResourceIDs.ModelCompositeKeyConditionInputTypeName(sortKeyInfo.model, toUpper(sortKeyInfo.keyName))); - } else { - namedType = makeNamedType(ModelResourceIDs.ModelKeyConditionInputTypeName(sortKeyInfo.typeName)) - } - - args.unshift( - makeInputValueDefinition(sortKeyInfo.fieldName, namedType) - ); +export function makeModelConnectionField( + fieldName: string, + returnTypeName: string, + sortKeyInfo?: SortKeyFieldInfo, + directives?: DirectiveNode[] +): FieldDefinitionNode { + const args = [ + makeInputValueDefinition('filter', makeNamedType(ModelResourceIDs.ModelFilterInputTypeName(returnTypeName))), + makeInputValueDefinition('sortDirection', makeNamedType('ModelSortDirection')), + makeInputValueDefinition('limit', makeNamedType('Int')), + makeInputValueDefinition('nextToken', makeNamedType('String')), + ]; + if (sortKeyInfo) { + let namedType: NamedTypeNode; + if (sortKeyInfo.typeName === 'Composite') { + namedType = makeNamedType(ModelResourceIDs.ModelCompositeKeyConditionInputTypeName(sortKeyInfo.model, toUpper(sortKeyInfo.keyName))); + } else { + namedType = makeNamedType(ModelResourceIDs.ModelKeyConditionInputTypeName(sortKeyInfo.typeName)); } - return makeField( - fieldName, - args, - makeNamedType(ModelResourceIDs.ModelConnectionTypeName(returnTypeName)), - directives - ); + + args.unshift(makeInputValueDefinition(sortKeyInfo.fieldName, namedType)); + } + return makeField(fieldName, args, makeNamedType(ModelResourceIDs.ModelConnectionTypeName(returnTypeName)), directives); } export function makeScalarFilterInputs(): InputObjectTypeDefinitionNode[] { - return [ - makeModelScalarFilterInputObject('String'), - makeModelScalarFilterInputObject('ID'), - makeModelScalarFilterInputObject('Int'), - makeModelScalarFilterInputObject('Float'), - makeModelScalarFilterInputObject('Boolean') - ]; + return [ + makeModelScalarFilterInputObject('String'), + makeModelScalarFilterInputObject('ID'), + makeModelScalarFilterInputObject('Int'), + makeModelScalarFilterInputObject('Float'), + makeModelScalarFilterInputObject('Boolean'), + ]; } diff --git a/packages/graphql-dynamodb-transformer/src/index.ts b/packages/graphql-dynamodb-transformer/src/index.ts index 0ec6045736..499b666a9b 100644 --- a/packages/graphql-dynamodb-transformer/src/index.ts +++ b/packages/graphql-dynamodb-transformer/src/index.ts @@ -1,5 +1,5 @@ -import { DynamoDBModelTransformer } from './DynamoDBModelTransformer' -export * from './DynamoDBModelTransformer' -export default DynamoDBModelTransformer -export * from './definitions' -export * from './ModelDirectiveArgs'; \ No newline at end of file +import { DynamoDBModelTransformer } from './DynamoDBModelTransformer'; +export * from './DynamoDBModelTransformer'; +export default DynamoDBModelTransformer; +export * from './definitions'; +export * from './ModelDirectiveArgs'; diff --git a/packages/graphql-dynamodb-transformer/src/resources.ts b/packages/graphql-dynamodb-transformer/src/resources.ts index 05ee83ac6d..49a611eb4a 100644 --- a/packages/graphql-dynamodb-transformer/src/resources.ts +++ b/packages/graphql-dynamodb-transformer/src/resources.ts @@ -1,670 +1,668 @@ -import { DynamoDB, AppSync, IAM, Template, Fn, StringParameter, NumberParameter, Refs, IntrinsicFunction, DeletionPolicy } from 'cloudform-types' +import { + DynamoDB, + AppSync, + IAM, + Template, + Fn, + StringParameter, + NumberParameter, + Refs, + IntrinsicFunction, + DeletionPolicy, +} from 'cloudform-types'; import Output from 'cloudform-types/types/output'; import { - DynamoDBMappingTemplate, printBlock, str, print, - ref, obj, set, nul, - ifElse, compoundExpression, qref, bool, equals, iff, raw, comment, forEach, list -} from 'graphql-mapping-template' -import { ResourceConstants, plurality, graphqlName, toUpper, ModelResourceIDs } from 'graphql-transformer-common' + DynamoDBMappingTemplate, + printBlock, + str, + print, + ref, + obj, + set, + nul, + ifElse, + compoundExpression, + qref, + bool, + equals, + iff, + raw, + comment, + forEach, + list, +} from 'graphql-mapping-template'; +import { ResourceConstants, plurality, graphqlName, toUpper, ModelResourceIDs } from 'graphql-transformer-common'; export class ResourceFactory { + public makeParams() { + return { + [ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS]: new NumberParameter({ + Description: 'The number of read IOPS the table should support.', + Default: 5, + }), + [ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS]: new NumberParameter({ + Description: 'The number of write IOPS the table should support.', + Default: 5, + }), + [ResourceConstants.PARAMETERS.DynamoDBBillingMode]: new StringParameter({ + Description: 'Configure @model types to create DynamoDB tables with PAY_PER_REQUEST or PROVISIONED billing modes.', + Default: 'PAY_PER_REQUEST', + AllowedValues: ['PAY_PER_REQUEST', 'PROVISIONED'], + }), + [ResourceConstants.PARAMETERS.DynamoDBEnablePointInTimeRecovery]: new StringParameter({ + Description: 'Whether to enable Point in Time Recovery on the table', + Default: 'false', + AllowedValues: ['true', 'false'], + }), + [ResourceConstants.PARAMETERS.DynamoDBEnableServerSideEncryption]: new StringParameter({ + Description: 'Enable server side encryption powered by KMS.', + Default: 'true', + AllowedValues: ['true', 'false'], + }), + }; + } - public makeParams() { - return { - [ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS]: new NumberParameter({ - Description: 'The number of read IOPS the table should support.', - Default: 5 - }), - [ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS]: new NumberParameter({ - Description: 'The number of write IOPS the table should support.', - Default: 5 - }), - [ResourceConstants.PARAMETERS.DynamoDBBillingMode]: new StringParameter({ - Description: 'Configure @model types to create DynamoDB tables with PAY_PER_REQUEST or PROVISIONED billing modes.', - Default: 'PAY_PER_REQUEST', - AllowedValues: [ - 'PAY_PER_REQUEST', - 'PROVISIONED' - ] - }), - [ResourceConstants.PARAMETERS.DynamoDBEnablePointInTimeRecovery]: new StringParameter({ - Description: 'Whether to enable Point in Time Recovery on the table', - Default: 'false', - AllowedValues: [ - 'true', - 'false' - ] - }), - [ResourceConstants.PARAMETERS.DynamoDBEnableServerSideEncryption]: new StringParameter({ - Description: 'Enable server side encryption powered by KMS.', - Default: 'true', - AllowedValues: [ - 'true', - 'false' - ] - }) - } - } - - /** - * Creates the barebones template for an application. - */ - public initTemplate(): Template { - return { - Parameters: this.makeParams(), - Resources: { - [ResourceConstants.RESOURCES.GraphQLAPILogicalID]: this.makeAppSyncAPI() - }, - Outputs: { - [ResourceConstants.OUTPUTS.GraphQLAPIIdOutput]: this.makeAPIIDOutput(), - [ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput]: this.makeAPIEndpointOutput() - }, - Conditions: { - [ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling]: - Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBBillingMode), 'PAY_PER_REQUEST'), + /** + * Creates the barebones template for an application. + */ + public initTemplate(): Template { + return { + Parameters: this.makeParams(), + Resources: { + [ResourceConstants.RESOURCES.GraphQLAPILogicalID]: this.makeAppSyncAPI(), + }, + Outputs: { + [ResourceConstants.OUTPUTS.GraphQLAPIIdOutput]: this.makeAPIIDOutput(), + [ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput]: this.makeAPIEndpointOutput(), + }, + Conditions: { + [ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling]: Fn.Equals( + Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBBillingMode), + 'PAY_PER_REQUEST' + ), - [ResourceConstants.CONDITIONS.ShouldUsePointInTimeRecovery]: - Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBEnablePointInTimeRecovery), 'true'), - [ResourceConstants.CONDITIONS.ShouldUseServerSideEncryption]: - Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBEnableServerSideEncryption), 'true'), - } - } - } + [ResourceConstants.CONDITIONS.ShouldUsePointInTimeRecovery]: Fn.Equals( + Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBEnablePointInTimeRecovery), + 'true' + ), + [ResourceConstants.CONDITIONS.ShouldUseServerSideEncryption]: Fn.Equals( + Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBEnableServerSideEncryption), + 'true' + ), + }, + }; + } - /** - * Create the AppSync API. - */ - public makeAppSyncAPI() { - return new AppSync.GraphQLApi({ - Name: Fn.If( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - Fn.Join('-', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiName), Fn.Ref(ResourceConstants.PARAMETERS.Env)]), - Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiName) - ), - AuthenticationType: 'API_KEY' - }) - } + /** + * Create the AppSync API. + */ + public makeAppSyncAPI() { + return new AppSync.GraphQLApi({ + Name: Fn.If( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + Fn.Join('-', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiName), Fn.Ref(ResourceConstants.PARAMETERS.Env)]), + Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiName) + ), + AuthenticationType: 'API_KEY', + }); + } - public makeAppSyncSchema(schema: string) { - return new AppSync.GraphQLSchema({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Definition: schema - }) - } + public makeAppSyncSchema(schema: string) { + return new AppSync.GraphQLSchema({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Definition: schema, + }); + } - /** - * Outputs - */ - public makeAPIIDOutput(): Output { - return { - Description: "Your GraphQL API ID.", - Value: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Export: { - Name: Fn.Join(':', [Refs.StackName, "GraphQLApiId"]) - } - } - } + /** + * Outputs + */ + public makeAPIIDOutput(): Output { + return { + Description: 'Your GraphQL API ID.', + Value: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Export: { + Name: Fn.Join(':', [Refs.StackName, 'GraphQLApiId']), + }, + }; + } - public makeAPIEndpointOutput(): Output { - return { - Description: "Your GraphQL API endpoint.", - Value: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'GraphQLUrl'), - Export: { - Name: Fn.Join(':', [Refs.StackName, "GraphQLApiEndpoint"]) - } - } - } + public makeAPIEndpointOutput(): Output { + return { + Description: 'Your GraphQL API endpoint.', + Value: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'GraphQLUrl'), + Export: { + Name: Fn.Join(':', [Refs.StackName, 'GraphQLApiEndpoint']), + }, + }; + } - public makeTableStreamArnOutput(resourceId: string): Output { - return { - Description: "Your DynamoDB table StreamArn.", - Value: Fn.GetAtt(resourceId, "StreamArn"), - Export: { - Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), "GetAtt", resourceId, "StreamArn"]) - } - } - } + public makeTableStreamArnOutput(resourceId: string): Output { + return { + Description: 'Your DynamoDB table StreamArn.', + Value: Fn.GetAtt(resourceId, 'StreamArn'), + Export: { + Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), 'GetAtt', resourceId, 'StreamArn']), + }, + }; + } - public makeDataSourceOutput(resourceId: string): Output { - return { - Description: "Your model DataSource name.", - Value: Fn.GetAtt(resourceId, "Name"), - Export: { - Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), "GetAtt", resourceId, "Name"]) - } - } - } + public makeDataSourceOutput(resourceId: string): Output { + return { + Description: 'Your model DataSource name.', + Value: Fn.GetAtt(resourceId, 'Name'), + Export: { + Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), 'GetAtt', resourceId, 'Name']), + }, + }; + } - public makeTableNameOutput(resourceId: string): Output { - return { - Description: "Your DynamoDB table name.", - Value: Fn.Ref(resourceId), - Export: { - Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), "GetAtt", resourceId, "Name"]) - } - } - } + public makeTableNameOutput(resourceId: string): Output { + return { + Description: 'Your DynamoDB table name.', + Value: Fn.Ref(resourceId), + Export: { + Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), 'GetAtt', resourceId, 'Name']), + }, + }; + } - /** - * Create a DynamoDB table for a specific type. - */ - public makeModelTable(typeName: string, hashKey: string = 'id', rangeKey?: string, deletionPolicy: DeletionPolicy = DeletionPolicy.Delete) { - const keySchema = hashKey && rangeKey ? [ + /** + * Create a DynamoDB table for a specific type. + */ + public makeModelTable( + typeName: string, + hashKey: string = 'id', + rangeKey?: string, + deletionPolicy: DeletionPolicy = DeletionPolicy.Delete + ) { + const keySchema = + hashKey && rangeKey + ? [ { - AttributeName: hashKey, - KeyType: 'HASH' - }, { - AttributeName: rangeKey, - KeyType: 'RANGE' - }] : [{ AttributeName: hashKey, KeyType: 'HASH' }] - const attributeDefinitions = hashKey && rangeKey ? [ + AttributeName: hashKey, + KeyType: 'HASH', + }, { - AttributeName: hashKey, - AttributeType: 'S' - }, { - AttributeName: rangeKey, - AttributeType: 'S' - }] : [{ AttributeName: hashKey, AttributeType: 'S' }] - return new DynamoDB.Table({ - TableName: this.dynamoDBTableName(typeName), - KeySchema: keySchema, - AttributeDefinitions: attributeDefinitions, - StreamSpecification: { - StreamViewType: 'NEW_AND_OLD_IMAGES' + AttributeName: rangeKey, + KeyType: 'RANGE', }, - BillingMode: Fn.If( - ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, - 'PAY_PER_REQUEST', - Refs.NoValue - ), - ProvisionedThroughput: Fn.If( - ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, - Refs.NoValue, - { - ReadCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS), - WriteCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS) - } - ) as any, - SSESpecification: { - SSEEnabled: Fn.If( - ResourceConstants.CONDITIONS.ShouldUseServerSideEncryption, - true, - false - ) + ] + : [{ AttributeName: hashKey, KeyType: 'HASH' }]; + const attributeDefinitions = + hashKey && rangeKey + ? [ + { + AttributeName: hashKey, + AttributeType: 'S', }, - PointInTimeRecoverySpecification: Fn.If( - ResourceConstants.CONDITIONS.ShouldUsePointInTimeRecovery, - { - PointInTimeRecoveryEnabled: true - }, - Refs.NoValue - ) as any, - }).deletionPolicy(deletionPolicy) - } + { + AttributeName: rangeKey, + AttributeType: 'S', + }, + ] + : [{ AttributeName: hashKey, AttributeType: 'S' }]; + return new DynamoDB.Table({ + TableName: this.dynamoDBTableName(typeName), + KeySchema: keySchema, + AttributeDefinitions: attributeDefinitions, + StreamSpecification: { + StreamViewType: 'NEW_AND_OLD_IMAGES', + }, + BillingMode: Fn.If(ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, 'PAY_PER_REQUEST', Refs.NoValue), + ProvisionedThroughput: Fn.If(ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, Refs.NoValue, { + ReadCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS), + WriteCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS), + }) as any, + SSESpecification: { + SSEEnabled: Fn.If(ResourceConstants.CONDITIONS.ShouldUseServerSideEncryption, true, false), + }, + PointInTimeRecoverySpecification: Fn.If( + ResourceConstants.CONDITIONS.ShouldUsePointInTimeRecovery, + { + PointInTimeRecoveryEnabled: true, + }, + Refs.NoValue + ) as any, + }).deletionPolicy(deletionPolicy); + } - private dynamoDBTableName(typeName: string): IntrinsicFunction { - return Fn.If( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - Fn.Join('-', [ - typeName, - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Fn.Ref(ResourceConstants.PARAMETERS.Env) - ]), - Fn.Join('-', [typeName, Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId')]) - ) - } + private dynamoDBTableName(typeName: string): IntrinsicFunction { + return Fn.If( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + Fn.Join('-', [ + typeName, + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Fn.Ref(ResourceConstants.PARAMETERS.Env), + ]), + Fn.Join('-', [typeName, Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId')]) + ); + } - /** - * Create a single role that has access to all the resources created by the - * transform. - * @param name The name of the IAM role to create. - */ - public makeIAMRole(typeName: string) { - return new IAM.Role({ - RoleName: Fn.If( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - Fn.Join('-', [ - typeName.slice(0, 21), // max of 64. 64-10-26-4-3 = 21 - 'role', // 4 - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), // 26 - Fn.Ref(ResourceConstants.PARAMETERS.Env) // 10 - ]), - Fn.Join('-', [ - typeName.slice(0, 31), // max of 64. 64-26-4-3 = 31 - 'role', - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId') - ]) - ), - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'appsync.amazonaws.com' - }, - Action: 'sts:AssumeRole' - } - ] + /** + * Create a single role that has access to all the resources created by the + * transform. + * @param name The name of the IAM role to create. + */ + public makeIAMRole(typeName: string) { + return new IAM.Role({ + RoleName: Fn.If( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + Fn.Join('-', [ + typeName.slice(0, 21), // max of 64. 64-10-26-4-3 = 21 + 'role', // 4 + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), // 26 + Fn.Ref(ResourceConstants.PARAMETERS.Env), // 10 + ]), + Fn.Join('-', [ + typeName.slice(0, 31), // max of 64. 64-26-4-3 = 31 + 'role', + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + ]) + ), + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'appsync.amazonaws.com', }, - Policies: [ - new IAM.Role.Policy({ - PolicyName: 'DynamoDBAccess', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'dynamodb:BatchGetItem', - 'dynamodb:BatchWriteItem', - 'dynamodb:PutItem', - 'dynamodb:DeleteItem', - 'dynamodb:GetItem', - 'dynamodb:Scan', - 'dynamodb:Query', - 'dynamodb:UpdateItem' - ], - Resource: [ - Fn.Sub( - 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}', - { - tablename: this.dynamoDBTableName(typeName) - } - ), - Fn.Sub( - 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*', - { - tablename: this.dynamoDBTableName(typeName) - } - ) - ] - } - ] - } - }) - ] - }) - } + Action: 'sts:AssumeRole', + }, + ], + }, + Policies: [ + new IAM.Role.Policy({ + PolicyName: 'DynamoDBAccess', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: [ + 'dynamodb:BatchGetItem', + 'dynamodb:BatchWriteItem', + 'dynamodb:PutItem', + 'dynamodb:DeleteItem', + 'dynamodb:GetItem', + 'dynamodb:Scan', + 'dynamodb:Query', + 'dynamodb:UpdateItem', + ], + Resource: [ + Fn.Sub('arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}', { + tablename: this.dynamoDBTableName(typeName), + }), + Fn.Sub('arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*', { + tablename: this.dynamoDBTableName(typeName), + }), + ], + }, + ], + }, + }), + ], + }); + } - /** - * Given the name of a data source and optional logical id return a CF - * spec for a data source pointing to the dynamodb table. - */ - public makeDynamoDBDataSource(tableId: string, iamRoleLogicalID: string, typeName: string) { - return new AppSync.DataSource({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Name: tableId, - Type: 'AMAZON_DYNAMODB', - ServiceRoleArn: Fn.GetAtt(iamRoleLogicalID, 'Arn'), - DynamoDBConfig: { - AwsRegion: Refs.Region, - TableName: this.dynamoDBTableName(typeName) - } - }).dependsOn([iamRoleLogicalID]) - } + /** + * Given the name of a data source and optional logical id return a CF + * spec for a data source pointing to the dynamodb table. + */ + public makeDynamoDBDataSource(tableId: string, iamRoleLogicalID: string, typeName: string) { + return new AppSync.DataSource({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Name: tableId, + Type: 'AMAZON_DYNAMODB', + ServiceRoleArn: Fn.GetAtt(iamRoleLogicalID, 'Arn'), + DynamoDBConfig: { + AwsRegion: Refs.Region, + TableName: this.dynamoDBTableName(typeName), + }, + }).dependsOn([iamRoleLogicalID]); + } - /** - * Create a resolver that creates an item in DynamoDB. - * @param type - */ - public makeCreateResolver(type: string, nameOverride?: string, mutationTypeName: string = 'Mutation') { - const fieldName = nameOverride ? nameOverride : graphqlName('create' + toUpper(type)) - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), - FieldName: fieldName, - TypeName: mutationTypeName, - RequestMappingTemplate: printBlock('Prepare DynamoDB PutItem Request')( - compoundExpression([ - qref('$context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601()))'), - qref('$context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))'), - qref(`$context.args.input.put("__typename", "${type}")`), - DynamoDBMappingTemplate.putItem({ - key: ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), - obj({ - id: raw(`$util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId()))`) - }), - true - ), - attributeValues: ref('util.dynamodb.toMapValuesJson($context.args.input)'), - condition: obj({ - expression: str(`attribute_not_exists(#id)`), - expressionNames: obj({ - "#id": str('id') - }) - }) - }), - ]) + /** + * Create a resolver that creates an item in DynamoDB. + * @param type + */ + public makeCreateResolver(type: string, nameOverride?: string, mutationTypeName: string = 'Mutation') { + const fieldName = nameOverride ? nameOverride : graphqlName('create' + toUpper(type)); + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), + FieldName: fieldName, + TypeName: mutationTypeName, + RequestMappingTemplate: printBlock('Prepare DynamoDB PutItem Request')( + compoundExpression([ + qref('$context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601()))'), + qref('$context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))'), + qref(`$context.args.input.put("__typename", "${type}")`), + DynamoDBMappingTemplate.putItem({ + key: ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), + obj({ + id: raw(`$util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId()))`), + }), + true ), - ResponseMappingTemplate: print( - ref('util.toJson($context.result)') - ) - }) - } + attributeValues: ref('util.dynamodb.toMapValuesJson($context.args.input)'), + condition: obj({ + expression: str(`attribute_not_exists(#id)`), + expressionNames: obj({ + '#id': str('id'), + }), + }), + }), + ]) + ), + ResponseMappingTemplate: print(ref('util.toJson($context.result)')), + }); + } - public makeUpdateResolver(type: string, nameOverride?: string, mutationTypeName: string = 'Mutation') { - const fieldName = nameOverride ? nameOverride : graphqlName(`update` + toUpper(type)) - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), - FieldName: fieldName, - TypeName: mutationTypeName, - RequestMappingTemplate: print( + public makeUpdateResolver(type: string, nameOverride?: string, mutationTypeName: string = 'Mutation') { + const fieldName = nameOverride ? nameOverride : graphqlName(`update` + toUpper(type)); + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), + FieldName: fieldName, + TypeName: mutationTypeName, + RequestMappingTemplate: print( + compoundExpression([ + ifElse( + raw(`$${ResourceConstants.SNIPPETS.AuthCondition} && $${ResourceConstants.SNIPPETS.AuthCondition}.expression != ""`), + compoundExpression([ + set(ref('condition'), ref(ResourceConstants.SNIPPETS.AuthCondition)), + ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), [ + qref('$condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")'), + qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")'), + ]), compoundExpression([ - ifElse( - raw(`$${ResourceConstants.SNIPPETS.AuthCondition} && $${ResourceConstants.SNIPPETS.AuthCondition}.expression != ""`), - compoundExpression([ - set(ref('condition'), ref(ResourceConstants.SNIPPETS.AuthCondition)), - ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), - [ - qref('$condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")'), - qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")') - ]), - compoundExpression([ - qref('$condition.put("expression", "$condition.expression AND attribute_exists(#id)")'), - qref('$condition.expressionNames.put("#id", "id")') - ]) - ) - ]), - ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - compoundExpression([ - set(ref('condition'), obj({ - expression: str(""), - expressionNames: obj({}), - expressionValues: obj({}), - })), - forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), [ - ifElse( - raw('$velocityCount == 1'), - qref('$condition.put("expression", "attribute_exists(#keyCondition$velocityCount)")'), - qref('$condition.put(\ -"expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")'), - ), - qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")') - ]) - ]), - set(ref('condition'), obj({ - expression: str("attribute_exists(#id)"), - expressionNames: obj({ - "#id": str("id") - }), - expressionValues: obj({}), - })) - ) - ), - comment('Automatically set the updatedAt timestamp.'), - qref('$context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))'), - qref(`$context.args.input.put("__typename", "${type}")`), - comment('Update condition if type is @versioned'), - iff( - ref(ResourceConstants.SNIPPETS.VersionedCondition), - compoundExpression([ - // tslint:disable-next-line - qref(`$condition.put("expression", "($condition.expression) AND $${ResourceConstants.SNIPPETS.VersionedCondition}.expression")`), - qref(`$condition.expressionNames.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionNames)`), - qref(`$condition.expressionValues.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionValues)`) - ]) - ), - DynamoDBMappingTemplate.updateItem({ - key: ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), - obj({ - id: obj({ S: str('$context.args.input.id') }) - }), - true - ), - condition: ref('util.toJson($condition)'), - objectKeyVariable: ResourceConstants.SNIPPETS.ModelObjectKey, - nameOverrideMap: ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap - }) + qref('$condition.put("expression", "$condition.expression AND attribute_exists(#id)")'), + qref('$condition.expressionNames.put("#id", "id")'), ]) - ), - ResponseMappingTemplate: print( - ref('util.toJson($context.result)') - ) - }) - } - - /** - * Create a resolver that creates an item in DynamoDB. - * @param type - */ - public makeGetResolver(type: string, nameOverride?: string, queryTypeName: string = 'Query') { - const fieldName = nameOverride ? nameOverride : graphqlName('get' + toUpper(type)) - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), - FieldName: fieldName, - TypeName: queryTypeName, - RequestMappingTemplate: print( - DynamoDBMappingTemplate.getItem({ - key: ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), - obj({ - id: ref('util.dynamodb.toDynamoDBJson($ctx.args.id)') - }), - true - ) + ), + ]), + ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + compoundExpression([ + set( + ref('condition'), + obj({ + expression: str(''), + expressionNames: obj({}), + expressionValues: obj({}), + }) + ), + forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), [ + ifElse( + raw('$velocityCount == 1'), + qref('$condition.put("expression", "attribute_exists(#keyCondition$velocityCount)")'), + qref('$condition.put(\ +"expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")') + ), + qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")'), + ]), + ]), + set( + ref('condition'), + obj({ + expression: str('attribute_exists(#id)'), + expressionNames: obj({ + '#id': str('id'), + }), + expressionValues: obj({}), }) - ), - ResponseMappingTemplate: print( - ref('util.toJson($context.result)') + ) ) - }) - } - - /** - * Create a resolver that queries an item in DynamoDB. - * @param type - */ - public makeQueryResolver(type: string, nameOverride?: string, queryTypeName: string = 'Query') { - const fieldName = nameOverride ? nameOverride : graphqlName(`query${toUpper(type)}`) - const defaultPageLimit = 10 - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), - FieldName: fieldName, - TypeName: queryTypeName, - RequestMappingTemplate: print( - compoundExpression([ - set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), - DynamoDBMappingTemplate.query({ - query: obj({ - 'expression': str('#typename = :typename'), - 'expressionNames': obj({ - '#typename': str('__typename') - }), - 'expressionValues': obj({ - ':typename': obj({ - 'S': str(type) - }) - }) - }), - scanIndexForward: ifElse( - ref('context.args.sortDirection'), - ifElse( - equals(ref('context.args.sortDirection'), str('ASC')), - bool(true), - bool(false) - ), - bool(true) - ), - filter: ifElse( - ref('context.args.filter'), - ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), - nul() - ), - limit: ref('limit'), - nextToken: ifElse( - ref('context.args.nextToken'), - str('$context.args.nextToken'), - nul() - ) - }) - ]) + ), + comment('Automatically set the updatedAt timestamp.'), + qref('$context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))'), + qref(`$context.args.input.put("__typename", "${type}")`), + comment('Update condition if type is @versioned'), + iff( + ref(ResourceConstants.SNIPPETS.VersionedCondition), + compoundExpression([ + // tslint:disable-next-line + qref( + `$condition.put("expression", "($condition.expression) AND $${ResourceConstants.SNIPPETS.VersionedCondition}.expression")` + ), + qref(`$condition.expressionNames.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionNames)`), + qref(`$condition.expressionValues.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionValues)`), + ]) + ), + DynamoDBMappingTemplate.updateItem({ + key: ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), + obj({ + id: obj({ S: str('$context.args.input.id') }), + }), + true ), - ResponseMappingTemplate: print( - compoundExpression([ - iff(raw('!$result'), set(ref('result'), ref('ctx.result'))), - raw('$util.toJson($result)') - ]) - ) + condition: ref('util.toJson($condition)'), + objectKeyVariable: ResourceConstants.SNIPPETS.ModelObjectKey, + nameOverrideMap: ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap, + }), + ]) + ), + ResponseMappingTemplate: print(ref('util.toJson($context.result)')), + }); + } + + /** + * Create a resolver that creates an item in DynamoDB. + * @param type + */ + public makeGetResolver(type: string, nameOverride?: string, queryTypeName: string = 'Query') { + const fieldName = nameOverride ? nameOverride : graphqlName('get' + toUpper(type)); + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), + FieldName: fieldName, + TypeName: queryTypeName, + RequestMappingTemplate: print( + DynamoDBMappingTemplate.getItem({ + key: ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), + obj({ + id: ref('util.dynamodb.toDynamoDBJson($ctx.args.id)'), + }), + true + ), }) - } + ), + ResponseMappingTemplate: print(ref('util.toJson($context.result)')), + }); + } + /** + * Create a resolver that queries an item in DynamoDB. + * @param type + */ + public makeQueryResolver(type: string, nameOverride?: string, queryTypeName: string = 'Query') { + const fieldName = nameOverride ? nameOverride : graphqlName(`query${toUpper(type)}`); + const defaultPageLimit = 10; + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), + FieldName: fieldName, + TypeName: queryTypeName, + RequestMappingTemplate: print( + compoundExpression([ + set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), + DynamoDBMappingTemplate.query({ + query: obj({ + expression: str('#typename = :typename'), + expressionNames: obj({ + '#typename': str('__typename'), + }), + expressionValues: obj({ + ':typename': obj({ + S: str(type), + }), + }), + }), + scanIndexForward: ifElse( + ref('context.args.sortDirection'), + ifElse(equals(ref('context.args.sortDirection'), str('ASC')), bool(true), bool(false)), + bool(true) + ), + filter: ifElse(ref('context.args.filter'), ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), nul()), + limit: ref('limit'), + nextToken: ifElse(ref('context.args.nextToken'), str('$context.args.nextToken'), nul()), + }), + ]) + ), + ResponseMappingTemplate: print( + compoundExpression([iff(raw('!$result'), set(ref('result'), ref('ctx.result'))), raw('$util.toJson($result)')]) + ), + }); + } - /** - * Create a resolver that lists items in DynamoDB. - * TODO: actually fill out the right filter expression. This is a placeholder only. - * @param type - */ - public makeListResolver(type: string, nameOverride?: string, queryTypeName: string = 'Query') { - const fieldName = nameOverride ? nameOverride : graphqlName('list' + plurality(toUpper(type))) - const defaultPageLimit = 10 - const requestVariable = 'ListRequest'; - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), - FieldName: fieldName, - TypeName: queryTypeName, - RequestMappingTemplate: print( - compoundExpression([ - set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), - set( - ref(requestVariable), - obj({ - version: str('2017-02-28'), - limit: ref('limit') - }) - ), - iff( - ref('context.args.nextToken'), - set( - ref(`${requestVariable}.nextToken`), - str('$context.args.nextToken') - ) - ), - iff( - ref('context.args.filter'), - set( - ref(`${requestVariable}.filter`), - ref('util.parseJson("$util.transform.toDynamoDBFilterExpression($ctx.args.filter)")') - ), - ), - ifElse( - raw(`!$util.isNull($${ResourceConstants.SNIPPETS.ModelQueryExpression}) + /** + * Create a resolver that lists items in DynamoDB. + * TODO: actually fill out the right filter expression. This is a placeholder only. + * @param type + */ + public makeListResolver(type: string, nameOverride?: string, queryTypeName: string = 'Query') { + const fieldName = nameOverride ? nameOverride : graphqlName('list' + plurality(toUpper(type))); + const defaultPageLimit = 10; + const requestVariable = 'ListRequest'; + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), + FieldName: fieldName, + TypeName: queryTypeName, + RequestMappingTemplate: print( + compoundExpression([ + set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), + set( + ref(requestVariable), + obj({ + version: str('2017-02-28'), + limit: ref('limit'), + }) + ), + iff(ref('context.args.nextToken'), set(ref(`${requestVariable}.nextToken`), str('$context.args.nextToken'))), + iff( + ref('context.args.filter'), + set(ref(`${requestVariable}.filter`), ref('util.parseJson("$util.transform.toDynamoDBFilterExpression($ctx.args.filter)")')) + ), + ifElse( + raw(`!$util.isNull($${ResourceConstants.SNIPPETS.ModelQueryExpression}) && !$util.isNullOrEmpty($${ResourceConstants.SNIPPETS.ModelQueryExpression}.expression)`), - compoundExpression([ - qref(`$${requestVariable}.put("operation", "Query")`), - qref(`$${requestVariable}.put("query", $${ResourceConstants.SNIPPETS.ModelQueryExpression})`), - ifElse( - raw(`!$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == "DESC"`), - set(ref(`${requestVariable}.scanIndexForward`), bool(false)), - set(ref(`${requestVariable}.scanIndexForward`), bool(true)), - ) - ]), - qref(`$${requestVariable}.put("operation", "Scan")`) - ), - raw(`$util.toJson($${requestVariable})`) - ]) - ), - ResponseMappingTemplate: print( - raw('$util.toJson($ctx.result)') - ) - }) - } + compoundExpression([ + qref(`$${requestVariable}.put("operation", "Query")`), + qref(`$${requestVariable}.put("query", $${ResourceConstants.SNIPPETS.ModelQueryExpression})`), + ifElse( + raw(`!$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == "DESC"`), + set(ref(`${requestVariable}.scanIndexForward`), bool(false)), + set(ref(`${requestVariable}.scanIndexForward`), bool(true)) + ), + ]), + qref(`$${requestVariable}.put("operation", "Scan")`) + ), + raw(`$util.toJson($${requestVariable})`), + ]) + ), + ResponseMappingTemplate: print(raw('$util.toJson($ctx.result)')), + }); + } - /** - * Create a resolver that deletes an item from DynamoDB. - * @param type The name of the type to delete an item of. - * @param nameOverride A user provided override for the field name. - */ - public makeDeleteResolver(type: string, nameOverride?: string, mutationTypeName: string = 'Mutation') { - const fieldName = nameOverride ? nameOverride : graphqlName('delete' + toUpper(type)) - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), - FieldName: fieldName, - TypeName: mutationTypeName, - RequestMappingTemplate: print( + /** + * Create a resolver that deletes an item from DynamoDB. + * @param type The name of the type to delete an item of. + * @param nameOverride A user provided override for the field name. + */ + public makeDeleteResolver(type: string, nameOverride?: string, mutationTypeName: string = 'Mutation') { + const fieldName = nameOverride ? nameOverride : graphqlName('delete' + toUpper(type)); + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), + FieldName: fieldName, + TypeName: mutationTypeName, + RequestMappingTemplate: print( + compoundExpression([ + ifElse( + ref(ResourceConstants.SNIPPETS.AuthCondition), + compoundExpression([ + set(ref('condition'), ref(ResourceConstants.SNIPPETS.AuthCondition)), + ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), [ + qref('$condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")'), + qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")'), + ]), compoundExpression([ - ifElse( - ref(ResourceConstants.SNIPPETS.AuthCondition), - compoundExpression([ - set(ref('condition'), ref(ResourceConstants.SNIPPETS.AuthCondition)), - ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), - [ - qref('$condition.put("expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")'), - qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")') - ]), - compoundExpression([ - qref('$condition.put("expression", "$condition.expression AND attribute_exists(#id)")'), - qref('$condition.expressionNames.put("#id", "id")') - ]) - ) - ]), - ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - compoundExpression([ - set(ref('condition'), obj({ - expression: str(""), - expressionNames: obj({}), - })), - forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), [ - ifElse( - raw('$velocityCount == 1'), - qref('$condition.put("expression", "attribute_exists(#keyCondition$velocityCount)")'), - qref('$condition.put(\ -"expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")'), - ), - qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")') - ]) - ]), - set(ref('condition'), obj({ - expression: str("attribute_exists(#id)"), - expressionNames: obj({ - "#id": str("id") - }) - })) - ) - ), - iff( - ref(ResourceConstants.SNIPPETS.VersionedCondition), - compoundExpression([ - // tslint:disable-next-line - qref(`$condition.put("expression", "($condition.expression) AND $${ResourceConstants.SNIPPETS.VersionedCondition}.expression")`), - qref(`$condition.expressionNames.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionNames)`), - set(ref('expressionValues'), raw('$util.defaultIfNull($condition.expressionValues, {})')), - qref(`$expressionValues.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionValues)`), - set(ref('condition.expressionValues'), ref('expressionValues')) - ]) - ), - DynamoDBMappingTemplate.deleteItem({ - key: ifElse( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), - obj({ - id: ref('util.dynamodb.toDynamoDBJson($ctx.args.input.id)') - }), - true - ), - condition: ref('util.toJson($condition)') - }) + qref('$condition.put("expression", "$condition.expression AND attribute_exists(#id)")'), + qref('$condition.expressionNames.put("#id", "id")'), ]) - ), - ResponseMappingTemplate: print( - ref('util.toJson($context.result)') + ), + ]), + ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + compoundExpression([ + set( + ref('condition'), + obj({ + expression: str(''), + expressionNames: obj({}), + }) + ), + forEach(ref('entry'), ref(`${ResourceConstants.SNIPPETS.ModelObjectKey}.entrySet()`), [ + ifElse( + raw('$velocityCount == 1'), + qref('$condition.put("expression", "attribute_exists(#keyCondition$velocityCount)")'), + qref('$condition.put(\ +"expression", "$condition.expression AND attribute_exists(#keyCondition$velocityCount)")') + ), + qref('$condition.expressionNames.put("#keyCondition$velocityCount", "$entry.key")'), + ]), + ]), + set( + ref('condition'), + obj({ + expression: str('attribute_exists(#id)'), + expressionNames: obj({ + '#id': str('id'), + }), + }) + ) ) - }) - } + ), + iff( + ref(ResourceConstants.SNIPPETS.VersionedCondition), + compoundExpression([ + // tslint:disable-next-line + qref( + `$condition.put("expression", "($condition.expression) AND $${ResourceConstants.SNIPPETS.VersionedCondition}.expression")` + ), + qref(`$condition.expressionNames.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionNames)`), + set(ref('expressionValues'), raw('$util.defaultIfNull($condition.expressionValues, {})')), + qref(`$expressionValues.putAll($${ResourceConstants.SNIPPETS.VersionedCondition}.expressionValues)`), + set(ref('condition.expressionValues'), ref('expressionValues')), + ]) + ), + DynamoDBMappingTemplate.deleteItem({ + key: ifElse( + ref(ResourceConstants.SNIPPETS.ModelObjectKey), + raw(`$util.toJson(\$${ResourceConstants.SNIPPETS.ModelObjectKey})`), + obj({ + id: ref('util.dynamodb.toDynamoDBJson($ctx.args.input.id)'), + }), + true + ), + condition: ref('util.toJson($condition)'), + }), + ]) + ), + ResponseMappingTemplate: print(ref('util.toJson($context.result)')), + }); + } } diff --git a/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts b/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts index 4fb846d0a8..71ddc5c555 100644 --- a/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts +++ b/packages/graphql-elasticsearch-transformer/src/SearchableModelTransformer.ts @@ -1,240 +1,222 @@ +import { Transformer, TransformerContext, getDirectiveArguments, gql, InvalidDirectiveError } from 'graphql-transformer-core'; +import { DirectiveNode, ObjectTypeDefinitionNode } from 'graphql'; +import { ResourceFactory } from './resources'; import { - Transformer, TransformerContext, getDirectiveArguments, - gql, InvalidDirectiveError } from "graphql-transformer-core"; + makeSearchableScalarInputObject, + makeSearchableXFilterInputObject, + makeSearchableSortDirectionEnumObject, + makeSearchableXSortableFieldsEnumObject, + makeSearchableXSortInputObject, +} from './definitions'; import { - DirectiveNode, - ObjectTypeDefinitionNode -} from "graphql"; -import { ResourceFactory } from "./resources"; -import { - makeSearchableScalarInputObject, makeSearchableXFilterInputObject, - makeSearchableSortDirectionEnumObject, makeSearchableXSortableFieldsEnumObject, - makeSearchableXSortInputObject -} from "./definitions"; -import { - makeNamedType, - blankObjectExtension, - makeField, - extensionWithFields, - blankObject, - makeListType, - makeInputValueDefinition -} from "graphql-transformer-common"; + makeNamedType, + blankObjectExtension, + makeField, + extensionWithFields, + blankObject, + makeListType, + makeInputValueDefinition, +} from 'graphql-transformer-common'; import { Expression, str } from 'graphql-mapping-template'; -import { ResolverResourceIDs, SearchableResourceIDs, ModelResourceIDs, getBaseType } from 'graphql-transformer-common' +import { ResolverResourceIDs, SearchableResourceIDs, ModelResourceIDs, getBaseType } from 'graphql-transformer-common'; import path = require('path'); const STACK_NAME = 'SearchableStack'; -const nonKeywordTypes = ["Int", "Float", "Boolean", "AWSTimestamp", "AWSDate", "AWSDateTime"]; +const nonKeywordTypes = ['Int', 'Float', 'Boolean', 'AWSTimestamp', 'AWSDate', 'AWSDateTime']; interface SearchableQueryMap { - search?: string; + search?: string; } interface SearchableDirectiveArgs { - queries?: SearchableQueryMap + queries?: SearchableQueryMap; } /** * Handles the @searchable directive on OBJECT types. */ export class SearchableModelTransformer extends Transformer { - resources: ResourceFactory; - - constructor() { - super( - `SearchableModelTransformer`, - gql` - directive @searchable(queries: SearchableQueryMap) on OBJECT - input SearchableQueryMap { search: String } - ` - ); - this.resources = new ResourceFactory(); - } - - public before = (ctx: TransformerContext): void => { - const template = this.resources.initTemplate(); - ctx.mergeResources(template.Resources); - ctx.mergeParameters(template.Parameters); - ctx.mergeOutputs(template.Outputs); - ctx.mergeMappings(template.Mappings); - ctx.metadata.set('ElasticsearchPathToStreamingLambda', path.resolve(`${__dirname}/../lib/streaming-lambda.zip`)) - for (const resourceId of Object.keys(template.Resources)) { - ctx.mapResourceToStack(STACK_NAME, resourceId); - } - for (const outputId of Object.keys(template.Outputs)) { - ctx.mapResourceToStack(STACK_NAME, outputId); - } - for (const mappingId of Object.keys(template.Mappings)) { - ctx.mapResourceToStack(STACK_NAME, mappingId); - } - }; - - /** - * Given the initial input and context manipulate the context to handle this object directive. - * @param initial The input passed to the transform. - * @param ctx The accumulated context for the transform. - */ - public object = ( - def: ObjectTypeDefinitionNode, - directive: DirectiveNode, - ctx: TransformerContext - ): void => { - const modelDirective = def.directives.find((dir) => dir.name.value === 'model') - if (!modelDirective) { - throw new InvalidDirectiveError('Types annotated with @searchable must also be annotated with @model.') - } - const directiveArguments: SearchableDirectiveArgs = getDirectiveArguments(directive) - let shouldMakeSearch = true; - let searchFieldNameOverride = undefined; - - // Figure out which queries to make and if they have name overrides. - if (directiveArguments.queries) { - if (!directiveArguments.queries.search) { - shouldMakeSearch = false; - } else { - searchFieldNameOverride = directiveArguments.queries.search - } - } + resources: ResourceFactory; + + constructor() { + super( + `SearchableModelTransformer`, + gql` + directive @searchable(queries: SearchableQueryMap) on OBJECT + input SearchableQueryMap { + search: String + } + ` + ); + this.resources = new ResourceFactory(); + } + + public before = (ctx: TransformerContext): void => { + const template = this.resources.initTemplate(); + ctx.mergeResources(template.Resources); + ctx.mergeParameters(template.Parameters); + ctx.mergeOutputs(template.Outputs); + ctx.mergeMappings(template.Mappings); + ctx.metadata.set('ElasticsearchPathToStreamingLambda', path.resolve(`${__dirname}/../lib/streaming-lambda.zip`)); + for (const resourceId of Object.keys(template.Resources)) { + ctx.mapResourceToStack(STACK_NAME, resourceId); + } + for (const outputId of Object.keys(template.Outputs)) { + ctx.mapResourceToStack(STACK_NAME, outputId); + } + for (const mappingId of Object.keys(template.Mappings)) { + ctx.mapResourceToStack(STACK_NAME, mappingId); + } + }; + + /** + * Given the initial input and context manipulate the context to handle this object directive. + * @param initial The input passed to the transform. + * @param ctx The accumulated context for the transform. + */ + public object = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext): void => { + const modelDirective = def.directives.find(dir => dir.name.value === 'model'); + if (!modelDirective) { + throw new InvalidDirectiveError('Types annotated with @searchable must also be annotated with @model.'); + } + const directiveArguments: SearchableDirectiveArgs = getDirectiveArguments(directive); + let shouldMakeSearch = true; + let searchFieldNameOverride = undefined; + + // Figure out which queries to make and if they have name overrides. + if (directiveArguments.queries) { + if (!directiveArguments.queries.search) { + shouldMakeSearch = false; + } else { + searchFieldNameOverride = directiveArguments.queries.search; + } + } - const typeName = def.name.value - ctx.setResource( - SearchableResourceIDs.SearchableEventSourceMappingID(typeName), - this.resources.makeDynamoDBStreamEventSourceMapping(typeName) - ) - ctx.mapResourceToStack( - STACK_NAME, - SearchableResourceIDs.SearchableEventSourceMappingID(typeName) + const typeName = def.name.value; + ctx.setResource( + SearchableResourceIDs.SearchableEventSourceMappingID(typeName), + this.resources.makeDynamoDBStreamEventSourceMapping(typeName) + ); + ctx.mapResourceToStack(STACK_NAME, SearchableResourceIDs.SearchableEventSourceMappingID(typeName)); + + // SearchablePostSortableFields + const queryFields = []; + const nonKeywordFields: Expression[] = []; + def.fields.forEach(field => { + if (nonKeywordTypes.includes(getBaseType(field.type))) { + nonKeywordFields.push(str(field.name.value)); + } + }); + + // Get primary key to use as the default sort field + const primaryKey = this.getPrimaryKey(ctx, typeName); + + // Create list + if (shouldMakeSearch) { + this.generateSearchableInputs(ctx, def); + this.generateSearchableXConnectionType(ctx, def); + + const searchResolver = this.resources.makeSearchResolver( + def.name.value, + nonKeywordFields, + primaryKey, + ctx.getQueryTypeName(), + searchFieldNameOverride + ); + ctx.setResource(ResolverResourceIDs.ElasticsearchSearchResolverResourceID(def.name.value), searchResolver); + ctx.mapResourceToStack(STACK_NAME, ResolverResourceIDs.ElasticsearchSearchResolverResourceID(def.name.value)); + queryFields.push( + makeField( + searchResolver.Properties.FieldName, + [ + makeInputValueDefinition('filter', makeNamedType(`Searchable${def.name.value}FilterInput`)), + makeInputValueDefinition('sort', makeNamedType(`Searchable${def.name.value}SortInput`)), + makeInputValueDefinition('limit', makeNamedType('Int')), + makeInputValueDefinition('nextToken', makeNamedType('String')), + ], + makeNamedType(`Searchable${def.name.value}Connection`) ) + ); + } - // SearchablePostSortableFields - const queryFields = []; - const nonKeywordFields: Expression[] = []; - def.fields.forEach( field => { - if (nonKeywordTypes.includes(getBaseType(field.type))) { - nonKeywordFields.push(str(field.name.value)); - } - }); - - // Get primary key to use as the default sort field - const primaryKey = this.getPrimaryKey(ctx, typeName) - - // Create list - if (shouldMakeSearch) { - this.generateSearchableInputs(ctx, def) - this.generateSearchableXConnectionType(ctx, def) - - const searchResolver = this.resources.makeSearchResolver(def.name.value, nonKeywordFields, - primaryKey, ctx.getQueryTypeName(), - searchFieldNameOverride); - ctx.setResource(ResolverResourceIDs.ElasticsearchSearchResolverResourceID(def.name.value), searchResolver) - ctx.mapResourceToStack( - STACK_NAME, - ResolverResourceIDs.ElasticsearchSearchResolverResourceID(def.name.value) - ) - queryFields.push(makeField( - searchResolver.Properties.FieldName, - [ - makeInputValueDefinition('filter', makeNamedType(`Searchable${def.name.value}FilterInput`)), - makeInputValueDefinition('sort', makeNamedType(`Searchable${def.name.value}SortInput`)), - makeInputValueDefinition('limit', makeNamedType('Int')), - makeInputValueDefinition('nextToken', makeNamedType('String')) - ], - makeNamedType(`Searchable${def.name.value}Connection`) - )) - } - - ctx.addQueryFields(queryFields) - }; - - private generateSearchableXConnectionType(ctx: TransformerContext, def: ObjectTypeDefinitionNode): void { - const searchableXConnectionName = `Searchable${def.name.value}Connection` - if (this.typeExist(searchableXConnectionName, ctx)) { - return - } + ctx.addQueryFields(queryFields); + }; - // Create the TableXConnection - const connectionType = blankObject(searchableXConnectionName) - ctx.addObject(connectionType) - - // Create TableXConnection type with items and nextToken - let connectionTypeExtension = blankObjectExtension(searchableXConnectionName) - connectionTypeExtension = extensionWithFields( - connectionTypeExtension, - [makeField( - 'items', - [], - makeListType(makeNamedType(def.name.value)) - )] - ) - connectionTypeExtension = extensionWithFields( - connectionTypeExtension, - [makeField( - 'nextToken', - [], - makeNamedType('String') - )] - ) - ctx.addObjectExtension(connectionTypeExtension) + private generateSearchableXConnectionType(ctx: TransformerContext, def: ObjectTypeDefinitionNode): void { + const searchableXConnectionName = `Searchable${def.name.value}Connection`; + if (this.typeExist(searchableXConnectionName, ctx)) { + return; } - private typeExist(type: string, ctx: TransformerContext): boolean { - return Boolean(type in ctx.nodeMap); + // Create the TableXConnection + const connectionType = blankObject(searchableXConnectionName); + ctx.addObject(connectionType); + + // Create TableXConnection type with items and nextToken + let connectionTypeExtension = blankObjectExtension(searchableXConnectionName); + connectionTypeExtension = extensionWithFields(connectionTypeExtension, [ + makeField('items', [], makeListType(makeNamedType(def.name.value))), + ]); + connectionTypeExtension = extensionWithFields(connectionTypeExtension, [makeField('nextToken', [], makeNamedType('String'))]); + ctx.addObjectExtension(connectionTypeExtension); + } + + private typeExist(type: string, ctx: TransformerContext): boolean { + return Boolean(type in ctx.nodeMap); + } + + private generateSearchableInputs(ctx: TransformerContext, def: ObjectTypeDefinitionNode): void { + // Create the Scalar filter inputs + if (!this.typeExist('SearchableStringFilterInput', ctx)) { + const searchableStringFilterInput = makeSearchableScalarInputObject('String'); + ctx.addInput(searchableStringFilterInput); } - private generateSearchableInputs(ctx: TransformerContext, def: ObjectTypeDefinitionNode): void { - - // Create the Scalar filter inputs - if (!this.typeExist('SearchableStringFilterInput', ctx)) { - const searchableStringFilterInput = makeSearchableScalarInputObject('String') - ctx.addInput(searchableStringFilterInput) - } - - if (!this.typeExist('SearchableIDFilterInput', ctx)) { - const searchableIDFilterInput = makeSearchableScalarInputObject('ID') - ctx.addInput(searchableIDFilterInput) - } - - if (!this.typeExist('SearchableIntFilterInput', ctx)) { - const searchableIntFilterInput = makeSearchableScalarInputObject('Int') - ctx.addInput(searchableIntFilterInput) - } + if (!this.typeExist('SearchableIDFilterInput', ctx)) { + const searchableIDFilterInput = makeSearchableScalarInputObject('ID'); + ctx.addInput(searchableIDFilterInput); + } - if (!this.typeExist('SearchableFloatFilterInput', ctx)) { - const searchableFloatFilterInput = makeSearchableScalarInputObject('Float') - ctx.addInput(searchableFloatFilterInput) - } + if (!this.typeExist('SearchableIntFilterInput', ctx)) { + const searchableIntFilterInput = makeSearchableScalarInputObject('Int'); + ctx.addInput(searchableIntFilterInput); + } - if (!this.typeExist('SearchableBooleanFilterInput', ctx)) { - const searchableBooleanFilterInput = makeSearchableScalarInputObject('Boolean') - ctx.addInput(searchableBooleanFilterInput) - } + if (!this.typeExist('SearchableFloatFilterInput', ctx)) { + const searchableFloatFilterInput = makeSearchableScalarInputObject('Float'); + ctx.addInput(searchableFloatFilterInput); + } - const searchableXQueryFilterInput = makeSearchableXFilterInputObject(def) - if (!this.typeExist(searchableXQueryFilterInput.name.value, ctx)) { - ctx.addInput(searchableXQueryFilterInput) - } + if (!this.typeExist('SearchableBooleanFilterInput', ctx)) { + const searchableBooleanFilterInput = makeSearchableScalarInputObject('Boolean'); + ctx.addInput(searchableBooleanFilterInput); + } - if (!this.typeExist('SearchableSortDirection', ctx)) { - const searchableSortDirection = makeSearchableSortDirectionEnumObject() - ctx.addEnum(searchableSortDirection) - } + const searchableXQueryFilterInput = makeSearchableXFilterInputObject(def); + if (!this.typeExist(searchableXQueryFilterInput.name.value, ctx)) { + ctx.addInput(searchableXQueryFilterInput); + } - if (!this.typeExist(`Searchable${def.name.value}SortableFields`, ctx)) { - const searchableXSortableFieldsDirection = makeSearchableXSortableFieldsEnumObject(def) - ctx.addEnum(searchableXSortableFieldsDirection) - } + if (!this.typeExist('SearchableSortDirection', ctx)) { + const searchableSortDirection = makeSearchableSortDirectionEnumObject(); + ctx.addEnum(searchableSortDirection); + } - if (!this.typeExist(`Searchable${def.name.value}SortInput`, ctx)) { - const searchableXSortableInputDirection = makeSearchableXSortInputObject(def) - ctx.addInput(searchableXSortableInputDirection) - } + if (!this.typeExist(`Searchable${def.name.value}SortableFields`, ctx)) { + const searchableXSortableFieldsDirection = makeSearchableXSortableFieldsEnumObject(def); + ctx.addEnum(searchableXSortableFieldsDirection); } - private getPrimaryKey(ctx: TransformerContext, typeName: string) : string { - const tableResourceID = ModelResourceIDs.ModelTableResourceID(typeName); - const tableResource = ctx.getResource(tableResourceID) - const primaryKeySchemaElement = tableResource.Properties.KeySchema.find( (keyElement: any) => keyElement.KeyType === 'HASH') - return primaryKeySchemaElement.AttributeName + if (!this.typeExist(`Searchable${def.name.value}SortInput`, ctx)) { + const searchableXSortableInputDirection = makeSearchableXSortInputObject(def); + ctx.addInput(searchableXSortableInputDirection); } + } + + private getPrimaryKey(ctx: TransformerContext, typeName: string): string { + const tableResourceID = ModelResourceIDs.ModelTableResourceID(typeName); + const tableResource = ctx.getResource(tableResourceID); + const primaryKeySchemaElement = tableResource.Properties.KeySchema.find((keyElement: any) => keyElement.KeyType === 'HASH'); + return primaryKeySchemaElement.AttributeName; + } } diff --git a/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts b/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts index 3918245934..2caef98d59 100644 --- a/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts +++ b/packages/graphql-elasticsearch-transformer/src/__tests__/SearchableModelTransformer.test.ts @@ -56,7 +56,7 @@ // }); // test('Test SearchableModelTransformer with only create mutations', () => { -// const validSchema = `type Post @model(mutations: { create: "customCreatePost" }) @searchable { +// const validSchema = `type Post @model(mutations: { create: "customCreatePost" }) @searchable { // id: ID! // title: String! // createdAt: String @@ -192,7 +192,6 @@ // const sortInputType = getInputType(parsed, 'SearchablePostSortInput') // expect(sortInputType).toBeDefined() - // expect(verifyInputCount(parsed, 'ModelStringFilterInput', 1)).toBeTruthy; // expect(verifyInputCount(parsed, 'ModelBooleanFilterInput', 1)).toBeTruthy; // expect(verifyInputCount(parsed, 'ModelIntFilterInput', 1)).toBeTruthy; @@ -237,4 +236,4 @@ // function verifyInputCount(doc: DocumentNode, type: string, count: number): boolean { // return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; -// } \ No newline at end of file +// } diff --git a/packages/graphql-elasticsearch-transformer/src/definitions.ts b/packages/graphql-elasticsearch-transformer/src/definitions.ts index d783361724..57f352cb34 100644 --- a/packages/graphql-elasticsearch-transformer/src/definitions.ts +++ b/packages/graphql-elasticsearch-transformer/src/definitions.ts @@ -1,220 +1,217 @@ import { - ObjectTypeDefinitionNode, InputValueDefinitionNode, InputObjectTypeDefinitionNode, - FieldDefinitionNode, Kind, TypeNode, EnumTypeDefinitionNode, EnumValueDefinitionNode -} from 'graphql' -import { - graphqlName, makeNamedType, isScalar, - makeListType, getBaseType, SearchableResourceIDs -} from 'graphql-transformer-common' + ObjectTypeDefinitionNode, + InputValueDefinitionNode, + InputObjectTypeDefinitionNode, + FieldDefinitionNode, + Kind, + TypeNode, + EnumTypeDefinitionNode, + EnumValueDefinitionNode, +} from 'graphql'; +import { graphqlName, makeNamedType, isScalar, makeListType, getBaseType, SearchableResourceIDs } from 'graphql-transformer-common'; -const STRING_CONDITIONS = ['ne', 'eq', 'match', 'matchPhrase', 'matchPhrasePrefix', 'multiMatch', 'exists', 'wildcard', 'regexp'] -const ID_CONDITIONS = ['ne', 'eq', 'match', 'matchPhrase', 'matchPhrasePrefix', 'multiMatch', 'exists', 'wildcard', 'regexp'] -const INT_CONDITIONS = ['ne', 'gt', 'lt', 'gte', 'lte', 'eq', 'range'] -const FLOAT_CONDITIONS = ['ne', 'gt', 'lt', 'gte', 'lte', 'eq', 'range'] -const BOOLEAN_CONDITIONS = ['eq', 'ne'] +const STRING_CONDITIONS = ['ne', 'eq', 'match', 'matchPhrase', 'matchPhrasePrefix', 'multiMatch', 'exists', 'wildcard', 'regexp']; +const ID_CONDITIONS = ['ne', 'eq', 'match', 'matchPhrase', 'matchPhrasePrefix', 'multiMatch', 'exists', 'wildcard', 'regexp']; +const INT_CONDITIONS = ['ne', 'gt', 'lt', 'gte', 'lte', 'eq', 'range']; +const FLOAT_CONDITIONS = ['ne', 'gt', 'lt', 'gte', 'lte', 'eq', 'range']; +const BOOLEAN_CONDITIONS = ['eq', 'ne']; export function makeSearchableScalarInputObject(type: string): InputObjectTypeDefinitionNode { - const name = SearchableResourceIDs.SearchableFilterInputTypeName(type) - let conditions = getScalarConditions(type) - const fields: InputValueDefinitionNode[] = conditions - .map((condition: string) => ({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { kind: "Name" as "Name", value: condition }, - type: getScalarFilterInputType(condition, type, name), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - })) - return { - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + const name = SearchableResourceIDs.SearchableFilterInputTypeName(type); + let conditions = getScalarConditions(type); + const fields: InputValueDefinitionNode[] = conditions.map((condition: string) => ({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: { kind: 'Name' as 'Name', value: condition }, + type: getScalarFilterInputType(condition, type, name), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], + })); + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } export function makeSearchableXFilterInputObject(obj: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - const name = SearchableResourceIDs.SearchableFilterInputTypeName(obj.name.value) - const fields: InputValueDefinitionNode[] = obj.fields - .filter((field: FieldDefinitionNode) => isScalar(field.type) === true) - .map( - (field: FieldDefinitionNode) => ({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: field.name, - type: makeNamedType(SearchableResourceIDs.SearchableFilterInputTypeName(getBaseType(field.type))), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - }) - ) + const name = SearchableResourceIDs.SearchableFilterInputTypeName(obj.name.value); + const fields: InputValueDefinitionNode[] = obj.fields + .filter((field: FieldDefinitionNode) => isScalar(field.type) === true) + .map((field: FieldDefinitionNode) => ({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: field.name, + type: makeNamedType(SearchableResourceIDs.SearchableFilterInputTypeName(getBaseType(field.type))), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], + })); - fields.push( - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'and' - }, - type: makeListType(makeNamedType(name)), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - }, - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'or' - }, - type: makeListType(makeNamedType(name)), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - }, - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: 'not' - }, - type: makeNamedType(name), - // TODO: Service does not support new style descriptions so wait. - // description: field.description, - directives: [] - } - ) - return { - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] + fields.push( + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'and', + }, + type: makeListType(makeNamedType(name)), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], + }, + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'or', + }, + type: makeListType(makeNamedType(name)), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], + }, + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: 'not', + }, + type: makeNamedType(name), + // TODO: Service does not support new style descriptions so wait. + // description: field.description, + directives: [], } + ); + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } export function makeSearchableSortDirectionEnumObject(): EnumTypeDefinitionNode { - const name = graphqlName(`SearchableSortDirection`) - return { - kind: Kind.ENUM_TYPE_DEFINITION, - name: { - kind: 'Name', - value: name - }, - values: [ - { - kind: Kind.ENUM_VALUE_DEFINITION, - name: { kind: 'Name', value: 'asc' }, - directives: [] - }, - { - kind: Kind.ENUM_VALUE_DEFINITION, - name: { kind: 'Name', value: 'desc' }, - directives: [] - } - ], - directives: [] - } + const name = graphqlName(`SearchableSortDirection`); + return { + kind: Kind.ENUM_TYPE_DEFINITION, + name: { + kind: 'Name', + value: name, + }, + values: [ + { + kind: Kind.ENUM_VALUE_DEFINITION, + name: { kind: 'Name', value: 'asc' }, + directives: [], + }, + { + kind: Kind.ENUM_VALUE_DEFINITION, + name: { kind: 'Name', value: 'desc' }, + directives: [], + }, + ], + directives: [], + }; } export function makeSearchableXSortableFieldsEnumObject(obj: ObjectTypeDefinitionNode): EnumTypeDefinitionNode { - const name = graphqlName(`Searchable${obj.name.value}SortableFields`) - const values: EnumValueDefinitionNode[] = obj.fields - .filter((field: FieldDefinitionNode) => isScalar(field.type) === true) - .map( - (field: FieldDefinitionNode) => ({ - kind: Kind.ENUM_VALUE_DEFINITION, - name: field.name, - directives: [] - }) - ) + const name = graphqlName(`Searchable${obj.name.value}SortableFields`); + const values: EnumValueDefinitionNode[] = obj.fields + .filter((field: FieldDefinitionNode) => isScalar(field.type) === true) + .map((field: FieldDefinitionNode) => ({ + kind: Kind.ENUM_VALUE_DEFINITION, + name: field.name, + directives: [], + })); - return { - kind: Kind.ENUM_TYPE_DEFINITION, - name: { - kind: 'Name', - value: name - }, - values, - directives: [] - } + return { + kind: Kind.ENUM_TYPE_DEFINITION, + name: { + kind: 'Name', + value: name, + }, + values, + directives: [], + }; } export function makeSearchableXSortInputObject(obj: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - const name = graphqlName(`Searchable${obj.name.value}SortInput`) - return { - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + const name = graphqlName(`Searchable${obj.name.value}SortInput`); + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} delete mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields: [ + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { kind: 'Name', value: 'field' }, + type: makeNamedType(`Searchable${obj.name.value}SortableFields`), // TODO: Service does not support new style descriptions so wait. // description: { // kind: 'StringValue', - // value: `Input type for ${obj.name.value} delete mutations` + // value: `The id of the ${obj.name.value} to delete.` // }, - name: { - kind: 'Name', - value: name - }, - fields: [ - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { kind: 'Name', value: 'field' }, - type: makeNamedType(`Searchable${obj.name.value}SortableFields`), - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `The id of the ${obj.name.value} to delete.` - // }, - directives: [] - }, - { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { kind: 'Name', value: 'direction' }, - type: makeNamedType('SearchableSortDirection'), - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `The id of the ${obj.name.value} to delete.` - // }, - directives: [] - } - ], - directives: [] - } + directives: [], + }, + { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { kind: 'Name', value: 'direction' }, + type: makeNamedType('SearchableSortDirection'), + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `The id of the ${obj.name.value} to delete.` + // }, + directives: [], + }, + ], + directives: [], + }; } function getScalarFilterInputType(condition: string, type: string, filterInputName: string): TypeNode { - switch (condition) { - case 'range': - return makeListType(makeNamedType(type)) - case 'exists': - return makeNamedType('Boolean') - default: - return makeNamedType(type) - } + switch (condition) { + case 'range': + return makeListType(makeNamedType(type)); + case 'exists': + return makeNamedType('Boolean'); + default: + return makeNamedType(type); + } } function getScalarConditions(type: string): string[] { - switch (type) { - case 'String': - return STRING_CONDITIONS - case 'ID': - return ID_CONDITIONS - case 'Int': - return INT_CONDITIONS - case 'Float': - return FLOAT_CONDITIONS - case 'Boolean': - return BOOLEAN_CONDITIONS - default: - throw 'Valid types are String, ID, Int, Float, Boolean' - } + switch (type) { + case 'String': + return STRING_CONDITIONS; + case 'ID': + return ID_CONDITIONS; + case 'Int': + return INT_CONDITIONS; + case 'Float': + return FLOAT_CONDITIONS; + case 'Boolean': + return BOOLEAN_CONDITIONS; + default: + throw 'Valid types are String, ID, Int, Float, Boolean'; + } } - diff --git a/packages/graphql-elasticsearch-transformer/src/index.ts b/packages/graphql-elasticsearch-transformer/src/index.ts index 47f69b0f78..ec847cb208 100644 --- a/packages/graphql-elasticsearch-transformer/src/index.ts +++ b/packages/graphql-elasticsearch-transformer/src/index.ts @@ -1,3 +1,3 @@ -import { SearchableModelTransformer } from './SearchableModelTransformer' -export * from './SearchableModelTransformer' -export default SearchableModelTransformer \ No newline at end of file +import { SearchableModelTransformer } from './SearchableModelTransformer'; +export * from './SearchableModelTransformer'; +export default SearchableModelTransformer; diff --git a/packages/graphql-elasticsearch-transformer/src/resources.ts b/packages/graphql-elasticsearch-transformer/src/resources.ts index c76982d014..ad1fc44b5d 100644 --- a/packages/graphql-elasticsearch-transformer/src/resources.ts +++ b/packages/graphql-elasticsearch-transformer/src/resources.ts @@ -1,561 +1,525 @@ import Output from 'cloudform-types/types/output'; -import AppSync from 'cloudform-types/types/appSync' -import IAM from 'cloudform-types/types/iam' -import Template from 'cloudform-types/types/template' -import { Fn, StringParameter, NumberParameter, Lambda, Elasticsearch, Refs } from 'cloudform-types' +import AppSync from 'cloudform-types/types/appSync'; +import IAM from 'cloudform-types/types/iam'; +import Template from 'cloudform-types/types/template'; +import { Fn, StringParameter, NumberParameter, Lambda, Elasticsearch, Refs } from 'cloudform-types'; import { - ElasticsearchMappingTemplate, - print, str, ref, obj, set, iff, list, raw, - forEach, compoundExpression, qref, toJson, ifElse, - int, Expression, -} from 'graphql-mapping-template' -import { toUpper, plurality, graphqlName, ResourceConstants, ModelResourceIDs } from 'graphql-transformer-common' -import { MappingParameters } from 'graphql-transformer-core/src/TransformerContext' + ElasticsearchMappingTemplate, + print, + str, + ref, + obj, + set, + iff, + list, + raw, + forEach, + compoundExpression, + qref, + toJson, + ifElse, + int, + Expression, +} from 'graphql-mapping-template'; +import { toUpper, plurality, graphqlName, ResourceConstants, ModelResourceIDs } from 'graphql-transformer-common'; +import { MappingParameters } from 'graphql-transformer-core/src/TransformerContext'; export class ResourceFactory { - - public makeParams() { - return { - [ResourceConstants.PARAMETERS.ElasticsearchAccessIAMRoleName]: new StringParameter({ - Description: 'The name of the IAM role assumed by AppSync for Elasticsearch.', - Default: 'AppSyncElasticsearchRole' - }), - [ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaHandlerName]: new StringParameter({ - Description: 'The name of the lambda handler.', - Default: 'python_streaming_function.lambda_handler' - }), - [ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaRuntime]: new StringParameter({ - Description: `The lambda runtime \ + public makeParams() { + return { + [ResourceConstants.PARAMETERS.ElasticsearchAccessIAMRoleName]: new StringParameter({ + Description: 'The name of the IAM role assumed by AppSync for Elasticsearch.', + Default: 'AppSyncElasticsearchRole', + }), + [ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaHandlerName]: new StringParameter({ + Description: 'The name of the lambda handler.', + Default: 'python_streaming_function.lambda_handler', + }), + [ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaRuntime]: new StringParameter({ + Description: `The lambda runtime \ (https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime)`, - Default: 'python3.6' - }), - [ResourceConstants.PARAMETERS.ElasticsearchStreamingFunctionName]: new StringParameter({ - Description: 'The name of the streaming lambda function.', - Default: 'DdbToEsFn' - }), - [ResourceConstants.PARAMETERS.ElasticsearchStreamingIAMRoleName]: new StringParameter({ - Description: 'The name of the streaming lambda function IAM role.', - Default: 'SearchableLambdaIAMRole' - }), - [ResourceConstants.PARAMETERS.ElasticsearchDebugStreamingLambda]: new NumberParameter({ - Description: 'Enable debug logs for the Dynamo -> ES streaming lambda.', - Default: 1, - AllowedValues: [0, 1] - }), - [ResourceConstants.PARAMETERS.ElasticsearchInstanceCount]: new NumberParameter({ - Description: 'The number of instances to launch into the Elasticsearch domain.', - Default: 1 - }), - [ResourceConstants.PARAMETERS.ElasticsearchInstanceType]: new StringParameter({ - Description: 'The type of instance to launch into the Elasticsearch domain.', - Default: 't2.small.elasticsearch', - AllowedValues: [ - 't2.small.elasticsearch', 't2.medium.elasticsearch', 'c4.large.elasticsearch', - 'c4.xlarge.elasticsearch', 'c4.2xlarge.elasticsearch', 'c4.4xlarge.elasticsearch', - 'c4.8xlarge.elasticsearch', 'm3.medium.elasticsearch', 'm3.large.elasticsearch', - 'm3.xlarge.elasticsearch', 'm3.2xlarge.elasticsearch', 'm4.large.elasticsearch', - 'm4.xlarge.elasticsearch', 'm4.2xlarge.elasticsearch', 'm4.4xlarge.elasticsearch', - 'm4.10xlarge.elasticsearch', 'r3.large.elasticsearch', 'r3.xlarge.elasticsearch', - 'r3.2xlarge.elasticsearch', 'r3.4xlarge.elasticsearch', 'r3.8xlarge.elasticsearch', - 'r4.large.elasticsearch', 'r4.xlarge.elasticsearch', 'r4.2xlarge.elasticsearch', - 'r4.4xlarge.elasticsearch', 'r4.8xlarge.elasticsearch', 'r4.16xlarge.elasticsearch', - 'i2.xlarge.elasticsearch', 'i2.2xlarge.elasticsearch', 'i3.large.elasticsearch', - 'i3.xlarge.elasticsearch', 'i3.2xlarge.elasticsearch', 'i3.4xlarge.elasticsearch', - 'i3.8xlarge.elasticsearch', 'i3.16xlarge.elasticsearch' - ] - }), - [ResourceConstants.PARAMETERS.ElasticsearchEBSVolumeGB]: new NumberParameter({ - Description: 'The size in GB of the EBS volumes that contain our data.', - Default: 10 - }) - } - } + Default: 'python3.6', + }), + [ResourceConstants.PARAMETERS.ElasticsearchStreamingFunctionName]: new StringParameter({ + Description: 'The name of the streaming lambda function.', + Default: 'DdbToEsFn', + }), + [ResourceConstants.PARAMETERS.ElasticsearchStreamingIAMRoleName]: new StringParameter({ + Description: 'The name of the streaming lambda function IAM role.', + Default: 'SearchableLambdaIAMRole', + }), + [ResourceConstants.PARAMETERS.ElasticsearchDebugStreamingLambda]: new NumberParameter({ + Description: 'Enable debug logs for the Dynamo -> ES streaming lambda.', + Default: 1, + AllowedValues: [0, 1], + }), + [ResourceConstants.PARAMETERS.ElasticsearchInstanceCount]: new NumberParameter({ + Description: 'The number of instances to launch into the Elasticsearch domain.', + Default: 1, + }), + [ResourceConstants.PARAMETERS.ElasticsearchInstanceType]: new StringParameter({ + Description: 'The type of instance to launch into the Elasticsearch domain.', + Default: 't2.small.elasticsearch', + AllowedValues: [ + 't2.small.elasticsearch', + 't2.medium.elasticsearch', + 'c4.large.elasticsearch', + 'c4.xlarge.elasticsearch', + 'c4.2xlarge.elasticsearch', + 'c4.4xlarge.elasticsearch', + 'c4.8xlarge.elasticsearch', + 'm3.medium.elasticsearch', + 'm3.large.elasticsearch', + 'm3.xlarge.elasticsearch', + 'm3.2xlarge.elasticsearch', + 'm4.large.elasticsearch', + 'm4.xlarge.elasticsearch', + 'm4.2xlarge.elasticsearch', + 'm4.4xlarge.elasticsearch', + 'm4.10xlarge.elasticsearch', + 'r3.large.elasticsearch', + 'r3.xlarge.elasticsearch', + 'r3.2xlarge.elasticsearch', + 'r3.4xlarge.elasticsearch', + 'r3.8xlarge.elasticsearch', + 'r4.large.elasticsearch', + 'r4.xlarge.elasticsearch', + 'r4.2xlarge.elasticsearch', + 'r4.4xlarge.elasticsearch', + 'r4.8xlarge.elasticsearch', + 'r4.16xlarge.elasticsearch', + 'i2.xlarge.elasticsearch', + 'i2.2xlarge.elasticsearch', + 'i3.large.elasticsearch', + 'i3.xlarge.elasticsearch', + 'i3.2xlarge.elasticsearch', + 'i3.4xlarge.elasticsearch', + 'i3.8xlarge.elasticsearch', + 'i3.16xlarge.elasticsearch', + ], + }), + [ResourceConstants.PARAMETERS.ElasticsearchEBSVolumeGB]: new NumberParameter({ + Description: 'The size in GB of the EBS volumes that contain our data.', + Default: 10, + }), + }; + } - /** - * Creates the barebones template for an application. - */ - public initTemplate(): Template { - return { - Parameters: this.makeParams(), - Resources: { - [ResourceConstants.RESOURCES.ElasticsearchAccessIAMRoleLogicalID]: this.makeElasticsearchAccessIAMRole(), - [ResourceConstants.RESOURCES.ElasticsearchDataSourceLogicalID]: this.makeElasticsearchDataSource(), - [ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID]: this.makeElasticsearchDomain(), - [ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaIAMRoleLogicalID]: this.makeStreamingLambdaIAMRole(), - [ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID]: this.makeDynamoDBStreamingFunction() - }, - Mappings: this.getLayerMapping(), - Outputs: { - [ResourceConstants.OUTPUTS.ElasticsearchDomainArn]: this.makeDomainArnOutput(), - [ResourceConstants.OUTPUTS.ElasticsearchDomainEndpoint]: this.makeDomainEndpointOutput() - } - } - } + /** + * Creates the barebones template for an application. + */ + public initTemplate(): Template { + return { + Parameters: this.makeParams(), + Resources: { + [ResourceConstants.RESOURCES.ElasticsearchAccessIAMRoleLogicalID]: this.makeElasticsearchAccessIAMRole(), + [ResourceConstants.RESOURCES.ElasticsearchDataSourceLogicalID]: this.makeElasticsearchDataSource(), + [ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID]: this.makeElasticsearchDomain(), + [ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaIAMRoleLogicalID]: this.makeStreamingLambdaIAMRole(), + [ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID]: this.makeDynamoDBStreamingFunction(), + }, + Mappings: this.getLayerMapping(), + Outputs: { + [ResourceConstants.OUTPUTS.ElasticsearchDomainArn]: this.makeDomainArnOutput(), + [ResourceConstants.OUTPUTS.ElasticsearchDomainEndpoint]: this.makeDomainEndpointOutput(), + }, + }; + } - /** - * Given the name of a data source and optional logical id return a CF - * spec for a data source pointing to the elasticsearch domain. - * @param name The name for the data source. If a logicalId is not provided the name is used. - * @param logicalId The logicalId of the domain if it is different than the name of the data source. - */ - public makeElasticsearchDataSource() { - const logicalName = ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID - return new AppSync.DataSource({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Name: logicalName, - Type: 'AMAZON_ELASTICSEARCH', - ServiceRoleArn: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchAccessIAMRoleLogicalID, 'Arn'), - ElasticsearchConfig: { - AwsRegion: Fn.Select(3, Fn.Split(':', Fn.GetAtt(logicalName, 'DomainArn'))), - Endpoint: - Fn.Join('', [ - 'https://', - Fn.GetAtt(logicalName, 'DomainEndpoint') - ]) - } - }).dependsOn(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID) - } + /** + * Given the name of a data source and optional logical id return a CF + * spec for a data source pointing to the elasticsearch domain. + * @param name The name for the data source. If a logicalId is not provided the name is used. + * @param logicalId The logicalId of the domain if it is different than the name of the data source. + */ + public makeElasticsearchDataSource() { + const logicalName = ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID; + return new AppSync.DataSource({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Name: logicalName, + Type: 'AMAZON_ELASTICSEARCH', + ServiceRoleArn: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchAccessIAMRoleLogicalID, 'Arn'), + ElasticsearchConfig: { + AwsRegion: Fn.Select(3, Fn.Split(':', Fn.GetAtt(logicalName, 'DomainArn'))), + Endpoint: Fn.Join('', ['https://', Fn.GetAtt(logicalName, 'DomainEndpoint')]), + }, + }).dependsOn(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID); + } - public getLayerMapping(): MappingParameters { - return { - "LayerResourceMapping": { - "ap-northeast-1": { - "layerRegion": "arn:aws:lambda:ap-northeast-1:249908578461:layer:AWSLambda-Python-AWS-SDK:1" - }, - "us-east-1": { - "layerRegion": "arn:aws:lambda:us-east-1:668099181075:layer:AWSLambda-Python-AWS-SDK:1" - }, - "ap-southeast-1": { - "layerRegion": "arn:aws:lambda:ap-southeast-1:468957933125:layer:AWSLambda-Python-AWS-SDK:1" - }, - "eu-west-1": { - "layerRegion": "arn:aws:lambda:eu-west-1:399891621064:layer:AWSLambda-Python-AWS-SDK:1" - }, - "us-west-1": { - "layerRegion": "arn:aws:lambda:us-west-1:325793726646:layer:AWSLambda-Python-AWS-SDK:1" - }, - "ap-east-1": { - "layerRegion": "arn:aws:lambda:ap-east-1:118857876118:layer:AWSLambda-Python-AWS-SDK:1" - }, - "ap-northeast-2": { - "layerRegion": "arn:aws:lambda:ap-northeast-2:296580773974:layer:AWSLambda-Python-AWS-SDK:1" - }, - "ap-northeast-3": { - "layerRegion": "arn:aws:lambda:ap-northeast-3:961244031340:layer:AWSLambda-Python-AWS-SDK:1" - }, - "ap-south-1": { - "layerRegion": "arn:aws:lambda:ap-south-1:631267018583:layer:AWSLambda-Python-AWS-SDK:1" - }, - "ap-southeast-2": { - "layerRegion": "arn:aws:lambda:ap-southeast-2:817496625479:layer:AWSLambda-Python-AWS-SDK:1" - }, - "ca-central-1": { - "layerRegion": "arn:aws:lambda:ca-central-1:778625758767:layer:AWSLambda-Python-AWS-SDK:1" - }, - "eu-central-1": { - "layerRegion": "arn:aws:lambda:eu-central-1:292169987271:layer:AWSLambda-Python-AWS-SDK:1" - }, - "eu-north-1": { - "layerRegion": "arn:aws:lambda:eu-north-1:642425348156:layer:AWSLambda-Python-AWS-SDK:1" - }, - "eu-west-2": { - "layerRegion": "arn:aws:lambda:eu-west-2:142628438157:layer:AWSLambda-Python-AWS-SDK:1" - }, - "eu-west-3": { - "layerRegion": "arn:aws:lambda:eu-west-3:959311844005:layer:AWSLambda-Python-AWS-SDK:1" - }, - "sa-east-1": { - "layerRegion": "arn:aws:lambda:sa-east-1:640010853179:layer:AWSLambda-Python-AWS-SDK:1" - }, - "us-east-2": { - "layerRegion": "arn:aws:lambda:us-east-2:259788987135:layer:AWSLambda-Python-AWS-SDK:1" - }, - "us-west-2": { - "layerRegion": "arn:aws:lambda:us-west-2:420165488524:layer:AWSLambda-Python-AWS-SDK:1" - }, - "cn-north-1": { - "layerRegion": "arn:aws-cn:lambda:cn-north-1:683298794825:layer:AWSLambda-Python-AWS-SDK:1" - }, - "cn-northwest-1": { - "layerRegion": "arn:aws-cn:lambda:cn-northwest-1:382066503313:layer:AWSLambda-Python-AWS-SDK:1" - }, - "us-gov-west-1": { - "layerRegion": "arn:aws-us-gov:lambda:us-gov-west-1:556739011827:layer:AWSLambda-Python-AWS-SDK:1" - }, - "us-gov-east-1": { - "layerRegion": "arn:aws-us-gov:lambda:us-gov-east-1:138526772879:layer:AWSLambda-Python-AWS-SDK:1" - } - } - } - } + public getLayerMapping(): MappingParameters { + return { + LayerResourceMapping: { + 'ap-northeast-1': { + layerRegion: 'arn:aws:lambda:ap-northeast-1:249908578461:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'us-east-1': { + layerRegion: 'arn:aws:lambda:us-east-1:668099181075:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'ap-southeast-1': { + layerRegion: 'arn:aws:lambda:ap-southeast-1:468957933125:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'eu-west-1': { + layerRegion: 'arn:aws:lambda:eu-west-1:399891621064:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'us-west-1': { + layerRegion: 'arn:aws:lambda:us-west-1:325793726646:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'ap-east-1': { + layerRegion: 'arn:aws:lambda:ap-east-1:118857876118:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'ap-northeast-2': { + layerRegion: 'arn:aws:lambda:ap-northeast-2:296580773974:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'ap-northeast-3': { + layerRegion: 'arn:aws:lambda:ap-northeast-3:961244031340:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'ap-south-1': { + layerRegion: 'arn:aws:lambda:ap-south-1:631267018583:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'ap-southeast-2': { + layerRegion: 'arn:aws:lambda:ap-southeast-2:817496625479:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'ca-central-1': { + layerRegion: 'arn:aws:lambda:ca-central-1:778625758767:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'eu-central-1': { + layerRegion: 'arn:aws:lambda:eu-central-1:292169987271:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'eu-north-1': { + layerRegion: 'arn:aws:lambda:eu-north-1:642425348156:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'eu-west-2': { + layerRegion: 'arn:aws:lambda:eu-west-2:142628438157:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'eu-west-3': { + layerRegion: 'arn:aws:lambda:eu-west-3:959311844005:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'sa-east-1': { + layerRegion: 'arn:aws:lambda:sa-east-1:640010853179:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'us-east-2': { + layerRegion: 'arn:aws:lambda:us-east-2:259788987135:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'us-west-2': { + layerRegion: 'arn:aws:lambda:us-west-2:420165488524:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'cn-north-1': { + layerRegion: 'arn:aws-cn:lambda:cn-north-1:683298794825:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'cn-northwest-1': { + layerRegion: 'arn:aws-cn:lambda:cn-northwest-1:382066503313:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'us-gov-west-1': { + layerRegion: 'arn:aws-us-gov:lambda:us-gov-west-1:556739011827:layer:AWSLambda-Python-AWS-SDK:1', + }, + 'us-gov-east-1': { + layerRegion: 'arn:aws-us-gov:lambda:us-gov-east-1:138526772879:layer:AWSLambda-Python-AWS-SDK:1', + }, + }, + }; + } - /** - * Deploy a lambda function that will stream data from our DynamoDB table - * to our elasticsearch index. - */ - public makeDynamoDBStreamingFunction() { - return new Lambda.Function({ - Code: { - S3Bucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - S3Key: Fn.Join('/', [ - Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - "functions", - Fn.Join('.', [ - ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID, - "zip" - ]), - ]) - }, - FunctionName: this.joinWithEnv("-", [ - Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingFunctionName), - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - ]), - Handler: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaHandlerName), - Role: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaIAMRoleLogicalID, 'Arn'), - Runtime: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaRuntime), - Layers: [Fn.FindInMap('LayerResourceMapping', Fn.Ref("AWS::Region"), "layerRegion")], - Environment: { - Variables: { - ES_ENDPOINT: Fn.Join('', [ - 'https://', - Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainEndpoint') - ]), - ES_REGION: Fn.Select(3, Fn.Split(':', Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainArn'))), - DEBUG: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchDebugStreamingLambda) - } - } - }).dependsOn([ - ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaIAMRoleLogicalID, - ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID - ]) - } + /** + * Deploy a lambda function that will stream data from our DynamoDB table + * to our elasticsearch index. + */ + public makeDynamoDBStreamingFunction() { + return new Lambda.Function({ + Code: { + S3Bucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + S3Key: Fn.Join('/', [ + Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + 'functions', + Fn.Join('.', [ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID, 'zip']), + ]), + }, + FunctionName: this.joinWithEnv('-', [ + Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingFunctionName), + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + ]), + Handler: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaHandlerName), + Role: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaIAMRoleLogicalID, 'Arn'), + Runtime: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingLambdaRuntime), + Layers: [Fn.FindInMap('LayerResourceMapping', Fn.Ref('AWS::Region'), 'layerRegion')], + Environment: { + Variables: { + ES_ENDPOINT: Fn.Join('', ['https://', Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainEndpoint')]), + ES_REGION: Fn.Select(3, Fn.Split(':', Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainArn'))), + DEBUG: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchDebugStreamingLambda), + }, + }, + }).dependsOn([ + ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaIAMRoleLogicalID, + ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, + ]); + } - public makeDynamoDBStreamEventSourceMapping(typeName: string) { - return new Lambda.EventSourceMapping({ - BatchSize: 1, - Enabled: true, - EventSourceArn: Fn.GetAtt(ModelResourceIDs.ModelTableResourceID(typeName), 'StreamArn'), - FunctionName: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID, 'Arn'), - StartingPosition: 'LATEST' - }).dependsOn([ - ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID - ]) - } + public makeDynamoDBStreamEventSourceMapping(typeName: string) { + return new Lambda.EventSourceMapping({ + BatchSize: 1, + Enabled: true, + EventSourceArn: Fn.GetAtt(ModelResourceIDs.ModelTableResourceID(typeName), 'StreamArn'), + FunctionName: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID, 'Arn'), + StartingPosition: 'LATEST', + }).dependsOn([ResourceConstants.RESOURCES.ElasticsearchStreamingLambdaFunctionLogicalID]); + } - private joinWithEnv(separator: string, listToJoin: any[]) { - return Fn.If( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - Fn.Join( - separator, - [ - ...listToJoin, - Fn.Ref(ResourceConstants.PARAMETERS.Env) - ] - ), - Fn.Join( - separator, - listToJoin - ) - ) - } + private joinWithEnv(separator: string, listToJoin: any[]) { + return Fn.If( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + Fn.Join(separator, [...listToJoin, Fn.Ref(ResourceConstants.PARAMETERS.Env)]), + Fn.Join(separator, listToJoin) + ); + } - /** - * Create a single role that has access to all the resources created by the - * transform. - * @param name The name of the IAM role to create. - */ - public makeElasticsearchAccessIAMRole() { - return new IAM.Role({ - RoleName: this.joinWithEnv("-", [ - Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchAccessIAMRoleName), - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId') - ]), - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'appsync.amazonaws.com' - }, - Action: 'sts:AssumeRole' - } - ] + /** + * Create a single role that has access to all the resources created by the + * transform. + * @param name The name of the IAM role to create. + */ + public makeElasticsearchAccessIAMRole() { + return new IAM.Role({ + RoleName: this.joinWithEnv('-', [ + Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchAccessIAMRoleName), + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + ]), + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'appsync.amazonaws.com', }, - Policies: [ - new IAM.Role.Policy({ - PolicyName: 'ElasticsearchAccess', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: [ - "es:ESHttpPost", - "es:ESHttpDelete", - "es:ESHttpHead", - "es:ESHttpGet", - "es:ESHttpPost", - "es:ESHttpPut" - ], - Effect: "Allow", - Resource: Fn.Join( - '', - [ - this.domainArn(), - "/*" - ] - ) - } - ] - } - }) - ] - }) - } + Action: 'sts:AssumeRole', + }, + ], + }, + Policies: [ + new IAM.Role.Policy({ + PolicyName: 'ElasticsearchAccess', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['es:ESHttpPost', 'es:ESHttpDelete', 'es:ESHttpHead', 'es:ESHttpGet', 'es:ESHttpPost', 'es:ESHttpPut'], + Effect: 'Allow', + Resource: Fn.Join('', [this.domainArn(), '/*']), + }, + ], + }, + }), + ], + }); + } - /** - * Create a single role that has access to all the resources created by the - * transform. - * @param name The name of the IAM role to create. - */ - public makeStreamingLambdaIAMRole() { - return new IAM.Role({ - RoleName: this.joinWithEnv("-", [ - Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingIAMRoleName), - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - ]), - AssumeRolePolicyDocument: { - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Principal: { - Service: "lambda.amazonaws.com" - }, - Action: "sts:AssumeRole" - } - ] + /** + * Create a single role that has access to all the resources created by the + * transform. + * @param name The name of the IAM role to create. + */ + public makeStreamingLambdaIAMRole() { + return new IAM.Role({ + RoleName: this.joinWithEnv('-', [ + Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchStreamingIAMRoleName), + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + ]), + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', }, - Policies: [ - new IAM.Role.Policy({ - PolicyName: 'ElasticsearchAccess', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: [ - "es:ESHttpPost", - "es:ESHttpDelete", - "es:ESHttpHead", - "es:ESHttpGet", - "es:ESHttpPost", - "es:ESHttpPut" - ], - Effect: "Allow", - Resource: Fn.Join( - '', - [ - this.domainArn(), - "/*" - ] - ) - } - ] - } - }), - new IAM.Role.Policy({ - PolicyName: 'DynamoDBStreamAccess', - PolicyDocument: { - Version: "2012-10-17", - Statement: [ - { - Action: [ - "dynamodb:DescribeStream", - "dynamodb:GetRecords", - "dynamodb:GetShardIterator", - "dynamodb:ListStreams" - ], - Effect: "Allow", - Resource: [ - "*" - // TODO: Scope this to each table individually. - // Fn.Join( - // '/', - // [Fn.GetAtt(ResourceConstants.RESOURCES.DynamoDBModelTableLogicalID, 'Arn'), '*'] - // ) - ] - } - ] - } - }), - new IAM.Role.Policy({ - PolicyName: 'CloudWatchLogsAccess', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: "Allow", - Action: [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - Resource: "arn:aws:logs:*:*:*" - } - ] - } - }) - ] - }) - // .dependsOn(ResourceConstants.RESOURCES.DynamoDBModelTableLogicalID) - } + Action: 'sts:AssumeRole', + }, + ], + }, + Policies: [ + new IAM.Role.Policy({ + PolicyName: 'ElasticsearchAccess', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['es:ESHttpPost', 'es:ESHttpDelete', 'es:ESHttpHead', 'es:ESHttpGet', 'es:ESHttpPost', 'es:ESHttpPut'], + Effect: 'Allow', + Resource: Fn.Join('', [this.domainArn(), '/*']), + }, + ], + }, + }), + new IAM.Role.Policy({ + PolicyName: 'DynamoDBStreamAccess', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['dynamodb:DescribeStream', 'dynamodb:GetRecords', 'dynamodb:GetShardIterator', 'dynamodb:ListStreams'], + Effect: 'Allow', + Resource: [ + '*', + // TODO: Scope this to each table individually. + // Fn.Join( + // '/', + // [Fn.GetAtt(ResourceConstants.RESOURCES.DynamoDBModelTableLogicalID, 'Arn'), '*'] + // ) + ], + }, + ], + }, + }), + new IAM.Role.Policy({ + PolicyName: 'CloudWatchLogsAccess', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + Resource: 'arn:aws:logs:*:*:*', + }, + ], + }, + }), + ], + }); + // .dependsOn(ResourceConstants.RESOURCES.DynamoDBModelTableLogicalID) + } - /** - * If there is an env, allow ES to create the domain name so we don't go - * over 28 characters. If there is no env, fallback to original behavior. - */ - private domainName() { - return Fn.If( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - Refs.NoValue, - Fn.Join( - '-', - [ - 'd', - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId') - ] - ) - ) - } + /** + * If there is an env, allow ES to create the domain name so we don't go + * over 28 characters. If there is no env, fallback to original behavior. + */ + private domainName() { + return Fn.If( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + Refs.NoValue, + Fn.Join('-', ['d', Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId')]) + ); + } - private domainArn() { - return Fn.GetAtt( - ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, - "DomainArn" - ) - } + private domainArn() { + return Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainArn'); + } - /** - * Create the elasticsearch domain. - */ - public makeElasticsearchDomain() { - return new Elasticsearch.Domain({ - DomainName: this.domainName(), - ElasticsearchVersion: '6.2', - ElasticsearchClusterConfig: { - ZoneAwarenessEnabled: false, - InstanceCount: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchInstanceCount), - InstanceType: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchInstanceType) - }, - EBSOptions: { - EBSEnabled: true, - VolumeType: 'gp2', - VolumeSize: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchEBSVolumeGB) - } - }) - } + /** + * Create the elasticsearch domain. + */ + public makeElasticsearchDomain() { + return new Elasticsearch.Domain({ + DomainName: this.domainName(), + ElasticsearchVersion: '6.2', + ElasticsearchClusterConfig: { + ZoneAwarenessEnabled: false, + InstanceCount: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchInstanceCount), + InstanceType: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchInstanceType), + }, + EBSOptions: { + EBSEnabled: true, + VolumeType: 'gp2', + VolumeSize: Fn.Ref(ResourceConstants.PARAMETERS.ElasticsearchEBSVolumeGB), + }, + }); + } - /** - * Create the Elasticsearch search resolver. - */ - public makeSearchResolver(type: string, nonKeywordFields: Expression[], - primaryKey: string, queryTypeName: string, nameOverride?: string) { - const fieldName = nameOverride ? nameOverride : graphqlName('search' + plurality(toUpper(type))); - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDataSourceLogicalID, 'Name'), - FieldName: fieldName, - TypeName: queryTypeName, - RequestMappingTemplate: print( - compoundExpression([ - set(ref('indexPath'), str(`/${type.toLowerCase()}/doc/_search`)), - set(ref('nonKeywordFields'), list(nonKeywordFields)), - ifElse( - ref('util.isNullOrEmpty($context.args.sort)'), - compoundExpression([ - set(ref('sortDirection'), str('desc')), - set(ref('sortField'), str(primaryKey)) - ]), - compoundExpression([ - set(ref('sortDirection'), raw('$util.defaultIfNull($context.args.sort.direction, "desc")')), - set(ref('sortField'), raw(`$util.defaultIfNull($context.args.sort.field, "${primaryKey}")`)) - ]), - ), - ElasticsearchMappingTemplate.searchItem({ - path: str('$indexPath'), - size: ifElse( - ref('context.args.limit'), - ref('context.args.limit'), - int(10), - true), - search_after: list([str('$context.args.nextToken')]), - query: ifElse( - ref('context.args.filter'), - ref('util.transform.toElasticsearchQueryDSL($ctx.args.filter)'), - obj({ - 'match_all': obj({}) - })), - sort: list([ - raw('{ #if($nonKeywordFields.contains($sortField))\ + /** + * Create the Elasticsearch search resolver. + */ + public makeSearchResolver( + type: string, + nonKeywordFields: Expression[], + primaryKey: string, + queryTypeName: string, + nameOverride?: string + ) { + const fieldName = nameOverride ? nameOverride : graphqlName('search' + plurality(toUpper(type))); + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDataSourceLogicalID, 'Name'), + FieldName: fieldName, + TypeName: queryTypeName, + RequestMappingTemplate: print( + compoundExpression([ + set(ref('indexPath'), str(`/${type.toLowerCase()}/doc/_search`)), + set(ref('nonKeywordFields'), list(nonKeywordFields)), + ifElse( + ref('util.isNullOrEmpty($context.args.sort)'), + compoundExpression([set(ref('sortDirection'), str('desc')), set(ref('sortField'), str(primaryKey))]), + compoundExpression([ + set(ref('sortDirection'), raw('$util.defaultIfNull($context.args.sort.direction, "desc")')), + set(ref('sortField'), raw(`$util.defaultIfNull($context.args.sort.field, "${primaryKey}")`)), + ]) + ), + ElasticsearchMappingTemplate.searchItem({ + path: str('$indexPath'), + size: ifElse(ref('context.args.limit'), ref('context.args.limit'), int(10), true), + search_after: list([str('$context.args.nextToken')]), + query: ifElse( + ref('context.args.filter'), + ref('util.transform.toElasticsearchQueryDSL($ctx.args.filter)'), + obj({ + match_all: obj({}), + }) + ), + sort: list([ + raw( + '{ #if($nonKeywordFields.contains($sortField))\ "$sortField" #else "${sortField}.keyword" #end : {\ "order" : "$sortDirection"\ -} }')]), - }) - ]) - ), - ResponseMappingTemplate: print( - compoundExpression([ - set(ref('es_items'), list([])), - forEach( - ref('entry'), - ref('context.result.hits.hits'), - [ - iff( - raw('!$foreach.hasNext'), - set(ref('nextToken'), ref('entry.sort.get(0)')) - ), - qref('$es_items.add($entry.get("_source"))') - ] - ), - toJson(obj({ - "items": ref('es_items'), - "total": ref('ctx.result.hits.total'), - "nextToken": ref('nextToken') - })) - ]) - ) - }).dependsOn([ - ResourceConstants.RESOURCES.ElasticsearchDataSourceLogicalID +} }' + ), + ]), + }), + ]) + ), + ResponseMappingTemplate: print( + compoundExpression([ + set(ref('es_items'), list([])), + forEach(ref('entry'), ref('context.result.hits.hits'), [ + iff(raw('!$foreach.hasNext'), set(ref('nextToken'), ref('entry.sort.get(0)'))), + qref('$es_items.add($entry.get("_source"))'), + ]), + toJson( + obj({ + items: ref('es_items'), + total: ref('ctx.result.hits.total'), + nextToken: ref('nextToken'), + }) + ), ]) - } + ), + }).dependsOn([ResourceConstants.RESOURCES.ElasticsearchDataSourceLogicalID]); + } - /** - * OUTPUTS - */ - /** - * Create output to export the Elasticsearch DomainArn - * @returns Output - */ - public makeDomainArnOutput(): Output { - return { - Description: "Elasticsearch instance Domain ARN.", - Value: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainArn'), - Export: { - Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), "GetAtt", "Elasticsearch", "DomainArn"]) - } - } - } - /** - * Create output to export the Elasticsearch DomainEndpoint - * @returns Output - */ - public makeDomainEndpointOutput(): Output { - return { - Description: "Elasticsearch instance Domain Endpoint.", - Value: Fn.Join('', ['https://', Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainEndpoint')]), - Export: { - Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), "GetAtt", "Elasticsearch", "DomainEndpoint"]) - } - } - } + /** + * OUTPUTS + */ + /** + * Create output to export the Elasticsearch DomainArn + * @returns Output + */ + public makeDomainArnOutput(): Output { + return { + Description: 'Elasticsearch instance Domain ARN.', + Value: Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainArn'), + Export: { + Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), 'GetAtt', 'Elasticsearch', 'DomainArn']), + }, + }; + } + /** + * Create output to export the Elasticsearch DomainEndpoint + * @returns Output + */ + public makeDomainEndpointOutput(): Output { + return { + Description: 'Elasticsearch instance Domain Endpoint.', + Value: Fn.Join('', ['https://', Fn.GetAtt(ResourceConstants.RESOURCES.ElasticsearchDomainLogicalID, 'DomainEndpoint')]), + Export: { + Name: Fn.Join(':', [Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), 'GetAtt', 'Elasticsearch', 'DomainEndpoint']), + }, + }; + } } diff --git a/packages/graphql-function-transformer/src/FunctionTransformer.ts b/packages/graphql-function-transformer/src/FunctionTransformer.ts index f7d5adcab0..84af1da4d9 100644 --- a/packages/graphql-function-transformer/src/FunctionTransformer.ts +++ b/packages/graphql-function-transformer/src/FunctionTransformer.ts @@ -1,186 +1,190 @@ -import { - Transformer, gql, TransformerContext, getDirectiveArguments, TransformerContractError -} from 'graphql-transformer-core'; +import { Transformer, gql, TransformerContext, getDirectiveArguments, TransformerContractError } from 'graphql-transformer-core'; import { obj, str, ref, printBlock, compoundExpression, qref, raw, iff } from 'graphql-mapping-template'; import { ResolverResourceIDs, FunctionResourceIDs, ResourceConstants } from 'graphql-transformer-common'; import { ObjectTypeDefinitionNode, FieldDefinitionNode, DirectiveNode } from 'graphql'; -import { AppSync, IAM, Fn } from 'cloudform-types' +import { AppSync, IAM, Fn } from 'cloudform-types'; import { lambdaArnResource } from './lambdaArns'; const FUNCTION_DIRECTIVE_STACK = 'FunctionDirectiveStack'; export default class FunctionTransformer extends Transformer { + constructor() { + super( + 'FunctionTransformer', + gql` + directive @function(name: String!, region: String) on FIELD_DEFINITION + ` + ); + } - constructor() { - super( - 'FunctionTransformer', - gql`directive @function(name: String!, region: String) on FIELD_DEFINITION` - ) + /** + * Add the required resources to invoke a lambda function for this field. + */ + field = (parent: ObjectTypeDefinitionNode, definition: FieldDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const { name, region } = getDirectiveArguments(directive); + if (!name) { + throw new TransformerContractError(`Must supply a 'name' to @function.`); } - /** - * Add the required resources to invoke a lambda function for this field. - */ - field = (parent: ObjectTypeDefinitionNode, definition: FieldDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const { name, region } = getDirectiveArguments(directive); - if (!name) { - throw new TransformerContractError(`Must supply a 'name' to @function.`) - } - - // Add the iam role if it does not exist. - const iamRoleKey = FunctionResourceIDs.FunctionIAMRoleID(name, region); - if (!ctx.getResource(iamRoleKey)) { - ctx.setResource(iamRoleKey, this.role(name, region)); - ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, iamRoleKey); - } - - // Add the data source if it does not exist. - const lambdaDataSourceKey = FunctionResourceIDs.FunctionDataSourceID(name, region); - if (!ctx.getResource(lambdaDataSourceKey)) { - ctx.setResource(lambdaDataSourceKey, this.datasource(name, region)); - ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, lambdaDataSourceKey); - } - - // Add function that invokes the lambda function - const functionConfigurationKey = FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region); - if (!ctx.getResource(functionConfigurationKey)) { - ctx.setResource(functionConfigurationKey, this.function(name, region)); - ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, functionConfigurationKey); - } - - // Add resolver that invokes our function - const typeName = parent.name.value; - const fieldName = definition.name.value; - const resolverKey = ResolverResourceIDs.ResolverResourceID(typeName, fieldName); - const resolver = ctx.getResource(resolverKey); - if (!resolver) { - ctx.setResource(resolverKey, this.resolver(typeName, fieldName, name, region)); - ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, resolverKey); - } else if (resolver.Properties.Kind === 'PIPELINE') { - ctx.setResource(resolverKey, this.appendFunctionToResolver(resolver, FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region))) - } - }; + // Add the iam role if it does not exist. + const iamRoleKey = FunctionResourceIDs.FunctionIAMRoleID(name, region); + if (!ctx.getResource(iamRoleKey)) { + ctx.setResource(iamRoleKey, this.role(name, region)); + ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, iamRoleKey); + } - /** - * Create a role that allows our AppSync API to talk to our Lambda function. - */ - role = (name: string, region: string): any => { - return new IAM.Role({ - RoleName: Fn.If( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - Fn.Join('-', [ - FunctionResourceIDs.FunctionIAMRoleID(name, region).slice(0, 26), // max of 64. 64-10-26-28 = 0 - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), // 26 - Fn.Ref(ResourceConstants.PARAMETERS.Env) // 10 - ]), - Fn.Join('-', [ - FunctionResourceIDs.FunctionIAMRoleID(name, region).slice(0, 37), // max of 64. 64-26-38 = 0 - Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), // 26 - ]) - ), - AssumeRolePolicyDocument: { - Version: "2012-10-17", - Statement: [{ - Effect: "Allow", - Principal: { - Service: "appsync.amazonaws.com" - }, - Action: "sts:AssumeRole" - }] - }, - Policies: [{ - PolicyName: "InvokeLambdaFunction", - PolicyDocument: { - Version: "2012-10-17", - Statement: [{ - Effect: "Allow", - Action: [ - "lambda:InvokeFunction" - ], - Resource: lambdaArnResource(name, region) - }] - } - }] - }) + // Add the data source if it does not exist. + const lambdaDataSourceKey = FunctionResourceIDs.FunctionDataSourceID(name, region); + if (!ctx.getResource(lambdaDataSourceKey)) { + ctx.setResource(lambdaDataSourceKey, this.datasource(name, region)); + ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, lambdaDataSourceKey); } - /** - * Creates a lambda data source that registers the lambda function and associated role. - */ - datasource = (name: string, region: string): any => { - return new AppSync.DataSource({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - Name: FunctionResourceIDs.FunctionDataSourceID(name, region), - Type: "AWS_LAMBDA", - ServiceRoleArn: Fn.GetAtt(FunctionResourceIDs.FunctionIAMRoleID(name, region), "Arn"), - LambdaConfig: { - LambdaFunctionArn: lambdaArnResource(name, region) - } - }).dependsOn(FunctionResourceIDs.FunctionIAMRoleID(name, region)) + // Add function that invokes the lambda function + const functionConfigurationKey = FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region); + if (!ctx.getResource(functionConfigurationKey)) { + ctx.setResource(functionConfigurationKey, this.function(name, region)); + ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, functionConfigurationKey); } - /** - * Create a new pipeline function that calls out to the lambda function and returns the value. - */ - function = (name: string, region: string): any => { - return new AppSync.FunctionConfiguration({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - Name: FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region), - DataSourceName: FunctionResourceIDs.FunctionDataSourceID(name, region), - FunctionVersion: "2018-05-29", - RequestMappingTemplate: printBlock(`Invoke AWS Lambda data source: ${FunctionResourceIDs.FunctionDataSourceID(name, region)}`)(obj({ - version: str('2018-05-29'), - operation: str('Invoke'), - payload: obj({ - typeName: str('$ctx.stash.get("typeName")'), - fieldName: str('$ctx.stash.get("fieldName")'), - arguments: ref('util.toJson($ctx.arguments)'), - identity: ref('util.toJson($ctx.identity)'), - source: ref('util.toJson($ctx.source)'), - request: ref('util.toJson($ctx.request)'), - prev: ref('util.toJson($ctx.prev)'), - }) - })), - ResponseMappingTemplate: printBlock('Handle error or return result')(compoundExpression([ - iff(ref('ctx.error'), raw('$util.error($ctx.error.message, $ctx.error.type)')), - raw('$util.toJson($ctx.result)') - ])) - }).dependsOn(FunctionResourceIDs.FunctionDataSourceID(name, region)) + // Add resolver that invokes our function + const typeName = parent.name.value; + const fieldName = definition.name.value; + const resolverKey = ResolverResourceIDs.ResolverResourceID(typeName, fieldName); + const resolver = ctx.getResource(resolverKey); + if (!resolver) { + ctx.setResource(resolverKey, this.resolver(typeName, fieldName, name, region)); + ctx.mapResourceToStack(FUNCTION_DIRECTIVE_STACK, resolverKey); + } else if (resolver.Properties.Kind === 'PIPELINE') { + ctx.setResource( + resolverKey, + this.appendFunctionToResolver(resolver, FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region)) + ); } + }; - /** - * Create a resolver of one that calls the "function" function. - */ - resolver = (type: string, field: string, name: string, region?: string): any => { - return new AppSync.Resolver({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - TypeName: type, - FieldName: field, - Kind: 'PIPELINE', - PipelineConfig: { - Functions: [ - Fn.GetAtt(FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region), "FunctionId") - ] + /** + * Create a role that allows our AppSync API to talk to our Lambda function. + */ + role = (name: string, region: string): any => { + return new IAM.Role({ + RoleName: Fn.If( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + Fn.Join('-', [ + FunctionResourceIDs.FunctionIAMRoleID(name, region).slice(0, 26), // max of 64. 64-10-26-28 = 0 + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), // 26 + Fn.Ref(ResourceConstants.PARAMETERS.Env), // 10 + ]), + Fn.Join('-', [ + FunctionResourceIDs.FunctionIAMRoleID(name, region).slice(0, 37), // max of 64. 64-26-38 = 0 + Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), // 26 + ]) + ), + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'appsync.amazonaws.com', }, - RequestMappingTemplate: printBlock('Stash resolver specific context.')(compoundExpression([ - qref(`$ctx.stash.put("typeName", "${type}")`), - qref(`$ctx.stash.put("fieldName", "${field}")`), - obj({}) - ])), - ResponseMappingTemplate: '$util.toJson($ctx.prev.result)' - }).dependsOn(FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region)) - } + Action: 'sts:AssumeRole', + }, + ], + }, + Policies: [ + { + PolicyName: 'InvokeLambdaFunction', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['lambda:InvokeFunction'], + Resource: lambdaArnResource(name, region), + }, + ], + }, + }, + ], + }); + }; + + /** + * Creates a lambda data source that registers the lambda function and associated role. + */ + datasource = (name: string, region: string): any => { + return new AppSync.DataSource({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + Name: FunctionResourceIDs.FunctionDataSourceID(name, region), + Type: 'AWS_LAMBDA', + ServiceRoleArn: Fn.GetAtt(FunctionResourceIDs.FunctionIAMRoleID(name, region), 'Arn'), + LambdaConfig: { + LambdaFunctionArn: lambdaArnResource(name, region), + }, + }).dependsOn(FunctionResourceIDs.FunctionIAMRoleID(name, region)); + }; + + /** + * Create a new pipeline function that calls out to the lambda function and returns the value. + */ + function = (name: string, region: string): any => { + return new AppSync.FunctionConfiguration({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + Name: FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region), + DataSourceName: FunctionResourceIDs.FunctionDataSourceID(name, region), + FunctionVersion: '2018-05-29', + RequestMappingTemplate: printBlock(`Invoke AWS Lambda data source: ${FunctionResourceIDs.FunctionDataSourceID(name, region)}`)( + obj({ + version: str('2018-05-29'), + operation: str('Invoke'), + payload: obj({ + typeName: str('$ctx.stash.get("typeName")'), + fieldName: str('$ctx.stash.get("fieldName")'), + arguments: ref('util.toJson($ctx.arguments)'), + identity: ref('util.toJson($ctx.identity)'), + source: ref('util.toJson($ctx.source)'), + request: ref('util.toJson($ctx.request)'), + prev: ref('util.toJson($ctx.prev)'), + }), + }) + ), + ResponseMappingTemplate: printBlock('Handle error or return result')( + compoundExpression([ + iff(ref('ctx.error'), raw('$util.error($ctx.error.message, $ctx.error.type)')), + raw('$util.toJson($ctx.result)'), + ]) + ), + }).dependsOn(FunctionResourceIDs.FunctionDataSourceID(name, region)); + }; + + /** + * Create a resolver of one that calls the "function" function. + */ + resolver = (type: string, field: string, name: string, region?: string): any => { + return new AppSync.Resolver({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + TypeName: type, + FieldName: field, + Kind: 'PIPELINE', + PipelineConfig: { + Functions: [Fn.GetAtt(FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region), 'FunctionId')], + }, + RequestMappingTemplate: printBlock('Stash resolver specific context.')( + compoundExpression([qref(`$ctx.stash.put("typeName", "${type}")`), qref(`$ctx.stash.put("fieldName", "${field}")`), obj({})]) + ), + ResponseMappingTemplate: '$util.toJson($ctx.prev.result)', + }).dependsOn(FunctionResourceIDs.FunctionAppSyncFunctionConfigurationID(name, region)); + }; - appendFunctionToResolver(resolver: any, functionId: string) { - if ( - resolver.Properties.PipelineConfig && - resolver.Properties.PipelineConfig.Functions && - Array.isArray(resolver.Properties.PipelineConfig.Functions) - ) { - resolver.Properties.PipelineConfig.Functions.push( - Fn.GetAtt(functionId, "FunctionId") - ); - } - return resolver; + appendFunctionToResolver(resolver: any, functionId: string) { + if ( + resolver.Properties.PipelineConfig && + resolver.Properties.PipelineConfig.Functions && + Array.isArray(resolver.Properties.PipelineConfig.Functions) + ) { + resolver.Properties.PipelineConfig.Functions.push(Fn.GetAtt(functionId, 'FunctionId')); } + return resolver; + } } diff --git a/packages/graphql-function-transformer/src/__tests__/FunctionTransformer.test.ts b/packages/graphql-function-transformer/src/__tests__/FunctionTransformer.test.ts index c749e3cb82..56a34d32b0 100644 --- a/packages/graphql-function-transformer/src/__tests__/FunctionTransformer.test.ts +++ b/packages/graphql-function-transformer/src/__tests__/FunctionTransformer.test.ts @@ -1,121 +1,109 @@ -import GraphQLTransform, { Transformer } from 'graphql-transformer-core' -import FunctionTransformer from '../FunctionTransformer' +import GraphQLTransform, { Transformer } from 'graphql-transformer-core'; +import FunctionTransformer from '../FunctionTransformer'; test('FunctionTransformer should add a datasource, IAM role and a resolver resources', () => { - const validSchema = ` + const validSchema = ` type Query { echo(msg: String): String @function(name: "echofunction-\${env}") } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new FunctionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new FunctionTransformer()], + }); - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - // EchofunctionLambdaDataSource, EchofunctionLambdaDataSourceRole, QueryEchoResolver, GraphQLSchema - expect(Object.keys(out.stacks.FunctionDirectiveStack.Resources).length).toEqual(4) + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + // EchofunctionLambdaDataSource, EchofunctionLambdaDataSourceRole, QueryEchoResolver, GraphQLSchema + expect(Object.keys(out.stacks.FunctionDirectiveStack.Resources).length).toEqual(4); - let expectedLambdaArn = 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}' - // datasource - let datasourceResource = out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSource - expect(datasourceResource).toBeDefined() - expect( - datasourceResource.Properties.LambdaConfig.LambdaFunctionArn['Fn::If'][1]['Fn::Sub'][0], - ).toEqual(expectedLambdaArn) + let expectedLambdaArn = 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}'; + // datasource + let datasourceResource = out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSource; + expect(datasourceResource).toBeDefined(); + expect(datasourceResource.Properties.LambdaConfig.LambdaFunctionArn['Fn::If'][1]['Fn::Sub'][0]).toEqual(expectedLambdaArn); - // IAM role - let iamRoleResource = out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSourceRole - expect(iamRoleResource).toBeDefined() - expect( - iamRoleResource.Properties.AssumeRolePolicyDocument.Statement[0].Principal.Service - ).toEqual('appsync.amazonaws.com') - expect( - iamRoleResource.Properties.AssumeRolePolicyDocument.Statement[0].Action - ).toEqual('sts:AssumeRole') - expect( - iamRoleResource.Properties.Policies[0].PolicyDocument.Statement[0].Action[0] - ).toEqual('lambda:InvokeFunction') - expect( - iamRoleResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]['Fn::Sub'][0] - ).toEqual(expectedLambdaArn) - - // Resolver - let resolverResource = out.stacks.FunctionDirectiveStack.Resources.QueryechoResolver - expect(resolverResource).toBeDefined() - expect(resolverResource.Properties.FieldName).toEqual("echo") - expect(resolverResource.Properties.TypeName).toEqual("Query") - expect(resolverResource.Properties.Kind).toEqual('PIPELINE') - expect(resolverResource.Properties.PipelineConfig.Functions.length).toEqual(1) -}) + // IAM role + let iamRoleResource = out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSourceRole; + expect(iamRoleResource).toBeDefined(); + expect(iamRoleResource.Properties.AssumeRolePolicyDocument.Statement[0].Principal.Service).toEqual('appsync.amazonaws.com'); + expect(iamRoleResource.Properties.AssumeRolePolicyDocument.Statement[0].Action).toEqual('sts:AssumeRole'); + expect(iamRoleResource.Properties.Policies[0].PolicyDocument.Statement[0].Action[0]).toEqual('lambda:InvokeFunction'); + expect(iamRoleResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]['Fn::Sub'][0]).toEqual(expectedLambdaArn); + + // Resolver + let resolverResource = out.stacks.FunctionDirectiveStack.Resources.QueryechoResolver; + expect(resolverResource).toBeDefined(); + expect(resolverResource.Properties.FieldName).toEqual('echo'); + expect(resolverResource.Properties.TypeName).toEqual('Query'); + expect(resolverResource.Properties.Kind).toEqual('PIPELINE'); + expect(resolverResource.Properties.PipelineConfig.Functions.length).toEqual(1); +}); test('two @function directives for the same lambda should produce a single datasource, single role and two resolvers', () => { - const validSchema = ` + const validSchema = ` type Query { echo(msg: String): String @function(name: "echofunction-\${env}") magic(msg: String): String @function(name: "echofunction-\${env}") } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new FunctionTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new FunctionTransformer()], + }); - const out = transformer.transform(validSchema) - expect(out).toBeDefined() - expect(Object.keys(out.stacks.FunctionDirectiveStack.Resources).length).toEqual(5) - expect(out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSource).toBeDefined() - expect(out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSourceRole).toBeDefined() - expect(out.stacks.FunctionDirectiveStack.Resources.QueryechoResolver).toBeDefined() - expect(out.stacks.FunctionDirectiveStack.Resources.QuerymagicResolver).toBeDefined() -}) + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + expect(Object.keys(out.stacks.FunctionDirectiveStack.Resources).length).toEqual(5); + expect(out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSource).toBeDefined(); + expect(out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSourceRole).toBeDefined(); + expect(out.stacks.FunctionDirectiveStack.Resources.QueryechoResolver).toBeDefined(); + expect(out.stacks.FunctionDirectiveStack.Resources.QuerymagicResolver).toBeDefined(); +}); test('two @function directives for the same field should be valid', () => { - const validSchema = ` + const validSchema = ` type Query { echo(msg: String): String @function(name: "echofunction-\${env}") @function(name: "otherfunction") } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new FunctionTransformer() - ] - }) - const out = transformer.transform(validSchema) - let resolverResource = out.stacks.FunctionDirectiveStack.Resources.QueryechoResolver - expect(resolverResource).toBeDefined() - expect(resolverResource.Properties.FieldName).toEqual("echo") - expect(resolverResource.Properties.TypeName).toEqual("Query") - expect(resolverResource.Properties.PipelineConfig.Functions.length).toEqual(2) - const otherFunctionIamResource = out.stacks.FunctionDirectiveStack.Resources.OtherfunctionLambdaDataSourceRole; - expect(otherFunctionIamResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]["Fn::Sub"][0]).toEqual('arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:otherfunction'); - const echoFunctionIamResource = out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSourceRole; - expect(echoFunctionIamResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]["Fn::Sub"][0]).toEqual('arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}'); - expect(echoFunctionIamResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]["Fn::Sub"][1].env.Ref).toEqual('env'); -}) + const transformer = new GraphQLTransform({ + transformers: [new FunctionTransformer()], + }); + const out = transformer.transform(validSchema); + let resolverResource = out.stacks.FunctionDirectiveStack.Resources.QueryechoResolver; + expect(resolverResource).toBeDefined(); + expect(resolverResource.Properties.FieldName).toEqual('echo'); + expect(resolverResource.Properties.TypeName).toEqual('Query'); + expect(resolverResource.Properties.PipelineConfig.Functions.length).toEqual(2); + const otherFunctionIamResource = out.stacks.FunctionDirectiveStack.Resources.OtherfunctionLambdaDataSourceRole; + expect(otherFunctionIamResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]['Fn::Sub'][0]).toEqual( + 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:otherfunction' + ); + const echoFunctionIamResource = out.stacks.FunctionDirectiveStack.Resources.EchofunctionLambdaDataSourceRole; + expect(echoFunctionIamResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]['Fn::Sub'][0]).toEqual( + 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:echofunction-${env}' + ); + expect(echoFunctionIamResource.Properties.Policies[0].PolicyDocument.Statement[0].Resource['Fn::If'][1]['Fn::Sub'][1].env.Ref).toEqual( + 'env' + ); +}); test('@function directive applied to Object should throw SchemaValidationError', () => { - const invalidSchema = ` + const invalidSchema = ` type Query @function(name: "echofunction-\${env}") { echo(msg: String): String @function(name: "echofunction-\${env}") } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new FunctionTransformer() - ] - }) - try { - transformer.transform(invalidSchema) - fail("SchemaValidationError is expected to be thrown") - } catch (error) { - expect(error.name).toEqual("SchemaValidationError") - } -}) + const transformer = new GraphQLTransform({ + transformers: [new FunctionTransformer()], + }); + try { + transformer.transform(invalidSchema); + fail('SchemaValidationError is expected to be thrown'); + } catch (error) { + expect(error.name).toEqual('SchemaValidationError'); + } +}); diff --git a/packages/graphql-function-transformer/src/index.ts b/packages/graphql-function-transformer/src/index.ts index a16c267e40..a18b2f19ef 100644 --- a/packages/graphql-function-transformer/src/index.ts +++ b/packages/graphql-function-transformer/src/index.ts @@ -1,2 +1,2 @@ import FunctionTransformer from './FunctionTransformer'; -export default FunctionTransformer; \ No newline at end of file +export default FunctionTransformer; diff --git a/packages/graphql-function-transformer/src/lambdaArns.ts b/packages/graphql-function-transformer/src/lambdaArns.ts index 8787d510d7..3bcd55eb98 100644 --- a/packages/graphql-function-transformer/src/lambdaArns.ts +++ b/packages/graphql-function-transformer/src/lambdaArns.ts @@ -2,33 +2,27 @@ import { Fn, Refs } from 'cloudform-types'; import { ResourceConstants } from 'graphql-transformer-common'; export function lambdaArnResource(name: string, region?: string) { - const substitutions = {}; - if (referencesEnv(name)) { - substitutions['env'] = Fn.Ref(ResourceConstants.PARAMETERS.Env) - } - return Fn.If( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - Fn.Sub( - lambdaArnKey(name, region), - substitutions - ), - Fn.Sub( - lambdaArnKey(removeEnvReference(name), region), - {} - ), - ) + const substitutions = {}; + if (referencesEnv(name)) { + substitutions['env'] = Fn.Ref(ResourceConstants.PARAMETERS.Env); + } + return Fn.If( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + Fn.Sub(lambdaArnKey(name, region), substitutions), + Fn.Sub(lambdaArnKey(removeEnvReference(name), region), {}) + ); } export function lambdaArnKey(name: string, region?: string) { - return region ? - `arn:aws:lambda:${region}:\${AWS::AccountId}:function:${name}` : - `arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${name}`; + return region + ? `arn:aws:lambda:${region}:\${AWS::AccountId}:function:${name}` + : `arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${name}`; } function referencesEnv(value: string) { - return value.match(/(\${env})/) !== null; + return value.match(/(\${env})/) !== null; } function removeEnvReference(value: string) { - return value.replace(/(-\${env})/, ''); -} \ No newline at end of file + return value.replace(/(-\${env})/, ''); +} diff --git a/packages/graphql-http-transformer/src/HttpTransformer.ts b/packages/graphql-http-transformer/src/HttpTransformer.ts index 7d45d4aa88..5a987c0c38 100644 --- a/packages/graphql-http-transformer/src/HttpTransformer.ts +++ b/packages/graphql-http-transformer/src/HttpTransformer.ts @@ -1,34 +1,31 @@ -import { Transformer, TransformerContext, TransformerContractError, gql } from 'graphql-transformer-core' +import { Transformer, TransformerContext, TransformerContractError, gql } from 'graphql-transformer-core'; import { - DirectiveNode, ObjectTypeDefinitionNode, - Kind, FieldDefinitionNode, InterfaceTypeDefinitionNode, - InputValueDefinitionNode, print -} from 'graphql' -import { ResourceFactory } from './resources' -import { - getDirectiveArgument, isScalar -} from 'graphql-transformer-common' -import { ResolverResourceIDs, HttpResourceIDs } from 'graphql-transformer-common' -import { - makeUrlParamInputObject, - makeHttpArgument, - makeHttpQueryInputObject, - makeHttpBodyInputObject -} from './definitions'; - -const HTTP_STACK_NAME = 'HttpStack' - -type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + DirectiveNode, + ObjectTypeDefinitionNode, + Kind, + FieldDefinitionNode, + InterfaceTypeDefinitionNode, + InputValueDefinitionNode, + print, +} from 'graphql'; +import { ResourceFactory } from './resources'; +import { getDirectiveArgument, isScalar } from 'graphql-transformer-common'; +import { ResolverResourceIDs, HttpResourceIDs } from 'graphql-transformer-common'; +import { makeUrlParamInputObject, makeHttpArgument, makeHttpQueryInputObject, makeHttpBodyInputObject } from './definitions'; + +const HTTP_STACK_NAME = 'HttpStack'; + +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; export interface HttpHeader { - key: String, - value: String + key: String; + value: String; } interface HttpDirectiveArgs { - method?: HttpMethod, - url: String, - headers: HttpHeader[] + method?: HttpMethod; + url: String; + headers: HttpHeader[]; } /** @@ -38,251 +35,214 @@ interface HttpDirectiveArgs { * Works with GET, POST, PUT, DELETE requests. */ export class HttpTransformer extends Transformer { - - resources: ResourceFactory - - static urlRegex = /(http(s)?:\/\/)|(\/.*)/g - - constructor() { - super( - 'HttpTransformer', - gql` - directive @http( - method: HttpMethod = GET, - url: String!, - headers: [HttpHeader] = [] - ) on FIELD_DEFINITION - enum HttpMethod { - GET - POST - PUT - DELETE - PATCH - } - input HttpHeader { - key: String, - value: String - } - ` - ) - this.resources = new ResourceFactory(); + resources: ResourceFactory; + + static urlRegex = /(http(s)?:\/\/)|(\/.*)/g; + + constructor() { + super( + 'HttpTransformer', + gql` + directive @http(method: HttpMethod = GET, url: String!, headers: [HttpHeader] = []) on FIELD_DEFINITION + enum HttpMethod { + GET + POST + PUT + DELETE + PATCH + } + input HttpHeader { + key: String + value: String + } + ` + ); + this.resources = new ResourceFactory(); + } + + public before = (ctx: TransformerContext): void => { + let directiveList: DirectiveNode[] = []; + + // gather all the http directives + for (const def of ctx.inputDocument.definitions) { + if (def.kind === Kind.OBJECT_TYPE_DEFINITION) { + for (const field of def.fields) { + const httpDirective = field.directives.find(dir => dir.name.value === 'http'); + if (httpDirective) { + directiveList.push(httpDirective); + } + } + } } - public before = (ctx: TransformerContext): void => { - let directiveList: DirectiveNode[] = [] - - // gather all the http directives - for (const def of ctx.inputDocument.definitions) { - if (def.kind === Kind.OBJECT_TYPE_DEFINITION) { - for (const field of def.fields) { - const httpDirective = field.directives.find(dir => dir.name.value === 'http') - if (httpDirective) { - directiveList.push(httpDirective) - } - } - } - } + // create all the datasources we will need for this schema + directiveList.forEach((value: DirectiveNode) => { + const url = getDirectiveArgument(value, 'url'); + // require a protocol in the url + const protocolMatcher = /^http(s)?:\/\//; + if (!protocolMatcher.test(url)) { + throw new TransformerContractError( + `@http directive at location ${value.loc.start} ` + `requires a url parameter that begins with http:// or https://.` + ); + } + // extract just the base url with protocol + const baseURL = url.replace(HttpTransformer.urlRegex, '$1'); + const dataSourceID = HttpResourceIDs.HttpDataSourceID(baseURL); + // only create one DataSource per base URL + if (!ctx.getResource(dataSourceID)) { + ctx.mapResourceToStack(HTTP_STACK_NAME, dataSourceID); + ctx.setResource(dataSourceID, this.resources.makeHttpDataSource(baseURL)); + } + }); + }; + + /** + * Create and configure the HTTP resolver for this field + */ + public field = ( + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + field: FieldDefinitionNode, + directive: DirectiveNode, + ctx: TransformerContext + ): void => { + ctx.mapResourceToStack(HTTP_STACK_NAME, ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value)); + const url: string = getDirectiveArgument(directive, 'url'); + const baseURL: string = url.replace(HttpTransformer.urlRegex, '$1'); + // split the url into pieces, and get the path part off the end + let path: string = url.split(/(http(s)?:\/\/|www\.)|(\/.*)/g).slice(-2, -1)[0]; + + // extract any URL parameters from the path + let urlParams: string[] = path.match(/:\w+/g); + let queryBodyArgsArray: InputValueDefinitionNode[] = field.arguments as InputValueDefinitionNode[]; + let newFieldArgsArray: InputValueDefinitionNode[] = []; + + if (urlParams) { + urlParams = urlParams.map(p => p.replace(':', '')); + + // if there are URL parameters, remove them from the array we'll use + // to create the query and body types + queryBodyArgsArray = field.arguments.filter(e => isScalar(e.type) && !urlParams.includes(e.name.value)); + + // replace each URL parameter with $ctx.args.params.parameter_name for use in resolver template + path = path.replace(/:\w+/g, (str: string) => { + return `\$\{ctx.args.params.${str.replace(':', '')}\}`; + }); + + const urlParamInputObject = makeUrlParamInputObject(parent, field, urlParams); + ctx.addInput(urlParamInputObject); + + newFieldArgsArray.push(makeHttpArgument('params', urlParamInputObject, true)); + } - // create all the datasources we will need for this schema - directiveList.forEach((value: DirectiveNode) => { - const url = getDirectiveArgument(value, 'url') - // require a protocol in the url - const protocolMatcher = /^http(s)?:\/\// - if (!protocolMatcher.test(url)) { - throw new TransformerContractError(`@http directive at location ${value.loc.start} ` + - `requires a url parameter that begins with http:// or https://.`) - } - // extract just the base url with protocol - const baseURL = url.replace(HttpTransformer.urlRegex, '$1') - const dataSourceID = HttpResourceIDs.HttpDataSourceID(baseURL) - // only create one DataSource per base URL - if (!ctx.getResource(dataSourceID)) { - ctx.mapResourceToStack(HTTP_STACK_NAME, dataSourceID) - ctx.setResource( - dataSourceID, - this.resources.makeHttpDataSource(baseURL) - ) - } - }) + let method: HttpMethod = getDirectiveArgument(directive, 'method'); + if (!method) { + method = 'GET'; } - /** - * Create and configure the HTTP resolver for this field - */ - public field = ( - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - field: FieldDefinitionNode, - directive: DirectiveNode, - ctx: TransformerContext - ): void => { - ctx.mapResourceToStack( - HTTP_STACK_NAME, - ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value) - ) - const url: string = getDirectiveArgument(directive, 'url') - const baseURL: string = url.replace(HttpTransformer.urlRegex, '$1') - // split the url into pieces, and get the path part off the end - let path: string = url.split(/(http(s)?:\/\/|www\.)|(\/.*)/g).slice(-2, -1)[0] + let headers: HttpHeader[] = getDirectiveArgument(directive, 'headers'); - // extract any URL parameters from the path - let urlParams: string[] = path.match(/:\w+/g) - let queryBodyArgsArray: InputValueDefinitionNode[] = field.arguments as InputValueDefinitionNode[] - let newFieldArgsArray: InputValueDefinitionNode[] = [] + if (!headers || !Array.isArray(headers)) { + headers = []; + } - if (urlParams) { - urlParams = urlParams.map((p) => p.replace(':', '')) + if (queryBodyArgsArray.length > 0) { + // for GET requests, leave the nullability of the query parameters unchanged - + // but for PUT, POST and PATCH, unwrap any non-nulls + const queryInputObject = makeHttpQueryInputObject(parent, field, queryBodyArgsArray, method === 'GET' ? false : true); + const bodyInputObject = makeHttpBodyInputObject(parent, field, queryBodyArgsArray, true); - // if there are URL parameters, remove them from the array we'll use - // to create the query and body types - queryBodyArgsArray = field.arguments - .filter((e) => ( - isScalar(e.type) && - !urlParams.includes(e.name.value)) - ) + // if any of the arguments for the query are non-null, + // make the newly generated type wrapper non-null too (only really applies for GET requests) + const makeNonNull = queryInputObject.fields.filter(a => a.type.kind === Kind.NON_NULL_TYPE).length > 0 ? true : false; - // replace each URL parameter with $ctx.args.params.parameter_name for use in resolver template - path = path.replace(/:\w+/g, (str: string) => { - return `\$\{ctx.args.params.${str.replace(':', '')}\}` - }) + ctx.addInput(queryInputObject); + newFieldArgsArray.push(makeHttpArgument('query', queryInputObject, makeNonNull)); - const urlParamInputObject = makeUrlParamInputObject(parent, field, urlParams) - ctx.addInput(urlParamInputObject) + if (method !== 'GET' && method !== 'DELETE') { + ctx.addInput(bodyInputObject); + newFieldArgsArray.push(makeHttpArgument('body', bodyInputObject, makeNonNull)); + } + } - newFieldArgsArray.push(makeHttpArgument('params', urlParamInputObject, true)) + // build the payload + switch (method) { + case 'GET': + const getResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value); + if (!ctx.getResource(getResourceID)) { + const getResolver = this.resources.makeGetResolver(baseURL, path, parent.name.value, field.name.value, headers); + ctx.setResource(getResourceID, getResolver); } - - let method: HttpMethod = getDirectiveArgument(directive, 'method') - if (!method) { - method = 'GET' + break; + case 'POST': + const postResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value); + if (!ctx.getResource(postResourceID)) { + const postResolver = this.resources.makePostResolver( + baseURL, + path, + parent.name.value, + field.name.value, + queryBodyArgsArray.filter(a => a.type.kind === Kind.NON_NULL_TYPE).map(a => a.name.value), + headers + ); + ctx.setResource(postResourceID, postResolver); } - - let headers : HttpHeader[] = getDirectiveArgument(directive, 'headers') - - if (!headers || !Array.isArray(headers)) { - headers = []; + break; + case 'PUT': + const putResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value); + if (!ctx.getResource(putResourceID)) { + const putResolver = this.resources.makePutResolver( + baseURL, + path, + parent.name.value, + field.name.value, + queryBodyArgsArray.filter(a => a.type.kind === Kind.NON_NULL_TYPE).map(a => a.name.value), + headers + ); + ctx.setResource(putResourceID, putResolver); } - - if (queryBodyArgsArray.length > 0) { - // for GET requests, leave the nullability of the query parameters unchanged - - // but for PUT, POST and PATCH, unwrap any non-nulls - const queryInputObject = makeHttpQueryInputObject( - parent, - field, - queryBodyArgsArray, - method === 'GET' ? false : true - ) - const bodyInputObject = makeHttpBodyInputObject( - parent, - field, - queryBodyArgsArray, - true - ) - - // if any of the arguments for the query are non-null, - // make the newly generated type wrapper non-null too (only really applies for GET requests) - const makeNonNull = queryInputObject.fields - .filter(a => a.type.kind === Kind.NON_NULL_TYPE).length > 0 ? true : false - - ctx.addInput(queryInputObject) - newFieldArgsArray.push(makeHttpArgument('query', queryInputObject, makeNonNull)) - - if (method !== 'GET' && method !== 'DELETE') { - ctx.addInput(bodyInputObject) - newFieldArgsArray.push(makeHttpArgument('body', bodyInputObject, makeNonNull)) - } + break; + case 'DELETE': + const deleteResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value); + if (!ctx.getResource(deleteResourceID)) { + const deleteResolver = this.resources.makeDeleteResolver(baseURL, path, parent.name.value, field.name.value, headers); + ctx.setResource(deleteResourceID, deleteResolver); } - - // build the payload - switch (method) { - case 'GET': - const getResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value) - if (!ctx.getResource(getResourceID)) { - const getResolver = this.resources.makeGetResolver( - baseURL, - path, - parent.name.value, - field.name.value, - headers - ) - ctx.setResource(getResourceID, getResolver) - } - break; - case 'POST': - const postResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value) - if (!ctx.getResource(postResourceID)) { - const postResolver = this.resources.makePostResolver( - baseURL, - path, - parent.name.value, - field.name.value, - queryBodyArgsArray - .filter(a => a.type.kind === Kind.NON_NULL_TYPE) - .map(a => a.name.value), - headers - ) - ctx.setResource(postResourceID, postResolver) - } - break; - case 'PUT': - const putResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value) - if (!ctx.getResource(putResourceID)) { - const putResolver = this.resources.makePutResolver( - baseURL, - path, - parent.name.value, - field.name.value, - queryBodyArgsArray - .filter(a => a.type.kind === Kind.NON_NULL_TYPE) - .map(a => a.name.value), - headers - ) - ctx.setResource(putResourceID, putResolver) - } - break; - case 'DELETE': - const deleteResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value) - if (!ctx.getResource(deleteResourceID)) { - const deleteResolver = this.resources.makeDeleteResolver(baseURL, path, parent.name.value, field.name.value, headers) - ctx.setResource(deleteResourceID, deleteResolver) - } - break; - case 'PATCH': - const patchResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value) - if (!ctx.getResource(patchResourceID)) { - const patchResolver = this.resources.makePatchResolver( - baseURL, - path, - parent.name.value, - field.name.value, - queryBodyArgsArray - .filter(a => a.type.kind === Kind.NON_NULL_TYPE) - .map(a => a.name.value), - headers - ) - ctx.setResource(patchResourceID, patchResolver) - } - break; - default: - // nothing + break; + case 'PATCH': + const patchResourceID = ResolverResourceIDs.ResolverResourceID(parent.name.value, field.name.value); + if (!ctx.getResource(patchResourceID)) { + const patchResolver = this.resources.makePatchResolver( + baseURL, + path, + parent.name.value, + field.name.value, + queryBodyArgsArray.filter(a => a.type.kind === Kind.NON_NULL_TYPE).map(a => a.name.value), + headers + ); + ctx.setResource(patchResourceID, patchResolver); } + break; + default: + // nothing + } - // now update the field if necessary with the new arguments - if (newFieldArgsArray.length > 0) { - const updatedField = { - ...field, - arguments: newFieldArgsArray - } + // now update the field if necessary with the new arguments + if (newFieldArgsArray.length > 0) { + const updatedField = { + ...field, + arguments: newFieldArgsArray, + }; - const mostRecentParent = ctx.getType(parent.name.value) as ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode - let updatedFieldsInParent = mostRecentParent.fields.filter(f => f.name.value !== field.name.value) - updatedFieldsInParent.push(updatedField) + const mostRecentParent = ctx.getType(parent.name.value) as ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode; + let updatedFieldsInParent = mostRecentParent.fields.filter(f => f.name.value !== field.name.value); + updatedFieldsInParent.push(updatedField); - const updatedParentType = { - ...mostRecentParent, - fields: updatedFieldsInParent - } + const updatedParentType = { + ...mostRecentParent, + fields: updatedFieldsInParent, + }; - ctx.putType(updatedParentType) - } + ctx.putType(updatedParentType); } + }; } diff --git a/packages/graphql-http-transformer/src/__tests__/HttpTransformer.test.ts b/packages/graphql-http-transformer/src/__tests__/HttpTransformer.test.ts index 5f9c652e7a..c9cdefbaac 100644 --- a/packages/graphql-http-transformer/src/__tests__/HttpTransformer.test.ts +++ b/packages/graphql-http-transformer/src/__tests__/HttpTransformer.test.ts @@ -1,10 +1,10 @@ -import { parse } from 'graphql' -import GraphQLTransform from 'graphql-transformer-core' -import { ResolverResourceIDs } from 'graphql-transformer-common' -import { HttpTransformer } from '../HttpTransformer' +import { parse } from 'graphql'; +import GraphQLTransform from 'graphql-transformer-core'; +import { ResolverResourceIDs } from 'graphql-transformer-common'; +import { HttpTransformer } from '../HttpTransformer'; test('Test HttpTransformer with four basic requests', () => { - const validSchema = ` + const validSchema = ` type Comment { id: ID! content: String @http(method: POST, url: "http://www.api.com/ping") @@ -13,25 +13,23 @@ test('Test HttpTransformer with four basic requests', () => { evenMore: String @http(method: DELETE, url: "http://www.google.com/query/id") stillMore: String @http(method: PATCH, url: "https://www.api.com/ping/id") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new HttpTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - // expect(out.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content2')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'more')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'evenMore')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'stillMore')]).toBeTruthy() + `; + const transformer = new GraphQLTransform({ + transformers: [new HttpTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + // expect(out.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() + const schemaDoc = parse(out.schema); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content2')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'more')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'evenMore')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'stillMore')]).toBeTruthy(); }); test('Test HttpTransformer with URL params happy case', () => { - const validSchema = ` + const validSchema = ` type Comment { id: ID! title: String @@ -65,44 +63,40 @@ test('Test HttpTransformer with URL params happy case', () => { title: String body: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new HttpTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - // expect(out.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complex')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complexAgain')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complexPost')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complexPut')]).toBeTruthy() - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'deleter')]).toBeTruthy() + `; + const transformer = new GraphQLTransform({ + transformers: [new HttpTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + // expect(out.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() + const schemaDoc = parse(out.schema); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complex')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complexAgain')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complexPost')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'complexPut')]).toBeTruthy(); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'deleter')]).toBeTruthy(); }); test('Test that HttpTransformer throws an error when missing protocol in URL argument', () => { - const validSchema = ` + const validSchema = ` type Comment { id: ID! content: String @http(method: POST, url: "www.api.com/ping") } - ` - try { - const transformer = new GraphQLTransform({ - transformers: [ - new HttpTransformer() - ] - }) - const out = transformer.transform(validSchema); - } catch (e) { - expect(e.name).toEqual('TransformerContractError') - } + `; + try { + const transformer = new GraphQLTransform({ + transformers: [new HttpTransformer()], + }); + const out = transformer.transform(validSchema); + } catch (e) { + expect(e.name).toEqual('TransformerContractError'); + } }); test('Test HttpTransformer with URL and headers params happy case', () => { - const validSchema = ` + const validSchema = ` type Comment { id: ID! content: String @http(url: "https://www.api.com/ping", headers: [{key: "X-Header", value: "X-Header-Value"}]) @@ -116,29 +110,26 @@ test('Test HttpTransformer with URL and headers params happy case', () => { userId: Int! ): String @http(method: PUT, url: "https://jsonplaceholder.typicode.com/posts/:title/:id", headers: [{key: "X-Header", value: "X-Header-ValuePut"}]) } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new HttpTransformer() - ] - }) - const out = transformer.transform(validSchema); - - expect(out).toBeDefined() - // expect(out.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() - const schemaDoc = parse(out.schema) - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')]).toBeTruthy() - expect(out.resolvers['Comment.content.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-Value"))'); - expect(out.resolvers['Comment.contentDelete.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValueDelete"))'); - expect(out.resolvers['Comment.contentPatch.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValuePatch"))'); - expect(out.resolvers['Comment.contentPost.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValuePost"))'); - expect(out.resolvers['Comment.complexPut.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValuePut"))'); + `; + + const transformer = new GraphQLTransform({ + transformers: [new HttpTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + // expect(out.Resources[ResolverResourceIDs.ResolverResourceID('Post', 'comments')]).toBeTruthy() + const schemaDoc = parse(out.schema); + expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')]).toBeTruthy(); + expect(out.resolvers['Comment.content.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-Value"))'); + expect(out.resolvers['Comment.contentDelete.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValueDelete"))'); + expect(out.resolvers['Comment.contentPatch.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValuePatch"))'); + expect(out.resolvers['Comment.contentPost.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValuePost"))'); + expect(out.resolvers['Comment.complexPut.req.vtl']).toContain('$util.qr($headers.put("X-Header", "X-Header-ValuePut"))'); }); test('Test HttpTransformer with four basic requests with env on the URI', () => { - const validSchema = ` + const validSchema = ` type Comment { id: ID! content: String @http(method: POST, url: "http://www.api.com/ping\${env}") @@ -147,34 +138,72 @@ test('Test HttpTransformer with four basic requests with env on the URI', () => evenMore: String @http(method: DELETE, url: "http://www.google.com/query/id\${env}") stillMore: String @http(method: PATCH, url: "https://www.api.com/ping/id\${env}") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new HttpTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const schemaDoc = parse(out.schema) - - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "content")].Properties.RequestMappingTemplate['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "content")].Properties.RequestMappingTemplate['Fn::Sub'][1].env.Ref).toBe('env') - - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "content2")].Properties.RequestMappingTemplate['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "content2")].Properties.RequestMappingTemplate['Fn::Sub'][1].env.Ref).toBe('env') - - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "more")].Properties.RequestMappingTemplate['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "more")].Properties.RequestMappingTemplate['Fn::Sub'][1].env.Ref).toBe('env') - - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "evenMore")].Properties.RequestMappingTemplate['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "evenMore")].Properties.RequestMappingTemplate['Fn::Sub'][1].env.Ref).toBe('env') - - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "stillMore")].Properties.RequestMappingTemplate['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID("Comment", "stillMore")].Properties.RequestMappingTemplate['Fn::Sub'][1].env.Ref).toBe('env') + `; + const transformer = new GraphQLTransform({ + transformers: [new HttpTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const schemaDoc = parse(out.schema); + + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][0] + ).toContain('${env}'); + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][1].env.Ref + ).toBe('env'); + + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content2')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][0] + ).toContain('${env}'); + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content2')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][1].env.Ref + ).toBe('env'); + + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'more')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][0] + ).toContain('${env}'); + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'more')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][1].env.Ref + ).toBe('env'); + + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'evenMore')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][0] + ).toContain('${env}'); + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'evenMore')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][1].env.Ref + ).toBe('env'); + + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'stillMore')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][0] + ).toContain('${env}'); + expect( + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'stillMore')].Properties.RequestMappingTemplate[ + 'Fn::Sub' + ][1].env.Ref + ).toBe('env'); }); test('Test HttpTransformer with four basic requests with env on the hostname', () => { - const validSchema = ` + const validSchema = ` type Comment { id: ID! content: String @http(method: POST, url: "http://\${env}www.api.com/ping") @@ -183,35 +212,43 @@ test('Test HttpTransformer with four basic requests with env on the hostname', ( evenMore: String @http(method: DELETE, url: "http://\${env}www.google.com/query/id") stillMore: String @http(method: PATCH, url: "https://\${env}www.api.com/ping/id") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new HttpTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - - const schemaDoc = parse(out.schema) + `; + const transformer = new GraphQLTransform({ + transformers: [new HttpTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); - const contentDatasource = out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')].Properties.DataSourceName['Fn::GetAtt'][0]; - expect(out.stacks.HttpStack.Resources[contentDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[contentDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env') + const schemaDoc = parse(out.schema); - const content2Datasource = out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content2')].Properties.DataSourceName['Fn::GetAtt'][0]; - expect(out.stacks.HttpStack.Resources[content2Datasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[content2Datasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env') + const contentDatasource = + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content')].Properties.DataSourceName['Fn::GetAtt'][0]; + expect(out.stacks.HttpStack.Resources[contentDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}'); + expect(out.stacks.HttpStack.Resources[contentDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env'); - const moreDatasource = out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'more')].Properties.DataSourceName['Fn::GetAtt'][0]; - expect(out.stacks.HttpStack.Resources[moreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[moreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env') + const content2Datasource = + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'content2')].Properties.DataSourceName[ + 'Fn::GetAtt' + ][0]; + expect(out.stacks.HttpStack.Resources[content2Datasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}'); + expect(out.stacks.HttpStack.Resources[content2Datasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env'); - const evenMoreDatasource = out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'evenMore')].Properties.DataSourceName['Fn::GetAtt'][0]; - expect(out.stacks.HttpStack.Resources[evenMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[evenMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env') + const moreDatasource = + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'more')].Properties.DataSourceName['Fn::GetAtt'][0]; + expect(out.stacks.HttpStack.Resources[moreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}'); + expect(out.stacks.HttpStack.Resources[moreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env'); - const stillMoreDatasource = out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'stillMore')].Properties.DataSourceName['Fn::GetAtt'][0]; - expect(out.stacks.HttpStack.Resources[stillMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}') - expect(out.stacks.HttpStack.Resources[stillMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env') + const evenMoreDatasource = + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'evenMore')].Properties.DataSourceName[ + 'Fn::GetAtt' + ][0]; + expect(out.stacks.HttpStack.Resources[evenMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}'); + expect(out.stacks.HttpStack.Resources[evenMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env'); -}); \ No newline at end of file + const stillMoreDatasource = + out.stacks.HttpStack.Resources[ResolverResourceIDs.ResolverResourceID('Comment', 'stillMore')].Properties.DataSourceName[ + 'Fn::GetAtt' + ][0]; + expect(out.stacks.HttpStack.Resources[stillMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][0]).toContain('${env}'); + expect(out.stacks.HttpStack.Resources[stillMoreDatasource].Properties.HttpConfig.Endpoint['Fn::Sub'][1].env.Ref).toBe('env'); +}); diff --git a/packages/graphql-http-transformer/src/definitions.ts b/packages/graphql-http-transformer/src/definitions.ts index c160f8690e..a66de45473 100644 --- a/packages/graphql-http-transformer/src/definitions.ts +++ b/packages/graphql-http-transformer/src/definitions.ts @@ -1,124 +1,119 @@ import { - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - InterfaceTypeDefinitionNode, - ObjectTypeDefinitionNode, - FieldDefinitionNode, - Kind, - print -} from 'graphql' + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, + FieldDefinitionNode, + Kind, + print, +} from 'graphql'; import { - makeInputValueDefinition, - makeNonNullType, - makeNamedType, - ModelResourceIDs, - isScalar, - getBaseType, - withNamedNodeNamed, - makeArgument, - STANDARD_SCALARS, - unwrapNonNull + makeInputValueDefinition, + makeNonNullType, + makeNamedType, + ModelResourceIDs, + isScalar, + getBaseType, + withNamedNodeNamed, + makeArgument, + STANDARD_SCALARS, + unwrapNonNull, } from 'graphql-transformer-common'; import { TransformerContext } from 'graphql-transformer-core'; -export function makeHttpArgument( - name: string, - inputType: InputObjectTypeDefinitionNode, - makeNonNull: boolean -): InputValueDefinitionNode { - // the URL params type that we create will need to be non-null, so build in some flexibility here - const type = makeNonNull ? makeNonNullType(makeNamedType(inputType.name.value)) - : makeNamedType(inputType.name.value) - return makeInputValueDefinition(name, type) +export function makeHttpArgument(name: string, inputType: InputObjectTypeDefinitionNode, makeNonNull: boolean): InputValueDefinitionNode { + // the URL params type that we create will need to be non-null, so build in some flexibility here + const type = makeNonNull ? makeNonNullType(makeNamedType(inputType.name.value)) : makeNamedType(inputType.name.value); + return makeInputValueDefinition(name, type); } export function makeUrlParamInputObject( - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - field: FieldDefinitionNode, - urlParams: string[] + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + field: FieldDefinitionNode, + urlParams: string[] ): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.UrlParamsInputObjectName(parent.name.value, field.name.value) - const urlParamFields = urlParams.map((param: string) => { - return makeInputValueDefinition(param, makeNonNullType(makeNamedType('String'))) - }) - console.log(`urlParamInputObject has these fields: ${print(urlParamFields)}`) - const fields: InputValueDefinitionNode[] = urlParamFields - return { - kind: 'InputObjectTypeDefinition', - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + const name = ModelResourceIDs.UrlParamsInputObjectName(parent.name.value, field.name.value); + const urlParamFields = urlParams.map((param: string) => { + return makeInputValueDefinition(param, makeNonNullType(makeNamedType('String'))); + }); + console.log(`urlParamInputObject has these fields: ${print(urlParamFields)}`); + const fields: InputValueDefinitionNode[] = urlParamFields; + return { + kind: 'InputObjectTypeDefinition', + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } export function makeHttpQueryInputObject( - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - field: FieldDefinitionNode, - queryArgArray: InputValueDefinitionNode[], - deNull: boolean + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + field: FieldDefinitionNode, + queryArgArray: InputValueDefinitionNode[], + deNull: boolean ): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.HttpQueryInputObjectName(parent.name.value, field.name.value) - // unwrap all the non-nulls in the argument array if the flag is set - const fields: InputValueDefinitionNode[] = deNull ? - queryArgArray.map((arg: InputValueDefinitionNode) => { - return { - ...arg, - type: unwrapNonNull(arg.type) - } - }) : - queryArgArray - return { - kind: 'InputObjectTypeDefinition', - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + const name = ModelResourceIDs.HttpQueryInputObjectName(parent.name.value, field.name.value); + // unwrap all the non-nulls in the argument array if the flag is set + const fields: InputValueDefinitionNode[] = deNull + ? queryArgArray.map((arg: InputValueDefinitionNode) => { + return { + ...arg, + type: unwrapNonNull(arg.type), + }; + }) + : queryArgArray; + return { + kind: 'InputObjectTypeDefinition', + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } export function makeHttpBodyInputObject( - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - field: FieldDefinitionNode, - bodyArgArray: InputValueDefinitionNode[], - deNull: boolean + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + field: FieldDefinitionNode, + bodyArgArray: InputValueDefinitionNode[], + deNull: boolean ): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.HttpBodyInputObjectName(parent.name.value, field.name.value) - // unwrap all the non-nulls in the argument array if the flag is set - const fields: InputValueDefinitionNode[] = deNull ? - bodyArgArray.map((arg: InputValueDefinitionNode) => { - return { - ...arg, - type: unwrapNonNull(arg.type) - } - }) : - bodyArgArray - return { - kind: 'InputObjectTypeDefinition', - // TODO: Service does not support new style descriptions so wait. - // description: { - // kind: 'StringValue', - // value: `Input type for ${obj.name.value} mutations` - // }, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [] - } + const name = ModelResourceIDs.HttpBodyInputObjectName(parent.name.value, field.name.value); + // unwrap all the non-nulls in the argument array if the flag is set + const fields: InputValueDefinitionNode[] = deNull + ? bodyArgArray.map((arg: InputValueDefinitionNode) => { + return { + ...arg, + type: unwrapNonNull(arg.type), + }; + }) + : bodyArgArray; + return { + kind: 'InputObjectTypeDefinition', + // TODO: Service does not support new style descriptions so wait. + // description: { + // kind: 'StringValue', + // value: `Input type for ${obj.name.value} mutations` + // }, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + }; } diff --git a/packages/graphql-http-transformer/src/index.ts b/packages/graphql-http-transformer/src/index.ts index 937a294d4b..ed436a72c7 100644 --- a/packages/graphql-http-transformer/src/index.ts +++ b/packages/graphql-http-transformer/src/index.ts @@ -1,3 +1,3 @@ -import { HttpTransformer } from './HttpTransformer' -export * from './HttpTransformer' -export default HttpTransformer +import { HttpTransformer } from './HttpTransformer'; +export * from './HttpTransformer'; +export default HttpTransformer; diff --git a/packages/graphql-http-transformer/src/resources.ts b/packages/graphql-http-transformer/src/resources.ts index ff5bb43f46..d6778c1e7b 100644 --- a/packages/graphql-http-transformer/src/resources.ts +++ b/packages/graphql-http-transformer/src/resources.ts @@ -1,331 +1,325 @@ -import Table, { GlobalSecondaryIndex, KeySchema, Projection, ProvisionedThroughput, AttributeDefinition } from 'cloudform-types/types/dynamoDb/table' -import Resolver from 'cloudform-types/types/appSync/resolver' -import Template from 'cloudform-types/types/template' -import { Fn, AppSync } from 'cloudform-types' -import { Value } from 'cloudform-types/types/dataTypes' +import Table, { + GlobalSecondaryIndex, + KeySchema, + Projection, + ProvisionedThroughput, + AttributeDefinition, +} from 'cloudform-types/types/dynamoDb/table'; +import Resolver from 'cloudform-types/types/appSync/resolver'; +import Template from 'cloudform-types/types/template'; +import { Fn, AppSync } from 'cloudform-types'; +import { Value } from 'cloudform-types/types/dataTypes'; import { - HttpMappingTemplate, str, print, printBlock, qref, - ref, obj, set, nul, - ifElse, compoundExpression, bool, equals, iff, raw, Expression, comment, or, and, parens -} from 'graphql-mapping-template' -import { InputValueDefinitionNode } from 'graphql' -import { ResourceConstants, ModelResourceIDs, HttpResourceIDs, makeNonNullType } from 'graphql-transformer-common' + HttpMappingTemplate, + str, + print, + printBlock, + qref, + ref, + obj, + set, + nul, + ifElse, + compoundExpression, + bool, + equals, + iff, + raw, + Expression, + comment, + or, + and, + parens, +} from 'graphql-mapping-template'; +import { InputValueDefinitionNode } from 'graphql'; +import { ResourceConstants, ModelResourceIDs, HttpResourceIDs, makeNonNullType } from 'graphql-transformer-common'; import { InvalidDirectiveError } from 'graphql-transformer-core'; import { HttpHeader } from './HttpTransformer'; export class ResourceFactory { + public makeParams() { + return {}; + } - public makeParams() { - return {} - } + /** + * Creates the barebones template for an application. + */ + public initTemplate(): Template { + return { + Parameters: this.makeParams(), + Resources: {}, + Outputs: {}, + }; + } - /** - * Creates the barebones template for an application. - */ - public initTemplate(): Template { - return { - Parameters: this.makeParams(), - Resources: {}, - Outputs: {} - } - } + public makeHttpDataSource(baseURL: string) { + return new AppSync.DataSource({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Name: HttpResourceIDs.HttpDataSourceID(baseURL), + Type: 'HTTP', + HttpConfig: { + Endpoint: this.replaceEnv(baseURL), + }, + }); + } - public makeHttpDataSource(baseURL: string) { - return new AppSync.DataSource({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Name: HttpResourceIDs.HttpDataSourceID(baseURL), - Type: 'HTTP', - HttpConfig: { - Endpoint: this.replaceEnv(baseURL) - } - }) - } + private referencesEnv(value: string): boolean { + return value.match(/(\${env})/) !== null; + } - private referencesEnv(value: string): boolean { - return value.match(/(\${env})/) !== null; + private replaceEnv(value: string): Value { + if (!this.referencesEnv(value)) { + return value; } - private replaceEnv(value: string) : Value { - if (!this.referencesEnv(value)) { - return value; - } + return Fn.Sub(value, { + env: Fn.Ref(ResourceConstants.PARAMETERS.Env), + }); + } - return Fn.Sub( - value, - { - env: Fn.Ref(ResourceConstants.PARAMETERS.Env) - } - ); - } - - private makeVtlStringArray(inputArray: string[]) { - let returnArray = `[` - inputArray.forEach((e: string) => returnArray += `\'${e}\', `) - return returnArray.slice(0, -2) + `]` - } + private makeVtlStringArray(inputArray: string[]) { + let returnArray = `[`; + inputArray.forEach((e: string) => (returnArray += `\'${e}\', `)); + return returnArray.slice(0, -2) + `]`; + } - private makeNonNullChecks(nonNullArgs: string[]) { - return compoundExpression([ - comment("START: Manually checking that all non-null arguments are provided either in the query or the body"), - iff( - or(nonNullArgs.map( - (arg: string) => parens(and([raw(`!$ctx.args.body.${arg}`), raw(`!$ctx.args.query.${arg}`)])) - ) - ), - ref('util.error("An argument you marked as Non-Null is not present ' + - 'in the query nor the body of your request."))') - ), - comment("END: Manually checking that all non-null arguments are provided either in the query or the body"), - ]) - } + private makeNonNullChecks(nonNullArgs: string[]) { + return compoundExpression([ + comment('START: Manually checking that all non-null arguments are provided either in the query or the body'), + iff( + or(nonNullArgs.map((arg: string) => parens(and([raw(`!$ctx.args.body.${arg}`), raw(`!$ctx.args.query.${arg}`)])))), + ref('util.error("An argument you marked as Non-Null is not present ' + 'in the query nor the body of your request."))') + ), + comment('END: Manually checking that all non-null arguments are provided either in the query or the body'), + ]); + } - /** - * Create a resolver that makes a GET request. It assumes the endpoint expects query parameters in the exact - * shape of the input arguments to the http directive. Returns the result in JSON format, or an error if the status code - * is not 200 - * @param type - */ - public makeGetResolver(baseURL: string, path: string, type: string, field: string, headers: HttpHeader[]) { - const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)) + /** + * Create a resolver that makes a GET request. It assumes the endpoint expects query parameters in the exact + * shape of the input arguments to the http directive. Returns the result in JSON format, or an error if the status code + * is not 200 + * @param type + */ + public makeGetResolver(baseURL: string, path: string, type: string, field: string, headers: HttpHeader[]) { + const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)); - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: this.replaceEnv( - print( - compoundExpression([ - set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), - qref('$headers.put("accept-encoding", "application/json")'), - ...parsedHeaders, - HttpMappingTemplate.getRequest({ - resourcePath: path, - params: obj({ - query: ref('util.toJson($ctx.args.query)'), - headers: ref('util.toJson($headers)') - }) - }), - ]) - ) - ), - ResponseMappingTemplate: print( - ifElse( - raw('$ctx.result.statusCode == 200'), - ifElse( - ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), - ref('utils.xml.toJsonString($ctx.result.body)'), - ref('ctx.result.body') - ), - ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') - ) - ) - })//.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) - } + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: this.replaceEnv( + print( + compoundExpression([ + set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), + qref('$headers.put("accept-encoding", "application/json")'), + ...parsedHeaders, + HttpMappingTemplate.getRequest({ + resourcePath: path, + params: obj({ + query: ref('util.toJson($ctx.args.query)'), + headers: ref('util.toJson($headers)'), + }), + }), + ]) + ) + ), + ResponseMappingTemplate: print( + ifElse( + raw('$ctx.result.statusCode == 200'), + ifElse( + ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), + ref('utils.xml.toJsonString($ctx.result.body)'), + ref('ctx.result.body') + ), + ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') + ) + ), + }); //.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + } - /** - * Create a resolver that makes a POST request. It allows the user to provide arguments as either query - * parameters or in the body of the request. - * request. Returns the result in JSON format, or an error if the status code is not 200. - * Forwards the headers from the request, adding that the content type is JSON. - * @param type - */ - public makePostResolver( - baseURL: string, - path: string, - type: string, - field: string, - nonNullArgs: string[], - headers: HttpHeader[] - ) { - const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)) + /** + * Create a resolver that makes a POST request. It allows the user to provide arguments as either query + * parameters or in the body of the request. + * request. Returns the result in JSON format, or an error if the status code is not 200. + * Forwards the headers from the request, adding that the content type is JSON. + * @param type + */ + public makePostResolver(baseURL: string, path: string, type: string, field: string, nonNullArgs: string[], headers: HttpHeader[]) { + const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)); - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: this.replaceEnv( - print( - compoundExpression([ - nonNullArgs.length > 0 ? this.makeNonNullChecks(nonNullArgs) : null, - set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), - qref('$headers.put("Content-Type", "application/json")'), - qref('$headers.put("accept-encoding", "application/json")'), - ...parsedHeaders, - HttpMappingTemplate.postRequest({ - resourcePath: path, - params: obj({ - body: ref('util.toJson($ctx.args.body)'), - query: ref('util.toJson($ctx.args.query)'), - headers: ref('util.toJson($headers)') - }) - }), - ]) - ) - ), - ResponseMappingTemplate: print( - ifElse( - raw('$ctx.result.statusCode == 200 || $ctx.result.statusCode == 201'), - // check if the content type returned is XML, and convert to JSON if so - ifElse( - ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), - ref('utils.xml.toJsonString($ctx.result.body)'), - ref('ctx.result.body') - ), - ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') - ) - ) - })//.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) - } + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: this.replaceEnv( + print( + compoundExpression([ + nonNullArgs.length > 0 ? this.makeNonNullChecks(nonNullArgs) : null, + set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), + qref('$headers.put("Content-Type", "application/json")'), + qref('$headers.put("accept-encoding", "application/json")'), + ...parsedHeaders, + HttpMappingTemplate.postRequest({ + resourcePath: path, + params: obj({ + body: ref('util.toJson($ctx.args.body)'), + query: ref('util.toJson($ctx.args.query)'), + headers: ref('util.toJson($headers)'), + }), + }), + ]) + ) + ), + ResponseMappingTemplate: print( + ifElse( + raw('$ctx.result.statusCode == 200 || $ctx.result.statusCode == 201'), + // check if the content type returned is XML, and convert to JSON if so + ifElse( + ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), + ref('utils.xml.toJsonString($ctx.result.body)'), + ref('ctx.result.body') + ), + ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') + ) + ), + }); //.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + } - /** - * Create a resolver that makes a PUT request. It allows the user to provide arguments as either query - * parameters or in the body of the request. - * Returns the result in JSON format, or an error if the status code is not 200. - * Forwards the headers from the request, adding that the content type is JSON. - * @param type - */ - public makePutResolver( - baseURL: string, - path: string, - type: string, - field: string, - nonNullArgs: string[], - headers: HttpHeader[] - ) { - const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)) + /** + * Create a resolver that makes a PUT request. It allows the user to provide arguments as either query + * parameters or in the body of the request. + * Returns the result in JSON format, or an error if the status code is not 200. + * Forwards the headers from the request, adding that the content type is JSON. + * @param type + */ + public makePutResolver(baseURL: string, path: string, type: string, field: string, nonNullArgs: string[], headers: HttpHeader[]) { + const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)); - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: this.replaceEnv( - print( - compoundExpression([ - nonNullArgs.length > 0 ? this.makeNonNullChecks(nonNullArgs) : null, - set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), - qref('$headers.put("Content-Type", "application/json")'), - qref('$headers.put("accept-encoding", "application/json")'), - ...parsedHeaders, - HttpMappingTemplate.putRequest({ - resourcePath: path, - params: obj({ - body: ref('util.toJson($ctx.args.body)'), - query: ref('util.toJson($ctx.args.query)'), - headers: ref('util.toJson($headers)') - }) - }), - ]) - ) - ), - ResponseMappingTemplate: print( - ifElse( - raw('$ctx.result.statusCode == 200 || $ctx.result.statusCode == 201'), - ifElse( - ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), - ref('utils.xml.toJsonString($ctx.result.body)'), - ref('ctx.result.body') - ), - ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') - ) - ) - })//.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) - } + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: this.replaceEnv( + print( + compoundExpression([ + nonNullArgs.length > 0 ? this.makeNonNullChecks(nonNullArgs) : null, + set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), + qref('$headers.put("Content-Type", "application/json")'), + qref('$headers.put("accept-encoding", "application/json")'), + ...parsedHeaders, + HttpMappingTemplate.putRequest({ + resourcePath: path, + params: obj({ + body: ref('util.toJson($ctx.args.body)'), + query: ref('util.toJson($ctx.args.query)'), + headers: ref('util.toJson($headers)'), + }), + }), + ]) + ) + ), + ResponseMappingTemplate: print( + ifElse( + raw('$ctx.result.statusCode == 200 || $ctx.result.statusCode == 201'), + ifElse( + ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), + ref('utils.xml.toJsonString($ctx.result.body)'), + ref('ctx.result.body') + ), + ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') + ) + ), + }); //.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + } - /** - * Create a resolver that makes a DELETE request. - * @param type - */ - public makeDeleteResolver(baseURL: string, path: string, type: string, field: string, headers: HttpHeader[]) { - const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)) + /** + * Create a resolver that makes a DELETE request. + * @param type + */ + public makeDeleteResolver(baseURL: string, path: string, type: string, field: string, headers: HttpHeader[]) { + const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)); - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: this.replaceEnv( - print( - compoundExpression([ - set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), - qref('$headers.put("accept-encoding", "application/json")'), - ...parsedHeaders, - HttpMappingTemplate.deleteRequest({ - resourcePath: path, - params: obj({ - headers: ref('util.toJson($headers)') - }) - }), - ]) - ) - ), - ResponseMappingTemplate: print( - ifElse( - raw('$ctx.result.statusCode == 200'), - ifElse( - ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), - ref('utils.xml.toJsonString($ctx.result.body)'), - ref('ctx.result.body') - ), - ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') - ) - ) - })//.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) - } + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: this.replaceEnv( + print( + compoundExpression([ + set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), + qref('$headers.put("accept-encoding", "application/json")'), + ...parsedHeaders, + HttpMappingTemplate.deleteRequest({ + resourcePath: path, + params: obj({ + headers: ref('util.toJson($headers)'), + }), + }), + ]) + ) + ), + ResponseMappingTemplate: print( + ifElse( + raw('$ctx.result.statusCode == 200'), + ifElse( + ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), + ref('utils.xml.toJsonString($ctx.result.body)'), + ref('ctx.result.body') + ), + ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') + ) + ), + }); //.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + } - /** - * Create a resolver that makes a PUT request. It allows the user to provide arguments as either query - * parameters or in the body of the request. - * Returns the result in JSON format, or an error if the status code is not 200. - * Forwards the headers from the request, adding that the content type is JSON. - * @param type - */ - public makePatchResolver( - baseURL: string, - path: string, - type: string, - field: string, - nonNullArgs: string[], - headers: HttpHeader[] - ) { - const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)) + /** + * Create a resolver that makes a PUT request. It allows the user to provide arguments as either query + * parameters or in the body of the request. + * Returns the result in JSON format, or an error if the status code is not 200. + * Forwards the headers from the request, adding that the content type is JSON. + * @param type + */ + public makePatchResolver(baseURL: string, path: string, type: string, field: string, nonNullArgs: string[], headers: HttpHeader[]) { + const parsedHeaders = headers.map(header => qref(`$headers.put("${header.key}", "${header.value}")`)); - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), - FieldName: field, - TypeName: type, - RequestMappingTemplate: this.replaceEnv( - print( - compoundExpression([ - nonNullArgs.length > 0 ? this.makeNonNullChecks(nonNullArgs) : null, - set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), - qref('$headers.put("Content-Type", "application/json")'), - qref('$headers.put("accept-encoding", "application/json")'), - ...parsedHeaders, - HttpMappingTemplate.patchRequest({ - resourcePath: path, - params: obj({ - body: ref('util.toJson($ctx.args.body)'), - query: ref('util.toJson($ctx.args.query)'), - headers: ref('util.toJson($headers)') - }) - }), - ]) - ) - ), - ResponseMappingTemplate: print( - ifElse( - raw('$ctx.result.statusCode == 200 || $ctx.result.statusCode == 201'), - ifElse( - ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), - ref('utils.xml.toJsonString($ctx.result.body)'), - ref('ctx.result.body') - ), - ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') - ) - ) - })//.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) - } + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(HttpResourceIDs.HttpDataSourceID(baseURL), 'Name'), + FieldName: field, + TypeName: type, + RequestMappingTemplate: this.replaceEnv( + print( + compoundExpression([ + nonNullArgs.length > 0 ? this.makeNonNullChecks(nonNullArgs) : null, + set(ref('headers'), ref('utils.http.copyHeaders($ctx.request.headers)')), + qref('$headers.put("Content-Type", "application/json")'), + qref('$headers.put("accept-encoding", "application/json")'), + ...parsedHeaders, + HttpMappingTemplate.patchRequest({ + resourcePath: path, + params: obj({ + body: ref('util.toJson($ctx.args.body)'), + query: ref('util.toJson($ctx.args.query)'), + headers: ref('util.toJson($headers)'), + }), + }), + ]) + ) + ), + ResponseMappingTemplate: print( + ifElse( + raw('$ctx.result.statusCode == 200 || $ctx.result.statusCode == 201'), + ifElse( + ref('ctx.result.headers.get("Content-Type").toLowerCase().contains("xml")'), + ref('utils.xml.toJsonString($ctx.result.body)'), + ref('ctx.result.body') + ), + ref('util.qr($util.appendError($ctx.result.body, $ctx.result.statusCode))') + ) + ), + }); //.dependsOn(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID) + } } diff --git a/packages/graphql-key-transformer/src/KeyTransformer.ts b/packages/graphql-key-transformer/src/KeyTransformer.ts index 02ce370c60..7bbf6c0cba 100644 --- a/packages/graphql-key-transformer/src/KeyTransformer.ts +++ b/packages/graphql-key-transformer/src/KeyTransformer.ts @@ -1,516 +1,562 @@ import { - Transformer, gql, TransformerContext, getDirectiveArguments, TransformerContractError, InvalidDirectiveError + Transformer, + gql, + TransformerContext, + getDirectiveArguments, + TransformerContractError, + InvalidDirectiveError, } from 'graphql-transformer-core'; import { - obj, str, ref, printBlock, compoundExpression, newline, raw, qref, set, Expression, print, - ifElse, iff, block, bool, forEach, list + obj, + str, + ref, + printBlock, + compoundExpression, + newline, + raw, + qref, + set, + Expression, + print, + ifElse, + iff, + block, + bool, + forEach, + list, } from 'graphql-mapping-template'; import { - ResolverResourceIDs, ResourceConstants, isNonNullType, - attributeTypeFromScalar, ModelResourceIDs, makeInputValueDefinition, - wrapNonNull, withNamedNodeNamed, - makeNonNullType, makeNamedType, getBaseType, - makeConnectionField, - makeScalarKeyConditionForType, applyKeyExpressionForCompositeKey, - makeCompositeKeyConditionInputForKey, makeCompositeKeyInputForKey, toCamelCase, graphqlName, toUpper + ResolverResourceIDs, + ResourceConstants, + isNonNullType, + attributeTypeFromScalar, + ModelResourceIDs, + makeInputValueDefinition, + wrapNonNull, + withNamedNodeNamed, + makeNonNullType, + makeNamedType, + getBaseType, + makeConnectionField, + makeScalarKeyConditionForType, + applyKeyExpressionForCompositeKey, + makeCompositeKeyConditionInputForKey, + makeCompositeKeyInputForKey, + toCamelCase, + graphqlName, + toUpper, } from 'graphql-transformer-common'; import { - ObjectTypeDefinitionNode, FieldDefinitionNode, DirectiveNode, - InputObjectTypeDefinitionNode, TypeNode, Kind, InputValueDefinitionNode, EnumTypeDefinitionNode + ObjectTypeDefinitionNode, + FieldDefinitionNode, + DirectiveNode, + InputObjectTypeDefinitionNode, + TypeNode, + Kind, + InputValueDefinitionNode, + EnumTypeDefinitionNode, } from 'graphql'; -import { AppSync, IAM, Fn, DynamoDB, Refs } from 'cloudform-types' +import { AppSync, IAM, Fn, DynamoDB, Refs } from 'cloudform-types'; import { Projection, GlobalSecondaryIndex, LocalSecondaryIndex } from 'cloudform-types/types/dynamoDb/table'; interface KeyArguments { - name?: string; - fields: string[]; - queryField?: string; + name?: string; + fields: string[]; + queryField?: string; } export default class KeyTransformer extends Transformer { - - constructor() { - super( - 'KeyTransformer', - gql`directive @key(name: String, fields: [String!]!, queryField: String) on OBJECT` - ) + constructor() { + super( + 'KeyTransformer', + gql` + directive @key(name: String, fields: [String!]!, queryField: String) on OBJECT + ` + ); + } + + /** + * Augment the table key structures based on the @key. + */ + object = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + this.validate(definition, directive, ctx); + this.updateIndexStructures(definition, directive, ctx); + this.updateSchema(definition, directive, ctx); + this.updateResolvers(definition, directive, ctx); + this.addKeyConditionInputs(definition, directive, ctx); + }; + + /** + * Update the existing @model table's index structures. Includes primary key, GSI, and LSIs. + * @param definition The object type definition node. + * @param directive The @key directive + * @param ctx The transformer context + */ + private updateIndexStructures = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + if (this.isPrimaryKey(directive)) { + // Set the table's primary key using the @key definition. + this.replacePrimaryKey(definition, directive, ctx); + } else { + // Append a GSI/LSI to the table configuration. + this.appendSecondaryIndex(definition, directive, ctx); + } + }; + + /** + * Update the structural components of the schema that are relevant to the new index structures. + * + * Updates: + * 1. getX with new primary key information. + * 2. listX with new primary key information. + * + * Creates: + * 1. A query field for each secondary index. + */ + private updateSchema = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + this.updateQueryFields(definition, directive, ctx); + this.updateInputObjects(definition, directive, ctx); + }; + + /** + * Update the get, list, create, update, and delete resolvers with updated key information. + */ + private updateResolvers = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const directiveArgs: KeyArguments = getDirectiveArguments(directive); + const getResolver = ctx.getResource(ResolverResourceIDs.DynamoDBGetResolverResourceID(definition.name.value)); + const listResolver = ctx.getResource(ResolverResourceIDs.DynamoDBListResolverResourceID(definition.name.value)); + const createResolver = ctx.getResource(ResolverResourceIDs.DynamoDBCreateResolverResourceID(definition.name.value)); + const updateResolver = ctx.getResource(ResolverResourceIDs.DynamoDBUpdateResolverResourceID(definition.name.value)); + const deleteResolver = ctx.getResource(ResolverResourceIDs.DynamoDBDeleteResolverResourceID(definition.name.value)); + if (this.isPrimaryKey(directive)) { + // When looking at a primary key we update the primary paths for writing/reading data. + // and ensure any composite sort keys for the primary index. + if (getResolver) { + getResolver.Properties.RequestMappingTemplate = joinSnippets([ + this.setKeySnippet(directive), + getResolver.Properties.RequestMappingTemplate, + ]); + } + if (listResolver) { + listResolver.Properties.RequestMappingTemplate = joinSnippets([ + print(setQuerySnippet(definition, directive, ctx)), + listResolver.Properties.RequestMappingTemplate, + ]); + } + if (createResolver) { + createResolver.Properties.RequestMappingTemplate = joinSnippets([ + this.setKeySnippet(directive, true), + ensureCompositeKeySnippet(directive), + createResolver.Properties.RequestMappingTemplate, + ]); + } + if (updateResolver) { + updateResolver.Properties.RequestMappingTemplate = joinSnippets([ + this.setKeySnippet(directive, true), + ensureCompositeKeySnippet(directive), + updateResolver.Properties.RequestMappingTemplate, + ]); + } + if (deleteResolver) { + deleteResolver.Properties.RequestMappingTemplate = joinSnippets([ + this.setKeySnippet(directive, true), + deleteResolver.Properties.RequestMappingTemplate, + ]); + } + } else { + // When looking at a secondary key we need to ensure any composite sort key values + // and validate update operations to protect the integrity of composite sort keys. + if (createResolver) { + createResolver.Properties.RequestMappingTemplate = joinSnippets([ + ensureCompositeKeySnippet(directive), + createResolver.Properties.RequestMappingTemplate, + ]); + } + if (updateResolver) { + updateResolver.Properties.RequestMappingTemplate = joinSnippets([ + this.validateKeyUpdateArgumentsSnippet(directive), + ensureCompositeKeySnippet(directive), + updateResolver.Properties.RequestMappingTemplate, + ]); + } + if (deleteResolver) { + deleteResolver.Properties.RequestMappingTemplate = joinSnippets([ + ensureCompositeKeySnippet(directive), + deleteResolver.Properties.RequestMappingTemplate, + ]); + } + if (directiveArgs.queryField) { + const queryTypeName = ctx.getQueryTypeName(); + const queryResolverId = ResolverResourceIDs.ResolverResourceID(queryTypeName, directiveArgs.queryField); + const queryResolver = makeQueryResolver(definition, directive, ctx); + ctx.mapResourceToStack(definition.name.value, queryResolverId); + ctx.setResource(queryResolverId, queryResolver); + } } + }; - /** - * Augment the table key structures based on the @key. - */ - object = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - this.validate(definition, directive, ctx); - this.updateIndexStructures(definition, directive, ctx); - this.updateSchema(definition, directive, ctx); - this.updateResolvers(definition, directive, ctx); - this.addKeyConditionInputs(definition, directive, ctx); - }; - - /** - * Update the existing @model table's index structures. Includes primary key, GSI, and LSIs. - * @param definition The object type definition node. - * @param directive The @key directive - * @param ctx The transformer context - */ - private updateIndexStructures = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - if (this.isPrimaryKey(directive)) { - // Set the table's primary key using the @key definition. - this.replacePrimaryKey(definition, directive, ctx); + private addKeyConditionInputs = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const args: KeyArguments = getDirectiveArguments(directive); + if (args.fields.length > 2) { + const compositeKeyFieldNames = args.fields.slice(1); + // To make sure we get the intended behavior and type conversion we have to keep the order of the fields + // as it is in the key field list + const compositeKeyFields = []; + for (const compositeKeyFieldName of compositeKeyFieldNames) { + const field = definition.fields.find(field => field.name.value === compositeKeyFieldName); + if (!field) { + throw new InvalidDirectiveError( + `Can't find field: ${compositeKeyFieldName} in ${definition.name.value}, but it was specified in the @key definition.` + ); } else { - // Append a GSI/LSI to the table configuration. - this.appendSecondaryIndex(definition, directive, ctx); + compositeKeyFields.push(field); } - } + } + const keyName = toUpper(args.name || 'Primary'); + const keyConditionInput = makeCompositeKeyConditionInputForKey(definition.name.value, keyName, compositeKeyFields); + if (!ctx.getType(keyConditionInput.name.value)) { + ctx.addInput(keyConditionInput); + } + const compositeKeyInput = makeCompositeKeyInputForKey(definition.name.value, keyName, compositeKeyFields); + if (!ctx.getType(compositeKeyInput.name.value)) { + ctx.addInput(compositeKeyInput); + } + } else if (args.fields.length === 2) { + const finalSortKeyFieldName = args.fields[1]; + const finalSortKeyField = definition.fields.find(f => f.name.value === finalSortKeyFieldName); + const typeResolver = (baseType: string) => { + const resolvedEnumType = ctx.getType(baseType) as EnumTypeDefinitionNode; + return resolvedEnumType ? 'String' : undefined; + }; + const sortKeyConditionInput = makeScalarKeyConditionForType(finalSortKeyField.type, typeResolver); + + if (!sortKeyConditionInput) { + const checkedKeyName = args.name ? args.name : ''; + throw new InvalidDirectiveError( + `Cannot resolve type for field '${finalSortKeyFieldName}' in @key '${checkedKeyName}' on type '${definition.name.value}'.` + ); + } - /** - * Update the structural components of the schema that are relevant to the new index structures. - * - * Updates: - * 1. getX with new primary key information. - * 2. listX with new primary key information. - * - * Creates: - * 1. A query field for each secondary index. - */ - private updateSchema = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - this.updateQueryFields(definition, directive, ctx); - this.updateInputObjects(definition, directive, ctx); + if (!ctx.getType(sortKeyConditionInput.name.value)) { + ctx.addInput(sortKeyConditionInput); + } } - - /** - * Update the get, list, create, update, and delete resolvers with updated key information. - */ - private updateResolvers = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const directiveArgs: KeyArguments = getDirectiveArguments(directive); - const getResolver = ctx.getResource(ResolverResourceIDs.DynamoDBGetResolverResourceID(definition.name.value)); - const listResolver = ctx.getResource(ResolverResourceIDs.DynamoDBListResolverResourceID(definition.name.value)); - const createResolver = ctx.getResource(ResolverResourceIDs.DynamoDBCreateResolverResourceID(definition.name.value)); - const updateResolver = ctx.getResource(ResolverResourceIDs.DynamoDBUpdateResolverResourceID(definition.name.value)); - const deleteResolver = ctx.getResource(ResolverResourceIDs.DynamoDBDeleteResolverResourceID(definition.name.value)); - if (this.isPrimaryKey(directive)) { - // When looking at a primary key we update the primary paths for writing/reading data. - // and ensure any composite sort keys for the primary index. - if (getResolver) { - getResolver.Properties.RequestMappingTemplate = joinSnippets([ - this.setKeySnippet(directive), - getResolver.Properties.RequestMappingTemplate - ]); - } - if (listResolver) { - listResolver.Properties.RequestMappingTemplate = joinSnippets([ - print(setQuerySnippet(definition, directive, ctx)), - listResolver.Properties.RequestMappingTemplate - ]); - } - if (createResolver) { - createResolver.Properties.RequestMappingTemplate = joinSnippets([ - this.setKeySnippet(directive, true), - ensureCompositeKeySnippet(directive), - createResolver.Properties.RequestMappingTemplate - ]); - } - if (updateResolver) { - updateResolver.Properties.RequestMappingTemplate = joinSnippets([ - this.setKeySnippet(directive, true), - ensureCompositeKeySnippet(directive), - updateResolver.Properties.RequestMappingTemplate - ]); - } - if (deleteResolver) { - deleteResolver.Properties.RequestMappingTemplate = joinSnippets([ - this.setKeySnippet(directive, true), - deleteResolver.Properties.RequestMappingTemplate - ]); - } - } else { - // When looking at a secondary key we need to ensure any composite sort key values - // and validate update operations to protect the integrity of composite sort keys. - if (createResolver) { - createResolver.Properties.RequestMappingTemplate = joinSnippets([ - ensureCompositeKeySnippet(directive), - createResolver.Properties.RequestMappingTemplate - ]); - } - if (updateResolver) { - updateResolver.Properties.RequestMappingTemplate = joinSnippets([ - this.validateKeyUpdateArgumentsSnippet(directive), - ensureCompositeKeySnippet(directive), - updateResolver.Properties.RequestMappingTemplate - ]); - } - if (deleteResolver) { - deleteResolver.Properties.RequestMappingTemplate = joinSnippets([ - ensureCompositeKeySnippet(directive), - deleteResolver.Properties.RequestMappingTemplate - ]); - } - if (directiveArgs.queryField) { - const queryTypeName = ctx.getQueryTypeName(); - const queryResolverId = ResolverResourceIDs.ResolverResourceID(queryTypeName, directiveArgs.queryField); - const queryResolver = makeQueryResolver(definition, directive, ctx); - ctx.mapResourceToStack(definition.name.value, queryResolverId); - ctx.setResource(queryResolverId, queryResolver); - } - } + }; + + /** + * Updates query fields to include any arguments required by the key structures. + * @param definition The object type definition node. + * @param directive The @key directive + * @param ctx The transformer context + */ + private updateQueryFields = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + this.updateGetField(definition, directive, ctx); + this.updateListField(definition, directive, ctx); + this.ensureQueryField(definition, directive, ctx); + }; + + // If the get field exists, update its arguments with primary key information. + private updateGetField = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + let query = ctx.getQuery(); + const getResourceID = ResolverResourceIDs.DynamoDBGetResolverResourceID(definition.name.value); + const getResolverResource = ctx.getResource(getResourceID); + if (getResolverResource && this.isPrimaryKey(directive)) { + // By default takes a single argument named 'id'. Replace it with the updated primary key structure. + let getField: FieldDefinitionNode = query.fields.find( + field => field.name.value === getResolverResource.Properties.FieldName + ) as FieldDefinitionNode; + const args: KeyArguments = getDirectiveArguments(directive); + const getArguments = args.fields.map(keyAttributeName => { + const keyField = definition.fields.find(field => field.name.value === keyAttributeName); + const keyArgument = makeInputValueDefinition(keyAttributeName, makeNonNullType(makeNamedType(getBaseType(keyField.type)))); + return keyArgument; + }); + getField = { ...getField, arguments: getArguments }; + query = { ...query, fields: query.fields.map(field => (field.name.value === getField.name.value ? getField : field)) }; + ctx.putType(query); } - - private addKeyConditionInputs = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const args: KeyArguments = getDirectiveArguments(directive); - if (args.fields.length > 2) { - const compositeKeyFieldNames = args.fields.slice(1); - // To make sure we get the intended behavior and type conversion we have to keep the order of the fields - // as it is in the key field list - const compositeKeyFields = []; - for (const compositeKeyFieldName of compositeKeyFieldNames) { - const field = definition.fields.find(field => field.name.value === compositeKeyFieldName); - if (!field) { - throw new InvalidDirectiveError(`Can't find field: ${compositeKeyFieldName} in ${definition.name.value}, but it was specified in the @key definition.`); - } else { - compositeKeyFields.push(field); - } - } - const keyName = toUpper(args.name || 'Primary'); - const keyConditionInput = makeCompositeKeyConditionInputForKey(definition.name.value, keyName, compositeKeyFields); - if (!ctx.getType(keyConditionInput.name.value)) { - ctx.addInput(keyConditionInput); - } - const compositeKeyInput = makeCompositeKeyInputForKey(definition.name.value, keyName, compositeKeyFields); - if (!ctx.getType(compositeKeyInput.name.value)) { - ctx.addInput(compositeKeyInput); - } - } else if (args.fields.length === 2) { - const finalSortKeyFieldName = args.fields[1]; - const finalSortKeyField = definition.fields.find(f => f.name.value === finalSortKeyFieldName); - const typeResolver = (baseType: string) => { - const resolvedEnumType = ctx.getType(baseType) as EnumTypeDefinitionNode; - return resolvedEnumType ? 'String' : undefined; - }; - const sortKeyConditionInput = makeScalarKeyConditionForType(finalSortKeyField.type, typeResolver); - - if (!sortKeyConditionInput) { - const checkedKeyName = args.name ? args.name : ""; - throw new InvalidDirectiveError(`Cannot resolve type for field '${finalSortKeyFieldName}' in @key '${checkedKeyName}' on type '${definition.name.value}'.`); - } - - if (!ctx.getType(sortKeyConditionInput.name.value)) { - ctx.addInput(sortKeyConditionInput); - } - } + }; + + // If the list field exists, update its arguments with primary key information. + private updateListField = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const listResourceID = ResolverResourceIDs.DynamoDBListResolverResourceID(definition.name.value); + const listResolverResource = ctx.getResource(listResourceID); + if (listResolverResource && this.isPrimaryKey(directive)) { + // By default takes a single argument named 'id'. Replace it with the updated primary key structure. + let query = ctx.getQuery(); + let listField: FieldDefinitionNode = query.fields.find( + field => field.name.value === listResolverResource.Properties.FieldName + ) as FieldDefinitionNode; + let listArguments: InputValueDefinitionNode[] = [...listField.arguments]; + const args: KeyArguments = getDirectiveArguments(directive); + if (args.fields.length > 2) { + listArguments = addCompositeSortKey(definition, args, listArguments); + listArguments = addHashField(definition, args, listArguments); + } else if (args.fields.length === 2) { + listArguments = addSimpleSortKey(ctx, definition, args, listArguments); + listArguments = addHashField(definition, args, listArguments); + } else { + listArguments = addHashField(definition, args, listArguments); + } + listArguments.push(makeInputValueDefinition('sortDirection', makeNamedType('ModelSortDirection'))); + listField = { ...listField, arguments: listArguments }; + query = { ...query, fields: query.fields.map(field => (field.name.value === listField.name.value ? listField : field)) }; + ctx.putType(query); } + }; - /** - * Updates query fields to include any arguments required by the key structures. - * @param definition The object type definition node. - * @param directive The @key directive - * @param ctx The transformer context - */ - private updateQueryFields = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - this.updateGetField(definition, directive, ctx); - this.updateListField(definition, directive, ctx); - this.ensureQueryField(definition, directive, ctx); + // If this is a secondary key and a queryField has been provided, create the query field. + private ensureQueryField = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const args: KeyArguments = getDirectiveArguments(directive); + if (args.queryField && !this.isPrimaryKey(directive)) { + let queryType = ctx.getQuery(); + let queryArguments = []; + if (args.fields.length > 2) { + queryArguments = addCompositeSortKey(definition, args, queryArguments); + queryArguments = addHashField(definition, args, queryArguments); + } else if (args.fields.length === 2) { + queryArguments = addSimpleSortKey(ctx, definition, args, queryArguments); + queryArguments = addHashField(definition, args, queryArguments); + } else { + queryArguments = addHashField(definition, args, queryArguments); + } + queryArguments.push(makeInputValueDefinition('sortDirection', makeNamedType('ModelSortDirection'))); + const queryField = makeConnectionField(args.queryField, definition.name.value, queryArguments); + queryType = { + ...queryType, + fields: [...queryType.fields, queryField], + }; + ctx.putType(queryType); } - - // If the get field exists, update its arguments with primary key information. - private updateGetField = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - let query = ctx.getQuery(); - const getResourceID = ResolverResourceIDs.DynamoDBGetResolverResourceID(definition.name.value); - const getResolverResource = ctx.getResource(getResourceID); - if (getResolverResource && this.isPrimaryKey(directive)) { - // By default takes a single argument named 'id'. Replace it with the updated primary key structure. - let getField: FieldDefinitionNode = query.fields.find(field => field.name.value === getResolverResource.Properties.FieldName) as FieldDefinitionNode; - const args: KeyArguments = getDirectiveArguments(directive); - const getArguments = args.fields.map(keyAttributeName => { - const keyField = definition.fields.find(field => field.name.value === keyAttributeName); - const keyArgument = makeInputValueDefinition(keyAttributeName, makeNonNullType(makeNamedType(getBaseType(keyField.type)))); - return keyArgument; - }) - getField = { ...getField, arguments: getArguments }; - query = { ...query, fields: query.fields.map(field => field.name.value === getField.name.value ? getField : field)} - ctx.putType(query); - } + }; + + // Update the create, update, and delete input objects to account for any changes to the primary key. + private updateInputObjects = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + if (this.isPrimaryKey(directive)) { + const directiveArgs: KeyArguments = getDirectiveArguments(directive); + const createInput = ctx.getType(ModelResourceIDs.ModelCreateInputObjectName(definition.name.value)) as InputObjectTypeDefinitionNode; + if (createInput) { + ctx.putType(replaceCreateInput(definition, createInput, directiveArgs.fields)); + } + const updateInput = ctx.getType(ModelResourceIDs.ModelUpdateInputObjectName(definition.name.value)) as InputObjectTypeDefinitionNode; + if (updateInput) { + ctx.putType(replaceUpdateInput(definition, updateInput, directiveArgs.fields)); + } + const deleteInput = ctx.getType(ModelResourceIDs.ModelDeleteInputObjectName(definition.name.value)) as InputObjectTypeDefinitionNode; + if (deleteInput) { + ctx.putType(replaceDeleteInput(definition, deleteInput, directiveArgs.fields)); + } } - - // If the list field exists, update its arguments with primary key information. - private updateListField = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const listResourceID = ResolverResourceIDs.DynamoDBListResolverResourceID(definition.name.value); - const listResolverResource = ctx.getResource(listResourceID); - if (listResolverResource && this.isPrimaryKey(directive)) { - // By default takes a single argument named 'id'. Replace it with the updated primary key structure. - let query = ctx.getQuery(); - let listField: FieldDefinitionNode = query.fields.find(field => field.name.value === listResolverResource.Properties.FieldName) as FieldDefinitionNode; - let listArguments: InputValueDefinitionNode[] = [ ...listField.arguments ]; - const args: KeyArguments = getDirectiveArguments(directive); - if (args.fields.length > 2) { - listArguments = addCompositeSortKey(definition, args, listArguments); - listArguments = addHashField(definition, args, listArguments); - } else if (args.fields.length === 2) { - listArguments = addSimpleSortKey(ctx, definition, args, listArguments); - listArguments = addHashField(definition, args, listArguments); - } else { - listArguments = addHashField(definition, args, listArguments); - } - listArguments.push(makeInputValueDefinition('sortDirection', makeNamedType('ModelSortDirection'))); - listField = { ...listField, arguments: listArguments }; - query = { ...query, fields: query.fields.map(field => field.name.value === listField.name.value ? listField : field)} - ctx.putType(query); - } + }; + + // Return a VTL snippet that sets the key for key for get, update, and delete operations. + private setKeySnippet = (directive: DirectiveNode, isMutation: boolean = false) => { + const directiveArgs = getDirectiveArguments(directive); + const cmds: Expression[] = [set(ref(ResourceConstants.SNIPPETS.ModelObjectKey), modelObjectKey(directiveArgs, isMutation))]; + return printBlock(`Set the primary @key`)(compoundExpression(cmds)); + }; + + // When issuing an update mutation that changes one part of a composite sort key, + // you must supply the entire key so that the underlying composite key can be resaved + // in the update operation. We only need to update for composite sort keys on secondary indexes. + private validateKeyUpdateArgumentsSnippet = (directive: DirectiveNode): string => { + const directiveArgs: KeyArguments = getDirectiveArguments(directive); + if (!this.isPrimaryKey(directive) && directiveArgs.fields.length > 2) { + const sortKeyFields = directiveArgs.fields.slice(1); + return printBlock(`Validate update mutation for @key '${directiveArgs.name}'`)( + compoundExpression([ + set(ref('hasSeenSomeKeyArg'), bool(false)), + set(ref('keyFieldNames'), list(sortKeyFields.map(f => str(f)))), + forEach(ref('keyFieldName'), ref('keyFieldNames'), [ + iff(raw(`$ctx.args.input.containsKey("$keyFieldName")`), set(ref('hasSeenSomeKeyArg'), bool(true)), true), + ]), + forEach(ref('keyFieldName'), ref('keyFieldNames'), [ + iff( + raw(`$hasSeenSomeKeyArg && !$ctx.args.input.containsKey("$keyFieldName")`), + raw( + `$util.error("When updating any part of the composite sort key for @key '${directiveArgs.name}',` + + ` you must provide all fields for the key. Missing key: '$keyFieldName'.")` + ) + ), + ]), + ]) + ); } - - // If this is a secondary key and a queryField has been provided, create the query field. - private ensureQueryField = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const args: KeyArguments = getDirectiveArguments(directive); - if (args.queryField && !this.isPrimaryKey(directive)) { - let queryType = ctx.getQuery(); - let queryArguments = []; - if (args.fields.length > 2) { - queryArguments = addCompositeSortKey(definition, args, queryArguments); - queryArguments = addHashField(definition, args, queryArguments); - } else if (args.fields.length === 2) { - queryArguments = addSimpleSortKey(ctx, definition, args, queryArguments); - queryArguments = addHashField(definition, args, queryArguments); - } else { - queryArguments = addHashField(definition, args, queryArguments); - } - queryArguments.push(makeInputValueDefinition('sortDirection', makeNamedType('ModelSortDirection'))); - const queryField = makeConnectionField(args.queryField, definition.name.value, queryArguments); - queryType = { - ...queryType, - fields: [...queryType.fields, queryField] - }; - ctx.putType(queryType); + return ''; + }; + + /** + * Validates the directive usage is semantically valid. + * + * 1. There may only be 1 @key without a name (specifying the primary key) + * 2. There may only be 1 @key with a given name. + * 3. @key must only reference existing scalar fields that map to DynamoDB S, N, or B. + * 4. A primary key must not include a 'queryField'. + * 5. If there is no primary sort key, make sure there are no more LSIs. + * @param definition The object type definition node. + * @param directive The @key directive + * @param ctx The transformer context + */ + private validate = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const directiveArgs = getDirectiveArguments(directive); + if (!directiveArgs.name) { + // 1. Make sure there are no more directives without a name. + for (const otherDirective of definition.directives.filter(d => d.name.value === 'key')) { + const otherArgs = getDirectiveArguments(otherDirective); + if (otherDirective !== directive && !otherArgs.name) { + throw new InvalidDirectiveError(`You may only supply one primary @key on type '${definition.name.value}'.`); } - } - - // Update the create, update, and delete input objects to account for any changes to the primary key. - private updateInputObjects = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - if (this.isPrimaryKey(directive)) { - const directiveArgs: KeyArguments = getDirectiveArguments(directive); - const createInput = ctx.getType(ModelResourceIDs.ModelCreateInputObjectName(definition.name.value)) as InputObjectTypeDefinitionNode; - if (createInput) { - ctx.putType(replaceCreateInput(definition, createInput, directiveArgs.fields)); - } - const updateInput = ctx.getType(ModelResourceIDs.ModelUpdateInputObjectName(definition.name.value)) as InputObjectTypeDefinitionNode; - if (updateInput) { - ctx.putType(replaceUpdateInput(definition, updateInput, directiveArgs.fields)); - } - const deleteInput = ctx.getType(ModelResourceIDs.ModelDeleteInputObjectName(definition.name.value)) as InputObjectTypeDefinitionNode; - if (deleteInput) { - ctx.putType(replaceDeleteInput(definition, deleteInput, directiveArgs.fields)); - } + // 5. If there is no primary sort key, make sure there are no more LSIs. + const hasPrimarySortKey = directiveArgs.fields.length > 1; + const primaryHashField = directiveArgs.fields[0]; + const otherHashField = otherArgs.fields[0]; + if ( + otherDirective !== directive && + !hasPrimarySortKey && + // If the primary key and other key share the first field and are not the same directive it is an LSI. + primaryHashField === otherHashField + ) { + throw new InvalidDirectiveError( + `Invalid @key "${otherArgs.name}". You may not create a @key where the first field in 'fields' ` + + `is the same as that of the primary @key unless the primary @key has multiple 'fields'. ` + + `You cannot have a local secondary index without a sort key in the primary index.` + ); } + } + // 4. Make sure that a 'queryField' is not included on a primary @key. + if (directiveArgs.queryField) { + throw new InvalidDirectiveError(`You cannot pass 'queryField' to the primary @key on type '${definition.name.value}'.`); + } + } else { + // 2. Make sure there are no more directives with the same name. + for (const otherDirective of definition.directives.filter(d => d.name.value === 'key')) { + const otherArgs = getDirectiveArguments(otherDirective); + if (otherDirective !== directive && otherArgs.name === directiveArgs.name) { + throw new InvalidDirectiveError( + `You may only supply one @key with the name '${directiveArgs.name}' on type '${definition.name.value}'.` + ); + } + } } - - // Return a VTL snippet that sets the key for key for get, update, and delete operations. - private setKeySnippet = (directive: DirectiveNode, isMutation: boolean = false) => { - const directiveArgs = getDirectiveArguments(directive); - const cmds: Expression[] = [set( - ref(ResourceConstants.SNIPPETS.ModelObjectKey), - modelObjectKey(directiveArgs, isMutation) - )]; - return printBlock(`Set the primary @key`)(compoundExpression(cmds)); + // 3. Check that fields exists and are valid key types. + const fieldMap = new Map(); + for (const field of definition.fields) { + fieldMap.set(field.name.value, field); } - - // When issuing an update mutation that changes one part of a composite sort key, - // you must supply the entire key so that the underlying composite key can be resaved - // in the update operation. We only need to update for composite sort keys on secondary indexes. - private validateKeyUpdateArgumentsSnippet = (directive: DirectiveNode): string => { - const directiveArgs: KeyArguments = getDirectiveArguments(directive); - if (!this.isPrimaryKey(directive) && directiveArgs.fields.length > 2) { - const sortKeyFields = directiveArgs.fields.slice(1); - return printBlock(`Validate update mutation for @key '${directiveArgs.name}'`)(compoundExpression([ - set(ref('hasSeenSomeKeyArg'), bool(false)), - set(ref('keyFieldNames'), list(sortKeyFields.map(f => str(f)))), - forEach(ref('keyFieldName'), ref('keyFieldNames'), [ - iff( - raw(`$ctx.args.input.containsKey("$keyFieldName")`), - set(ref('hasSeenSomeKeyArg'), bool(true)), - true - ) - ]), - forEach(ref('keyFieldName'), ref('keyFieldNames'), [ - iff( - raw(`$hasSeenSomeKeyArg && !$ctx.args.input.containsKey("$keyFieldName")`), - raw(`$util.error("When updating any part of the composite sort key for @key '${directiveArgs.name}',` + - ` you must provide all fields for the key. Missing key: '$keyFieldName'.")`) - ) - ]) - ])); + for (const fieldName of directiveArgs.fields) { + if (!fieldMap.has(fieldName)) { + const checkedKeyName = directiveArgs.name ? directiveArgs.name : ''; + throw new InvalidDirectiveError( + `You cannot specify a non-existant field '${fieldName}' in @key '${checkedKeyName}' on type '${definition.name.value}'.` + ); + } else { + const existingField = fieldMap.get(fieldName); + const ddbKeyType = attributeTypeFromType(existingField.type, ctx); + if (this.isPrimaryKey(directive) && !isNonNullType(existingField.type)) { + throw new InvalidDirectiveError(`The primary @key on type '${definition.name.value}' must reference non-null fields.`); + } else if (ddbKeyType !== 'S' && ddbKeyType !== 'N' && ddbKeyType !== 'B') { + throw new InvalidDirectiveError(`A @key on type '${definition.name.value}' cannot reference non-scalar field ${fieldName}.`); } - return ''; + } } - - /** - * Validates the directive usage is semantically valid. - * - * 1. There may only be 1 @key without a name (specifying the primary key) - * 2. There may only be 1 @key with a given name. - * 3. @key must only reference existing scalar fields that map to DynamoDB S, N, or B. - * 4. A primary key must not include a 'queryField'. - * 5. If there is no primary sort key, make sure there are no more LSIs. - * @param definition The object type definition node. - * @param directive The @key directive - * @param ctx The transformer context - */ - private validate = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const directiveArgs = getDirectiveArguments(directive); - if (!directiveArgs.name) { - // 1. Make sure there are no more directives without a name. - for (const otherDirective of definition.directives.filter(d => d.name.value === 'key')) { - const otherArgs = getDirectiveArguments(otherDirective); - if (otherDirective !== directive && !otherArgs.name) { - throw new InvalidDirectiveError(`You may only supply one primary @key on type '${definition.name.value}'.`); - } - // 5. If there is no primary sort key, make sure there are no more LSIs. - const hasPrimarySortKey = directiveArgs.fields.length > 1; - const primaryHashField = directiveArgs.fields[0]; - const otherHashField = otherArgs.fields[0]; - if ( - otherDirective !== directive && - !hasPrimarySortKey && - // If the primary key and other key share the first field and are not the same directive it is an LSI. - primaryHashField === otherHashField - ) { - throw new InvalidDirectiveError( - `Invalid @key "${otherArgs.name}". You may not create a @key where the first field in 'fields' ` + - `is the same as that of the primary @key unless the primary @key has multiple 'fields'. ` + - `You cannot have a local secondary index without a sort key in the primary index.` - ); - } - } - // 4. Make sure that a 'queryField' is not included on a primary @key. - if (directiveArgs.queryField) { - throw new InvalidDirectiveError(`You cannot pass 'queryField' to the primary @key on type '${definition.name.value}'.`); - } - } else { - // 2. Make sure there are no more directives with the same name. - for (const otherDirective of definition.directives.filter(d => d.name.value === 'key')) { - const otherArgs = getDirectiveArguments(otherDirective); - if (otherDirective !== directive && otherArgs.name === directiveArgs.name) { - throw new InvalidDirectiveError(`You may only supply one @key with the name '${directiveArgs.name}' on type '${definition.name.value}'.`); - } - } - } - // 3. Check that fields exists and are valid key types. - const fieldMap = new Map(); - for (const field of definition.fields) { - fieldMap.set(field.name.value, field); + }; + + /** + * Returns true if the directive specifies a primary key. + * @param directive The directive node. + */ + isPrimaryKey = (directive: DirectiveNode) => { + const directiveArgs = getDirectiveArguments(directive); + return !Boolean(directiveArgs.name); + }; + + /** + * Replace the primary key schema with one defined by a @key. + * @param definition The object type definition node. + * @param directive The @key directive + * @param ctx The transformer context + */ + replacePrimaryKey = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const args: KeyArguments = getDirectiveArguments(directive); + const ks = keySchema(args); + const attrDefs = attributeDefinitions(args, definition, ctx); + const tableLogicalID = ModelResourceIDs.ModelTableResourceID(definition.name.value); + const tableResource = ctx.getResource(tableLogicalID); + if (!tableResource) { + throw new InvalidDirectiveError(`The @key directive may only be added to object definitions annotated with @model.`); + } else { + // First remove any attribute definitions in the current primary key. + const existingAttrDefSet = new Set(tableResource.Properties.AttributeDefinitions.map(ad => ad.AttributeName)); + for (const existingKey of tableResource.Properties.KeySchema) { + if (existingAttrDefSet.has(existingKey.AttributeName)) { + tableResource.Properties.AttributeDefinitions = tableResource.Properties.AttributeDefinitions.filter( + ad => ad.AttributeName !== existingKey.AttributeName + ); + existingAttrDefSet.delete(existingKey.AttributeName); } - for (const fieldName of directiveArgs.fields) { - if (!fieldMap.has(fieldName)) { - const checkedKeyName = directiveArgs.name ? directiveArgs.name : ""; - throw new InvalidDirectiveError(`You cannot specify a non-existant field '${fieldName}' in @key '${checkedKeyName}' on type '${definition.name.value}'.`); - } else { - const existingField = fieldMap.get(fieldName); - const ddbKeyType = attributeTypeFromType(existingField.type, ctx); - if (this.isPrimaryKey(directive) && !isNonNullType(existingField.type)) { - throw new InvalidDirectiveError(`The primary @key on type '${definition.name.value}' must reference non-null fields.`); - } else if (ddbKeyType !== 'S' && ddbKeyType !== 'N' && ddbKeyType !== 'B') { - throw new InvalidDirectiveError(`A @key on type '${definition.name.value}' cannot reference non-scalar field ${fieldName}.`); - } - } + } + // Then replace the KeySchema and add any new attribute definitions back. + tableResource.Properties.KeySchema = ks; + for (const attr of attrDefs) { + if (!existingAttrDefSet.has(attr.AttributeName)) { + tableResource.Properties.AttributeDefinitions.push(attr); } + } } - - /** - * Returns true if the directive specifies a primary key. - * @param directive The directive node. - */ - isPrimaryKey = (directive: DirectiveNode) => { - const directiveArgs = getDirectiveArguments(directive); - return !Boolean(directiveArgs.name); - } - - /** - * Replace the primary key schema with one defined by a @key. - * @param definition The object type definition node. - * @param directive The @key directive - * @param ctx The transformer context - */ - replacePrimaryKey = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const args: KeyArguments = getDirectiveArguments(directive); - const ks = keySchema(args); - const attrDefs = attributeDefinitions(args, definition, ctx); - const tableLogicalID = ModelResourceIDs.ModelTableResourceID(definition.name.value); - const tableResource = ctx.getResource(tableLogicalID); - if (!tableResource) { - throw new InvalidDirectiveError(`The @key directive may only be added to object definitions annotated with @model.`); - } else { - // First remove any attribute definitions in the current primary key. - const existingAttrDefSet = new Set(tableResource.Properties.AttributeDefinitions.map(ad => ad.AttributeName)); - for (const existingKey of tableResource.Properties.KeySchema) { - if (existingAttrDefSet.has(existingKey.AttributeName)) { - tableResource.Properties.AttributeDefinitions = tableResource.Properties.AttributeDefinitions.filter(ad => ad.AttributeName !== existingKey.AttributeName); - existingAttrDefSet.delete(existingKey.AttributeName); - } - } - // Then replace the KeySchema and add any new attribute definitions back. - tableResource.Properties.KeySchema = ks; - for (const attr of attrDefs) { - if (!existingAttrDefSet.has(attr.AttributeName)) { - tableResource.Properties.AttributeDefinitions.push(attr); - } - } - } - } - - /** - * Add a LSI or GSI to the table as defined by a @key. - * @param definition The object type definition node. - * @param directive The @key directive - * @param ctx The transformer context - */ - appendSecondaryIndex = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { - const args: KeyArguments = getDirectiveArguments(directive); - const ks = keySchema(args); - const attrDefs = attributeDefinitions(args, definition, ctx); - const tableLogicalID = ModelResourceIDs.ModelTableResourceID(definition.name.value); - const tableResource = ctx.getResource(tableLogicalID); - const primaryKeyDirective = getPrimaryKey(definition); - const primaryPartitionKeyName = primaryKeyDirective ? getDirectiveArguments(primaryKeyDirective).fields[0] : 'id'; - if (!tableResource) { - throw new InvalidDirectiveError(`The @key directive may only be added to object definitions annotated with @model.`); - } else { - const baseIndexProperties = { - IndexName: args.name, - KeySchema: ks, - Projection: new Projection({ - ProjectionType: 'ALL' - }) - }; - if (primaryPartitionKeyName === ks[0].AttributeName) { - // This is an LSI. - // Add the new secondary index and update the table's attribute definitions. - tableResource.Properties.LocalSecondaryIndexes = append( - tableResource.Properties.LocalSecondaryIndexes, - new LocalSecondaryIndex(baseIndexProperties) - ) - } else { - // This is a GSI. - // Add the new secondary index and update the table's attribute definitions. - tableResource.Properties.GlobalSecondaryIndexes = append( - tableResource.Properties.GlobalSecondaryIndexes, - new GlobalSecondaryIndex({ - ...baseIndexProperties, - ProvisionedThroughput: Fn.If( - ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, - Refs.NoValue, - { - ReadCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS), - WriteCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS) - } - ) as any, - }) - ) - } - const existingAttrDefSet = new Set(tableResource.Properties.AttributeDefinitions.map(ad => ad.AttributeName)); - for (const attr of attrDefs) { - if (!existingAttrDefSet.has(attr.AttributeName)) { - tableResource.Properties.AttributeDefinitions.push(attr); - } - } + }; + + /** + * Add a LSI or GSI to the table as defined by a @key. + * @param definition The object type definition node. + * @param directive The @key directive + * @param ctx The transformer context + */ + appendSecondaryIndex = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) => { + const args: KeyArguments = getDirectiveArguments(directive); + const ks = keySchema(args); + const attrDefs = attributeDefinitions(args, definition, ctx); + const tableLogicalID = ModelResourceIDs.ModelTableResourceID(definition.name.value); + const tableResource = ctx.getResource(tableLogicalID); + const primaryKeyDirective = getPrimaryKey(definition); + const primaryPartitionKeyName = primaryKeyDirective ? getDirectiveArguments(primaryKeyDirective).fields[0] : 'id'; + if (!tableResource) { + throw new InvalidDirectiveError(`The @key directive may only be added to object definitions annotated with @model.`); + } else { + const baseIndexProperties = { + IndexName: args.name, + KeySchema: ks, + Projection: new Projection({ + ProjectionType: 'ALL', + }), + }; + if (primaryPartitionKeyName === ks[0].AttributeName) { + // This is an LSI. + // Add the new secondary index and update the table's attribute definitions. + tableResource.Properties.LocalSecondaryIndexes = append( + tableResource.Properties.LocalSecondaryIndexes, + new LocalSecondaryIndex(baseIndexProperties) + ); + } else { + // This is a GSI. + // Add the new secondary index and update the table's attribute definitions. + tableResource.Properties.GlobalSecondaryIndexes = append( + tableResource.Properties.GlobalSecondaryIndexes, + new GlobalSecondaryIndex({ + ...baseIndexProperties, + ProvisionedThroughput: Fn.If(ResourceConstants.CONDITIONS.ShouldUsePayPerRequestBilling, Refs.NoValue, { + ReadCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableReadIOPS), + WriteCapacityUnits: Fn.Ref(ResourceConstants.PARAMETERS.DynamoDBModelTableWriteIOPS), + }) as any, + }) + ); + } + const existingAttrDefSet = new Set(tableResource.Properties.AttributeDefinitions.map(ad => ad.AttributeName)); + for (const attr of attrDefs) { + if (!existingAttrDefSet.has(attr.AttributeName)) { + tableResource.Properties.AttributeDefinitions.push(attr); } + } } + }; } /** @@ -518,24 +564,21 @@ export default class KeyTransformer extends Transformer { * @param args The arguments of the @key directive. */ function keySchema(args: KeyArguments) { - if (args.fields.length > 1) { - const condensedSortKey = condenseRangeKey(args.fields.slice(1)); - return [ - { AttributeName: args.fields[0], KeyType: 'HASH' }, - { AttributeName: condensedSortKey, KeyType: 'RANGE' }, - ]; - } else { - return [{ AttributeName: args.fields[0], KeyType: 'HASH' }]; - } + if (args.fields.length > 1) { + const condensedSortKey = condenseRangeKey(args.fields.slice(1)); + return [{ AttributeName: args.fields[0], KeyType: 'HASH' }, { AttributeName: condensedSortKey, KeyType: 'RANGE' }]; + } else { + return [{ AttributeName: args.fields[0], KeyType: 'HASH' }]; + } } function attributeTypeFromType(type: TypeNode, ctx: TransformerContext) { - const baseTypeName = getBaseType(type); - const ofType = ctx.getType(baseTypeName); - if (ofType && ofType.kind === Kind.ENUM_TYPE_DEFINITION) { - return 'S'; - } - return attributeTypeFromScalar(type); + const baseTypeName = getBaseType(type); + const ofType = ctx.getType(baseTypeName); + if (ofType && ofType.kind === Kind.ENUM_TYPE_DEFINITION) { + return 'S'; + } + return attributeTypeFromScalar(type); } /** @@ -544,242 +587,254 @@ function attributeTypeFromType(type: TypeNode, ctx: TransformerContext) { * @param def The object type definition containing the @key. */ function attributeDefinitions(args: KeyArguments, def: ObjectTypeDefinitionNode, ctx: TransformerContext) { - const fieldMap = new Map(); - for (const field of def.fields) { - fieldMap.set(field.name.value, field); - } - if (args.fields.length > 2) { - const hashName = args.fields[0]; - const condensedSortKey = condenseRangeKey(args.fields.slice(1)); - return [ - { AttributeName: hashName, AttributeType: attributeTypeFromType(fieldMap.get(hashName).type, ctx) }, - { AttributeName: condensedSortKey, AttributeType: 'S' }, - ]; - } else if (args.fields.length === 2) { - const hashName = args.fields[0]; - const sortName = args.fields[1]; - return [ - { AttributeName: hashName, AttributeType: attributeTypeFromType(fieldMap.get(hashName).type, ctx) }, - { AttributeName: sortName, AttributeType: attributeTypeFromType(fieldMap.get(sortName).type, ctx) }, - ]; - } else { - const fieldName = args.fields[0]; - return [{ AttributeName: fieldName, AttributeType: attributeTypeFromType(fieldMap.get(fieldName).type, ctx) }]; - } + const fieldMap = new Map(); + for (const field of def.fields) { + fieldMap.set(field.name.value, field); + } + if (args.fields.length > 2) { + const hashName = args.fields[0]; + const condensedSortKey = condenseRangeKey(args.fields.slice(1)); + return [ + { AttributeName: hashName, AttributeType: attributeTypeFromType(fieldMap.get(hashName).type, ctx) }, + { AttributeName: condensedSortKey, AttributeType: 'S' }, + ]; + } else if (args.fields.length === 2) { + const hashName = args.fields[0]; + const sortName = args.fields[1]; + return [ + { AttributeName: hashName, AttributeType: attributeTypeFromType(fieldMap.get(hashName).type, ctx) }, + { AttributeName: sortName, AttributeType: attributeTypeFromType(fieldMap.get(sortName).type, ctx) }, + ]; + } else { + const fieldName = args.fields[0]; + return [{ AttributeName: fieldName, AttributeType: attributeTypeFromType(fieldMap.get(fieldName).type, ctx) }]; + } } function append(maybeList: T[] | undefined, item: T) { - if (maybeList) { - return [...maybeList, item]; - } - return [item]; + if (maybeList) { + return [...maybeList, item]; + } + return [item]; } function getPrimaryKey(obj: ObjectTypeDefinitionNode): DirectiveNode | undefined { - for (const directive of obj.directives) { - if (directive.name.value === 'key' && !getDirectiveArguments(directive).name) { - return directive; - } + for (const directive of obj.directives) { + if (directive.name.value === 'key' && !getDirectiveArguments(directive).name) { + return directive; } + } } function primaryIdFields(definition: ObjectTypeDefinitionNode, keyFields: string[]): InputValueDefinitionNode[] { - return keyFields.map(keyFieldName => { - const keyField: FieldDefinitionNode = definition.fields.find(field => field.name.value === keyFieldName); - return makeInputValueDefinition(keyFieldName, makeNonNullType(makeNamedType(getBaseType(keyField.type)))); - }) + return keyFields.map(keyFieldName => { + const keyField: FieldDefinitionNode = definition.fields.find(field => field.name.value === keyFieldName); + return makeInputValueDefinition(keyFieldName, makeNonNullType(makeNamedType(getBaseType(keyField.type)))); + }); } // Key fields are non-nullable, non-key fields follow what their @model declaration makes. -function replaceCreateInput(definition: ObjectTypeDefinitionNode, input: InputObjectTypeDefinitionNode, keyFields: string[]): InputObjectTypeDefinitionNode { - return { - ...input, - fields: input.fields.reduce((acc, f) => { - // If the field is a key, make it non-null. - if (keyFields.find(k => k === f.name.value)) { - return [...acc, makeInputValueDefinition(f.name.value, makeNonNullType(makeNamedType(getBaseType(f.type))))]; - } - return [...acc, f]; - }, []) - }; -}; +function replaceCreateInput( + definition: ObjectTypeDefinitionNode, + input: InputObjectTypeDefinitionNode, + keyFields: string[] +): InputObjectTypeDefinitionNode { + return { + ...input, + fields: input.fields.reduce((acc, f) => { + // If the field is a key, make it non-null. + if (keyFields.find(k => k === f.name.value)) { + return [...acc, makeInputValueDefinition(f.name.value, makeNonNullType(makeNamedType(getBaseType(f.type))))]; + } + return [...acc, f]; + }, []), + }; +} // Key fields are non-nullable, non-key fields are not non-nullable. -function replaceUpdateInput(definition: ObjectTypeDefinitionNode, input: InputObjectTypeDefinitionNode, keyFields: string[]): InputObjectTypeDefinitionNode { - return { - ...input, - fields: input.fields.map( - f => { - if (keyFields.find(k => k === f.name.value)) { - return makeInputValueDefinition(f.name.value, wrapNonNull(withNamedNodeNamed(f.type, getBaseType(f.type)))); - } else { - return f; - } - } - ) - }; -}; +function replaceUpdateInput( + definition: ObjectTypeDefinitionNode, + input: InputObjectTypeDefinitionNode, + keyFields: string[] +): InputObjectTypeDefinitionNode { + return { + ...input, + fields: input.fields.map(f => { + if (keyFields.find(k => k === f.name.value)) { + return makeInputValueDefinition(f.name.value, wrapNonNull(withNamedNodeNamed(f.type, getBaseType(f.type)))); + } else { + return f; + } + }), + }; +} // Key fields are non-nullable, non-key fields are not non-nullable. -function replaceDeleteInput(definition: ObjectTypeDefinitionNode, input: InputObjectTypeDefinitionNode, keyFields: string[]): InputObjectTypeDefinitionNode { - return { - ...input, - fields: primaryIdFields(definition, keyFields) - }; -}; +function replaceDeleteInput( + definition: ObjectTypeDefinitionNode, + input: InputObjectTypeDefinitionNode, + keyFields: string[] +): InputObjectTypeDefinitionNode { + return { + ...input, + fields: primaryIdFields(definition, keyFields), + }; +} /** * Return a VTL object containing the compressed key information. * @param args The arguments of the @key directive. */ function modelObjectKey(args: KeyArguments, isMutation: boolean) { - const argsPrefix = isMutation ? - 'ctx.args.input' : - 'ctx.args'; - if (args.fields.length > 2) { - const rangeKeyFields = args.fields.slice(1); - const condensedSortKey = condenseRangeKey(rangeKeyFields); - const condensedSortKeyValue = condenseRangeKey( - rangeKeyFields.map(keyField => `\${${argsPrefix}.${keyField}}`) - ); - return obj({ - [args.fields[0]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[0]})`), - [condensedSortKey]: ref(`util.dynamodb.toDynamoDB("${condensedSortKeyValue}")`) - }); - } else if (args.fields.length === 2) { - return obj({ - [args.fields[0]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[0]})`), - [args.fields[1]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[1]})`) - }); - } else if (args.fields.length === 1) { - return obj({ - [args.fields[0]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[0]})`), - }); - } - throw new InvalidDirectiveError('@key directives must include at least one field.'); + const argsPrefix = isMutation ? 'ctx.args.input' : 'ctx.args'; + if (args.fields.length > 2) { + const rangeKeyFields = args.fields.slice(1); + const condensedSortKey = condenseRangeKey(rangeKeyFields); + const condensedSortKeyValue = condenseRangeKey(rangeKeyFields.map(keyField => `\${${argsPrefix}.${keyField}}`)); + return obj({ + [args.fields[0]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[0]})`), + [condensedSortKey]: ref(`util.dynamodb.toDynamoDB("${condensedSortKeyValue}")`), + }); + } else if (args.fields.length === 2) { + return obj({ + [args.fields[0]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[0]})`), + [args.fields[1]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[1]})`), + }); + } else if (args.fields.length === 1) { + return obj({ + [args.fields[0]]: ref(`util.dynamodb.toDynamoDB($${argsPrefix}.${args.fields[0]})`), + }); + } + throw new InvalidDirectiveError('@key directives must include at least one field.'); } function ensureCompositeKeySnippet(dir: DirectiveNode): string { - const args: KeyArguments = getDirectiveArguments(dir); - const argsPrefix = 'ctx.args.input'; - if (args.fields.length > 2) { - const rangeKeyFields = args.fields.slice(1); - const condensedSortKey = condenseRangeKey(rangeKeyFields); - const dynamoDBFriendlySortKeyName = toCamelCase(rangeKeyFields.map(f => graphqlName(f))); - const condensedSortKeyValue = condenseRangeKey( - rangeKeyFields.map(keyField => `\${${argsPrefix}.${keyField}}`) - ); - return print(compoundExpression([ - ifElse( - raw(`$util.isNull($${ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap})`), - set(ref(ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap), obj({ - [condensedSortKey]: str(dynamoDBFriendlySortKeyName) - })), - qref(`$${ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap}.put("${condensedSortKey}", "${dynamoDBFriendlySortKeyName}")`) - ), - qref(`$ctx.args.input.put("${condensedSortKey}","${condensedSortKeyValue}")`) - ])); - } - return ''; + const args: KeyArguments = getDirectiveArguments(dir); + const argsPrefix = 'ctx.args.input'; + if (args.fields.length > 2) { + const rangeKeyFields = args.fields.slice(1); + const condensedSortKey = condenseRangeKey(rangeKeyFields); + const dynamoDBFriendlySortKeyName = toCamelCase(rangeKeyFields.map(f => graphqlName(f))); + const condensedSortKeyValue = condenseRangeKey(rangeKeyFields.map(keyField => `\${${argsPrefix}.${keyField}}`)); + return print( + compoundExpression([ + ifElse( + raw(`$util.isNull($${ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap})`), + set( + ref(ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap), + obj({ + [condensedSortKey]: str(dynamoDBFriendlySortKeyName), + }) + ), + qref(`$${ResourceConstants.SNIPPETS.DynamoDBNameOverrideMap}.put("${condensedSortKey}", "${dynamoDBFriendlySortKeyName}")`) + ), + qref(`$ctx.args.input.put("${condensedSortKey}","${condensedSortKeyValue}")`), + ]) + ); + } + return ''; } function condenseRangeKey(fields: string[]) { - return fields.join(ModelResourceIDs.ModelCompositeKeySeparator()); + return fields.join(ModelResourceIDs.ModelCompositeKeySeparator()); } function makeQueryResolver(definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) { - const type = definition.name.value; - const directiveArgs: KeyArguments = getDirectiveArguments(directive); - const index = directiveArgs.name; - const fieldName = directiveArgs.queryField; - const queryTypeName = ctx.getQueryTypeName(); - const defaultPageLimit = 10 - const requestVariable = 'QueryRequest'; - return new AppSync.Resolver({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), - FieldName: fieldName, - TypeName: queryTypeName, - RequestMappingTemplate: print( - compoundExpression([ - setQuerySnippet(definition, directive, ctx), - set(ref('limit'), - ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), - set( - ref(requestVariable), - obj({ - version: str('2017-02-28'), - operation: str('Query'), - limit: ref('limit'), - query: ref(ResourceConstants.SNIPPETS.ModelQueryExpression), - index: str(index) - }) - ), - ifElse( - raw(`!$util.isNull($ctx.args.sortDirection) + const type = definition.name.value; + const directiveArgs: KeyArguments = getDirectiveArguments(directive); + const index = directiveArgs.name; + const fieldName = directiveArgs.queryField; + const queryTypeName = ctx.getQueryTypeName(); + const defaultPageLimit = 10; + const requestVariable = 'QueryRequest'; + return new AppSync.Resolver({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DataSourceName: Fn.GetAtt(ModelResourceIDs.ModelTableDataSourceID(type), 'Name'), + FieldName: fieldName, + TypeName: queryTypeName, + RequestMappingTemplate: print( + compoundExpression([ + setQuerySnippet(definition, directive, ctx), + set(ref('limit'), ref(`util.defaultIfNull($context.args.limit, ${defaultPageLimit})`)), + set( + ref(requestVariable), + obj({ + version: str('2017-02-28'), + operation: str('Query'), + limit: ref('limit'), + query: ref(ResourceConstants.SNIPPETS.ModelQueryExpression), + index: str(index), + }) + ), + ifElse( + raw(`!$util.isNull($ctx.args.sortDirection) && $ctx.args.sortDirection == "DESC"`), - set(ref(`${requestVariable}.scanIndexForward`), bool(false)), - set(ref(`${requestVariable}.scanIndexForward`), bool(true)), - ), - iff( - ref('context.args.nextToken'), - set( - ref(`${requestVariable}.nextToken`), - str('$context.args.nextToken') - ), - true - ), - iff( - ref('context.args.filter'), - set( - ref(`${requestVariable}.filter`), - ref('util.parseJson("$util.transform.toDynamoDBFilterExpression($ctx.args.filter)")') - ), - true - ), - raw(`$util.toJson($${requestVariable})`) - ]) + set(ref(`${requestVariable}.scanIndexForward`), bool(false)), + set(ref(`${requestVariable}.scanIndexForward`), bool(true)) ), - ResponseMappingTemplate: print( - raw('$util.toJson($ctx.result)') - ) - }) + iff(ref('context.args.nextToken'), set(ref(`${requestVariable}.nextToken`), str('$context.args.nextToken')), true), + iff( + ref('context.args.filter'), + set(ref(`${requestVariable}.filter`), ref('util.parseJson("$util.transform.toDynamoDBFilterExpression($ctx.args.filter)")')), + true + ), + raw(`$util.toJson($${requestVariable})`), + ]) + ), + ResponseMappingTemplate: print(raw('$util.toJson($ctx.result)')), + }); } function setQuerySnippet(definition: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext) { - const args: KeyArguments = getDirectiveArguments(directive); - const keys = args.fields; - const keyTypes = keys.map(k => { - const field = definition.fields.find(f => f.name.value === k); - return attributeTypeFromType(field.type, ctx); - }) - return block(`Set query expression for @key`, [ - set(ref(ResourceConstants.SNIPPETS.ModelQueryExpression), obj({})), - applyKeyExpressionForCompositeKey(keys, keyTypes, ResourceConstants.SNIPPETS.ModelQueryExpression) - ]) + const args: KeyArguments = getDirectiveArguments(directive); + const keys = args.fields; + const keyTypes = keys.map(k => { + const field = definition.fields.find(f => f.name.value === k); + return attributeTypeFromType(field.type, ctx); + }); + return block(`Set query expression for @key`, [ + set(ref(ResourceConstants.SNIPPETS.ModelQueryExpression), obj({})), + applyKeyExpressionForCompositeKey(keys, keyTypes, ResourceConstants.SNIPPETS.ModelQueryExpression), + ]); } -function addHashField(definition: ObjectTypeDefinitionNode, args: KeyArguments, elems: InputValueDefinitionNode[]): InputValueDefinitionNode[] { - let hashFieldName = args.fields[0]; - const hashField = definition.fields.find(field => field.name.value === hashFieldName); - const hashKey = makeInputValueDefinition(hashFieldName, makeNamedType(getBaseType(hashField.type))); - return [hashKey, ...elems]; +function addHashField( + definition: ObjectTypeDefinitionNode, + args: KeyArguments, + elems: InputValueDefinitionNode[] +): InputValueDefinitionNode[] { + let hashFieldName = args.fields[0]; + const hashField = definition.fields.find(field => field.name.value === hashFieldName); + const hashKey = makeInputValueDefinition(hashFieldName, makeNamedType(getBaseType(hashField.type))); + return [hashKey, ...elems]; } -function addSimpleSortKey(ctx: TransformerContext, definition: ObjectTypeDefinitionNode, args: KeyArguments, elems: InputValueDefinitionNode[]): InputValueDefinitionNode[] { - let sortKeyName = args.fields[1]; - const sortField = definition.fields.find(field => field.name.value === sortKeyName); - const baseType = getBaseType(sortField.type); - const resolvedTypeIfEnum = ctx.getType(baseType) as EnumTypeDefinitionNode ? 'String' : undefined; - const resolvedType = resolvedTypeIfEnum ? resolvedTypeIfEnum : baseType; - const hashKey = makeInputValueDefinition(sortKeyName, makeNamedType(ModelResourceIDs.ModelKeyConditionInputTypeName(resolvedType))); - return [hashKey, ...elems]; +function addSimpleSortKey( + ctx: TransformerContext, + definition: ObjectTypeDefinitionNode, + args: KeyArguments, + elems: InputValueDefinitionNode[] +): InputValueDefinitionNode[] { + let sortKeyName = args.fields[1]; + const sortField = definition.fields.find(field => field.name.value === sortKeyName); + const baseType = getBaseType(sortField.type); + const resolvedTypeIfEnum = (ctx.getType(baseType) as EnumTypeDefinitionNode) ? 'String' : undefined; + const resolvedType = resolvedTypeIfEnum ? resolvedTypeIfEnum : baseType; + const hashKey = makeInputValueDefinition(sortKeyName, makeNamedType(ModelResourceIDs.ModelKeyConditionInputTypeName(resolvedType))); + return [hashKey, ...elems]; } -function addCompositeSortKey(definition: ObjectTypeDefinitionNode, args: KeyArguments, elems: InputValueDefinitionNode[]): InputValueDefinitionNode[] { - let sortKeyNames = args.fields.slice(1); - const compositeSortKeyName = toCamelCase(sortKeyNames); - const hashKey = makeInputValueDefinition(compositeSortKeyName, makeNamedType(ModelResourceIDs.ModelCompositeKeyConditionInputTypeName(definition.name.value, toUpper(args.name || 'Primary')))); - return [hashKey, ...elems]; +function addCompositeSortKey( + definition: ObjectTypeDefinitionNode, + args: KeyArguments, + elems: InputValueDefinitionNode[] +): InputValueDefinitionNode[] { + let sortKeyNames = args.fields.slice(1); + const compositeSortKeyName = toCamelCase(sortKeyNames); + const hashKey = makeInputValueDefinition( + compositeSortKeyName, + makeNamedType(ModelResourceIDs.ModelCompositeKeyConditionInputTypeName(definition.name.value, toUpper(args.name || 'Primary'))) + ); + return [hashKey, ...elems]; } function joinSnippets(lines: string[]): string { - return lines.join('\n'); + return lines.join('\n'); } diff --git a/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts b/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts index 3fcefb03e0..6c06df6a4e 100644 --- a/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts +++ b/packages/graphql-key-transformer/src/__tests__/KeyTransformer.test.ts @@ -1,104 +1,91 @@ -import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core' -import KeyTransformer from '../KeyTransformer' +import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core'; +import KeyTransformer from '../KeyTransformer'; test('KeyTransformer should fail if more than 1 @key is provided without a name.', () => { - const validSchema = ` + const validSchema = ` type Test @key(fields: ["id"]) @key(fields: ["email"]) { id: ID! email: String } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new KeyTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new KeyTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); -}) + expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); +}); test('KeyTransformer should fail if more than 1 @key is provided with the same name.', () => { - const validSchema = ` + const validSchema = ` type Test @key(name: "Test", fields: ["id"]) @key(name: "Test", fields: ["email"]) { id: ID! email: String } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new KeyTransformer() - ] - }) - - expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); -}) + const transformer = new GraphQLTransform({ + transformers: [new KeyTransformer()], + }); + expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); +}); test('KeyTransformer should fail if referencing a field that does not exist.', () => { - const validSchema = ` + const validSchema = ` type Test @key(fields: ["someWeirdId"]) { id: ID! email: String } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new KeyTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new KeyTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); -}) + expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); +}); test('Test that a primary @key fails if pointing to nullable fields.', () => { - const validSchema = ` + const validSchema = ` type Test @key(fields: ["email"]) { id: ID! email: String } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new KeyTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new KeyTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); -}) + expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); +}); test('Test that model with an LSI but no primary sort key will fail.', () => { - const validSchema = ` + const validSchema = ` type Test @key(fields: ["id"]) @key(name: "SomeLSI", fields: ["id", "email"]) { id: ID! email: String! } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new KeyTransformer() - ] - }) - expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); -}) + const transformer = new GraphQLTransform({ + transformers: [new KeyTransformer()], + }); + expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); +}); test('KeyTransformer should fail if a non-existing type field is defined as key field.', () => { - const validSchema = ` + const validSchema = ` type Test @key(name: "Test", fields: ["one"]) { id: ID! email: String } - ` + `; - const transformer = new GraphQLTransform({ - transformers: [ - new KeyTransformer() - ] - }) + const transformer = new GraphQLTransform({ + transformers: [new KeyTransformer()], + }); - expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); -}) + expect(() => transformer.transform(validSchema)).toThrowError(InvalidDirectiveError); +}); diff --git a/packages/graphql-key-transformer/src/index.ts b/packages/graphql-key-transformer/src/index.ts index 732333baf0..873662a5f0 100644 --- a/packages/graphql-key-transformer/src/index.ts +++ b/packages/graphql-key-transformer/src/index.ts @@ -1,2 +1,2 @@ import KeyTransformer from './KeyTransformer'; -export default KeyTransformer; \ No newline at end of file +export default KeyTransformer; diff --git a/packages/graphql-mapping-template/src/__tests__/ast.test.ts b/packages/graphql-mapping-template/src/__tests__/ast.test.ts index 76d44d4b59..d5da70b63f 100644 --- a/packages/graphql-mapping-template/src/__tests__/ast.test.ts +++ b/packages/graphql-mapping-template/src/__tests__/ast.test.ts @@ -1,57 +1,47 @@ -import { ref, obj, str, forEach, qref, set, compoundExpression, ifElse, nul, bool } from '../ast' -import { DynamoDBMappingTemplate } from '../dynamodb' -import { print } from '../print' +import { ref, obj, str, forEach, qref, set, compoundExpression, ifElse, nul, bool } from '../ast'; +import { DynamoDBMappingTemplate } from '../dynamodb'; +import { print } from '../print'; test('create a put item resolver with the ast', () => { - const resolver = DynamoDBMappingTemplate.putItem({ - key: obj({ - type: str('Post'), - id: ref('util.autoId()') - }), - attributeValues: obj({ - value: ref(`util.dynamodb.toMapJson(\${ctx.input})`) - }) - }) - const template = print(resolver) - expect(template).toBeDefined() + const resolver = DynamoDBMappingTemplate.putItem({ + key: obj({ + type: str('Post'), + id: ref('util.autoId()'), + }), + attributeValues: obj({ + value: ref(`util.dynamodb.toMapJson(\${ctx.input})`), + }), + }); + const template = print(resolver); + expect(template).toBeDefined(); }); test('create a query resolver with the ast', () => { - const resolver = DynamoDBMappingTemplate.query({ - query: obj({ - 'expression' : str('#typename = :typename'), - 'expressionNames' : obj({ - '#typename' : str('__typename') - }), - 'expressionValues' : obj({ - ':typename' : obj({ - 'S' : str('test') - }) - }) + const resolver = DynamoDBMappingTemplate.query({ + query: obj({ + expression: str('#typename = :typename'), + expressionNames: obj({ + '#typename': str('__typename'), + }), + expressionValues: obj({ + ':typename': obj({ + S: str('test'), }), - scanIndexForward: bool(true), - filter: ifElse( - ref('context.args.filter'), - ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), - nul() - ), - limit: ref('limit'), - nextToken: ifElse( - ref('context.args.nextToken'), - str('$context.args.nextToken'), - nul() - ) - }) - const template = print(resolver) - expect(template).toBeDefined() + }), + }), + scanIndexForward: bool(true), + filter: ifElse(ref('context.args.filter'), ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), nul()), + limit: ref('limit'), + nextToken: ifElse(ref('context.args.nextToken'), str('$context.args.nextToken'), nul()), + }); + const template = print(resolver); + expect(template).toBeDefined(); }); test('create a response mapping template that merges a nested object', () => { - const setResult = set(ref('result'), ref('util.map.copyAndRemoveAllKeys($context.result, ["value"])')) - const mergeLoop = forEach(ref('entry'), ref('context.result.value.entrySet()'), [ - qref('$result.put($entry.key, $entry.value)') - ]); - const returnStatement = ref('util.toJson($result)') - const template = print(compoundExpression([setResult, mergeLoop, returnStatement])) - expect(template).toBeDefined() -}) \ No newline at end of file + const setResult = set(ref('result'), ref('util.map.copyAndRemoveAllKeys($context.result, ["value"])')); + const mergeLoop = forEach(ref('entry'), ref('context.result.value.entrySet()'), [qref('$result.put($entry.key, $entry.value)')]); + const returnStatement = ref('util.toJson($result)'); + const template = print(compoundExpression([setResult, mergeLoop, returnStatement])); + expect(template).toBeDefined(); +}); diff --git a/packages/graphql-mapping-template/src/ast.ts b/packages/graphql-mapping-template/src/ast.ts index dd898c14e9..c5098862ca 100644 --- a/packages/graphql-mapping-template/src/ast.ts +++ b/packages/graphql-mapping-template/src/ast.ts @@ -2,240 +2,240 @@ * An if expression that takes a predicate and expression. */ export interface IfNode { - kind: 'If'; - predicate: Expression; - expr: Expression; - inline: boolean; + kind: 'If'; + predicate: Expression; + expr: Expression; + inline: boolean; } export function iff(predicate: Expression, expr: Expression, inline?: boolean): IfNode { - return { - kind: 'If', - predicate, - expr, - inline, - } + return { + kind: 'If', + predicate, + expr, + inline, + }; } /** * An if else expression that takes a predicate, if expr, and else expr. */ export interface IfElseNode { - kind: 'IfElse'; - predicate: Expression; - ifExpr: Expression; - elseExpr: Expression; - inline: boolean; + kind: 'IfElse'; + predicate: Expression; + ifExpr: Expression; + elseExpr: Expression; + inline: boolean; } export function ifElse(predicate: Expression, ifExpr: Expression, elseExpr: Expression, inline?: boolean): IfElseNode { - return { - kind: 'IfElse', - predicate, - ifExpr, - elseExpr, - inline - } + return { + kind: 'IfElse', + predicate, + ifExpr, + elseExpr, + inline, + }; } /** * An and expression that takes two or more expressions and joins them with &&. */ export interface AndNode { - kind: 'And'; - expressions: Expression[]; + kind: 'And'; + expressions: Expression[]; } export function and(expressions: Expression[]): AndNode { - return { - kind: 'And', - expressions - } + return { + kind: 'And', + expressions, + }; } /** * An or expression that takes two or more expressions and join them with || */ export interface OrNode { - kind: 'Or'; - expressions: Expression[]; + kind: 'Or'; + expressions: Expression[]; } export function or(expressions: Expression[]): OrNode { - return { - kind: 'Or', - expressions - } + return { + kind: 'Or', + expressions, + }; } /**Node * WrapsNodeNode an expression in (...) for order of operations. */ export interface ParensNode { - kind: 'Parens'; - expr: Expression; + kind: 'Parens'; + expr: Expression; } export function parens(expr: Expression): ParensNode { - return { - kind: 'Parens', - expr - } + return { + kind: 'Parens', + expr, + }; } /** * Compares two expressions for equality. */ export interface EqualsNode { - kind: 'Equals'; - leftExpr: Expression; - rightExpr: Expression; + kind: 'Equals'; + leftExpr: Expression; + rightExpr: Expression; } export function equals(leftExpr: Expression, rightExpr: Expression): EqualsNode { - return { - kind: 'Equals', - leftExpr, - rightExpr - } + return { + kind: 'Equals', + leftExpr, + rightExpr, + }; } /** * Compares two expressions for unequality. */ export interface NotEqualsNode { - kind: 'NotEquals'; - leftExpr: Expression; - rightExpr: Expression; + kind: 'NotEquals'; + leftExpr: Expression; + rightExpr: Expression; } export function notEquals(leftExpr: Expression, rightExpr: Expression): NotEqualsNode { - return { - kind: 'NotEquals', - leftExpr, - rightExpr - } + return { + kind: 'NotEquals', + leftExpr, + rightExpr, + }; } /** * Compares two expressions for unequality. */ export interface NotNode { - kind: 'Not'; - expr: Expression; + kind: 'Not'; + expr: Expression; } export function not(expr: Expression): NotNode { - return { - kind: 'Not', - expr, - } + return { + kind: 'Not', + expr, + }; } /** * Iterates through a collection. */ export interface ForEachNode { - kind: 'ForEach'; - key: ReferenceNode; - collection: ReferenceNode; - expressions: Expression[]; + kind: 'ForEach'; + key: ReferenceNode; + collection: ReferenceNode; + expressions: Expression[]; } export function forEach(key: ReferenceNode, collection: ReferenceNode, expressions: Expression[]): ForEachNode { - return { - kind: 'ForEach', - key, - collection, - expressions - } + return { + kind: 'ForEach', + key, + collection, + expressions, + }; } /** * A literal string that should be printed in the template with quotes. */ export interface StringNode { - kind: 'String'; - value: string; + kind: 'String'; + value: string; } export function str(value: string): StringNode { - return { - kind: 'String', - value - } + return { + kind: 'String', + value, + }; } /** * A literal string that should be printed in the template without quotes. */ export interface RawNode { - kind: 'Raw'; - value: string; + kind: 'Raw'; + value: string; } export function raw(value: string): RawNode { - return { - kind: 'Raw', - value - } + return { + kind: 'Raw', + value, + }; } /** * Wraps an expression in quotes. */ export interface QuotesNode { - kind: 'Quotes'; - expr: Expression; + kind: 'Quotes'; + expr: Expression; } export function quotes(expr: Expression): QuotesNode { - return { - kind: 'Quotes', - expr - } + return { + kind: 'Quotes', + expr, + }; } /** * A literal float that should be printed in the template. */ export interface FloatNode { - kind: 'Float'; - value: number; + kind: 'Float'; + value: number; } export function float(value: number): FloatNode { - return { - kind: 'Float', - value - } + return { + kind: 'Float', + value, + }; } /** * A literal int that should be printed in the template. */ export interface IntNode { - kind: 'Int'; - value: number; + kind: 'Int'; + value: number; } export function int(value: number): IntNode { - return { - kind: 'Int', - value - } + return { + kind: 'Int', + value, + }; } /** * A literal boolean that should be printed in the template. */ export interface BooleanNode { - kind: 'Boolean'; - value: boolean; + kind: 'Boolean'; + value: boolean; } export function bool(value: boolean): BooleanNode { - return { - kind: 'Boolean', - value - } + return { + kind: 'Boolean', + value, + }; } /** * A literal null to be printed in the template. */ export interface NullNode { - kind: 'Null'; + kind: 'Null'; } export function nul(): NullNode { - return { - kind: 'Null' - } + return { + kind: 'Null', + }; } /** @@ -243,14 +243,14 @@ export function nul(): NullNode { * VTL replaces placeholders with values from the context. */ export interface ReferenceNode { - kind: 'Reference'; - value: string; + kind: 'Reference'; + value: string; } export function ref(value: string): ReferenceNode { - return { - kind: 'Reference', - value - } + return { + kind: 'Reference', + value, + }; } /** @@ -258,142 +258,134 @@ export function ref(value: string): ReferenceNode { * VTL replaces placeholders with values from the context. */ export interface QuietReferenceNode { - kind: 'QuietReference'; - value: string; + kind: 'QuietReference'; + value: string; } export function qref(value: string): QuietReferenceNode { - return { - kind: 'QuietReference', - value - } + return { + kind: 'QuietReference', + value, + }; } /** * A JSON object serialized directly to the VTL. */ export interface ObjectNode { - kind: 'Object'; - attributes: [string, Expression][]; + kind: 'Object'; + attributes: [string, Expression][]; } // TODO: This can also take a plain object. What is easier in practice? -export function obj( - o: { [key: string]: Expression } -): ObjectNode { - const attributes = Object.keys(o).map( - (key: string) => ([key, o[key]] as [string, Expression]) - ) - return { - kind: 'Object', - attributes - } +export function obj(o: { [key: string]: Expression }): ObjectNode { + const attributes = Object.keys(o).map((key: string) => [key, o[key]] as [string, Expression]); + return { + kind: 'Object', + attributes, + }; } /** * A JSON object serialized directly to the VTL. */ export interface ListNode { - kind: 'List'; - expressions: Expression[]; + kind: 'List'; + expressions: Expression[]; } export function list(expressions: Expression[]): ListNode { - return { - kind: 'List', - expressions - } + return { + kind: 'List', + expressions, + }; } /** * Set a value in the mapping template. */ export interface SetNode { - kind: 'Set', - key: ReferenceNode, - value: Expression + kind: 'Set'; + key: ReferenceNode; + value: Expression; } export function set(key: ReferenceNode, value: Expression): SetNode { - return { - kind: 'Set', - key, - value - } + return { + kind: 'Set', + key, + value, + }; } export interface CommentNode { - kind: 'Comment', - text: string + kind: 'Comment'; + text: string; } export function comment(text: string): CommentNode { - return { - kind: 'Comment', - text - } + return { + kind: 'Comment', + text, + }; } export interface CompoundExpressionNode { - kind: 'CompoundExpression', - expressions: Expression[] + kind: 'CompoundExpression'; + expressions: Expression[]; } export function compoundExpression(expressions: Expression[]): CompoundExpressionNode { - return { - kind: 'CompoundExpression', - expressions - } + return { + kind: 'CompoundExpression', + expressions, + }; } export type ToJsonNode = { - kind: 'Util.ToJson', - expr: Expression -} + kind: 'Util.ToJson'; + expr: Expression; +}; export function toJson(expr: Expression): ToJsonNode { - return { - kind: 'Util.ToJson', - expr - } + return { + kind: 'Util.ToJson', + expr, + }; } export type NewLineNode = { - kind: 'NewLine' -} + kind: 'NewLine'; +}; export function newline(): NewLineNode { - return { - kind: 'NewLine', - } + return { + kind: 'NewLine', + }; } export function block(name: string, exprs: Expression[]): CompoundExpressionNode { - return compoundExpression([ - comment(`[Start] ${name}`), - ...exprs, - comment(`[End] ${name}`) - ]) + return compoundExpression([comment(`[Start] ${name}`), ...exprs, comment(`[End] ${name}`)]); } /** * A flow expression is one that dictates program flow e.g. if, ifelse, for, while, etc. */ export type Expression = - IfNode - | IfElseNode - | AndNode - | OrNode - | ParensNode - | EqualsNode - | NotEqualsNode - | ForEachNode - | StringNode - | RawNode - | QuotesNode - | FloatNode - | IntNode - | BooleanNode - | NullNode - | ReferenceNode - | QuietReferenceNode - | ObjectNode - | ListNode - | SetNode - | CommentNode - | CompoundExpressionNode - | ToJsonNode - | NotNode - | NewLineNode; + | IfNode + | IfElseNode + | AndNode + | OrNode + | ParensNode + | EqualsNode + | NotEqualsNode + | ForEachNode + | StringNode + | RawNode + | QuotesNode + | FloatNode + | IntNode + | BooleanNode + | NullNode + | ReferenceNode + | QuietReferenceNode + | ObjectNode + | ListNode + | SetNode + | CommentNode + | CompoundExpressionNode + | ToJsonNode + | NotNode + | NewLineNode; diff --git a/packages/graphql-mapping-template/src/dynamodb.ts b/packages/graphql-mapping-template/src/dynamodb.ts index 94b3bec1e1..2022aafe22 100644 --- a/packages/graphql-mapping-template/src/dynamodb.ts +++ b/packages/graphql-mapping-template/src/dynamodb.ts @@ -1,226 +1,246 @@ import { - obj, ref, Expression, ReferenceNode, StringNode, - IntNode, FloatNode, str, ObjectNode, compoundExpression, - set, list, forEach, ifElse, qref, iff, raw, - CompoundExpressionNode + obj, + ref, + Expression, + ReferenceNode, + StringNode, + IntNode, + FloatNode, + str, + ObjectNode, + compoundExpression, + set, + list, + forEach, + ifElse, + qref, + iff, + raw, + CompoundExpressionNode, } from './ast'; export class DynamoDBMappingTemplate { - /** - * Create a put item resolver template. - * @param keys A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) - */ - public static putItem({ key, attributeValues, condition }: { - key: ObjectNode | Expression, - attributeValues: Expression, - condition?: ObjectNode - }): ObjectNode { - return obj({ - version: str('2017-02-28'), - operation: str('PutItem'), - key, - attributeValues, - condition - }) - } + /** + * Create a put item resolver template. + * @param keys A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) + */ + public static putItem({ + key, + attributeValues, + condition, + }: { + key: ObjectNode | Expression; + attributeValues: Expression; + condition?: ObjectNode; + }): ObjectNode { + return obj({ + version: str('2017-02-28'), + operation: str('PutItem'), + key, + attributeValues, + condition, + }); + } - /** - * Create a get item resolver template. - * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) - */ - public static getItem({ key }: { - key: ObjectNode | Expression - }): ObjectNode { - return obj({ - version: str('2017-02-28'), - operation: str('GetItem'), - key - }) - } + /** + * Create a get item resolver template. + * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) + */ + public static getItem({ key }: { key: ObjectNode | Expression }): ObjectNode { + return obj({ + version: str('2017-02-28'), + operation: str('GetItem'), + key, + }); + } - /** - * Create a query resolver template. - * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) - */ - public static query(args: { - query: ObjectNode | Expression; - scanIndexForward: Expression; - filter: ObjectNode | Expression; - limit: Expression; - nextToken?: Expression; - index?: StringNode; - }): ObjectNode { - return obj({ - version: str('2017-02-28'), - operation: str('Query'), - ...args - }) - } + /** + * Create a query resolver template. + * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) + */ + public static query(args: { + query: ObjectNode | Expression; + scanIndexForward: Expression; + filter: ObjectNode | Expression; + limit: Expression; + nextToken?: Expression; + index?: StringNode; + }): ObjectNode { + return obj({ + version: str('2017-02-28'), + operation: str('Query'), + ...args, + }); + } - /** - * Create a list item resolver template. - * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) - */ - public static listItem({ filter, limit, nextToken, scanIndexForward, query, index }: { - filter: ObjectNode | Expression, - limit: Expression, - nextToken?: Expression, - scanIndexForward?: Expression; - query?: ObjectNode | Expression, - index?: StringNode, - }): ObjectNode { - return obj({ - version: str('2017-02-28'), - operation: str('Scan'), - filter, - limit, - nextToken, - query, - index, - scanIndexForward, - }) - } + /** + * Create a list item resolver template. + * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) + */ + public static listItem({ + filter, + limit, + nextToken, + scanIndexForward, + query, + index, + }: { + filter: ObjectNode | Expression; + limit: Expression; + nextToken?: Expression; + scanIndexForward?: Expression; + query?: ObjectNode | Expression; + index?: StringNode; + }): ObjectNode { + return obj({ + version: str('2017-02-28'), + operation: str('Scan'), + filter, + limit, + nextToken, + query, + index, + scanIndexForward, + }); + } - /** - * Create a delete item resolver template. - * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) - */ - public static deleteItem({ key, condition }: { - key: ObjectNode | Expression, - condition: ObjectNode | ReferenceNode - }): ObjectNode { - return obj({ - version: str('2017-02-28'), - operation: str('DeleteItem'), - key, - condition, - }) - } + /** + * Create a delete item resolver template. + * @param key A list of strings pointing to the key value locations. E.G. ctx.args.x (note no $) + */ + public static deleteItem({ key, condition }: { key: ObjectNode | Expression; condition: ObjectNode | ReferenceNode }): ObjectNode { + return obj({ + version: str('2017-02-28'), + operation: str('DeleteItem'), + key, + condition, + }); + } - /** - * Create an update item resolver template. - * @param key - */ - public static updateItem({ key, condition, objectKeyVariable, nameOverrideMap }: { - key: ObjectNode | Expression, - condition: ObjectNode | ReferenceNode, - objectKeyVariable: string, - nameOverrideMap?: string - }): CompoundExpressionNode { - // const keyFields = key.attributes.map((attr: [string, Expression]) => attr[0]) - // Auto timestamp - // qref('$input.put("updatedAt", "$util.time.nowISO8601()")'), - const entryKeyAttributeNameVar = 'entryKeyAttributeName'; - const handleRename = (keyVar: string) => ifElse( - raw(`!$util.isNull($${nameOverrideMap}) && $${nameOverrideMap}.containsKey("${keyVar}")`), - set(ref(entryKeyAttributeNameVar), raw(`$${nameOverrideMap}.get("${keyVar}")`)), - set(ref(entryKeyAttributeNameVar), raw(keyVar)), - ); - return compoundExpression([ - set(ref('expNames'), obj({})), - set(ref('expValues'), obj({})), - set(ref('expSet'), obj({})), - set(ref('expAdd'), obj({})), - set(ref('expRemove'), list([])), - ifElse( - ref(objectKeyVariable), - compoundExpression([ - set(ref('keyFields'), list([])), - forEach(ref('entry'), ref(`${objectKeyVariable}.entrySet()`),[ - qref('$keyFields.add("$entry.key")') - ]), - ]), - set(ref('keyFields'), list([str('id')])), - ), - forEach( - ref('entry'), - ref(`util.map.copyAndRemoveAllKeys($context.args.input, $keyFields).entrySet()`), - [ - handleRename('$entry.key'), - ifElse( - ref('util.isNull($entry.value)'), - compoundExpression([ - set(ref('discard'), ref(`expRemove.add("#$${entryKeyAttributeNameVar}")`)), - qref(`$expNames.put("#$${entryKeyAttributeNameVar}", "$entry.key")`) - ]), - compoundExpression([ - qref(`$expSet.put("#$${entryKeyAttributeNameVar}", ":$${entryKeyAttributeNameVar}")`), - qref(`$expNames.put("#$${entryKeyAttributeNameVar}", "$entry.key")`), - qref(`$expValues.put(":$${entryKeyAttributeNameVar}", $util.dynamodb.toDynamoDB($entry.value))`) - ]) - ) - ] - ), - set(ref('expression'), str('')), - iff(raw('!$expSet.isEmpty()'), compoundExpression([ - set(ref('expression'), str('SET')), - forEach(ref('entry'), ref('expSet.entrySet()'), [ - set(ref('expression'), str('$expression $entry.key = $entry.value')), - iff(ref('foreach.hasNext()'), set(ref('expression'), str('$expression,'))) - ]) - ])), - iff(raw('!$expAdd.isEmpty()'), compoundExpression([ - set(ref('expression'), str('$expression ADD')), - forEach(ref('entry'), ref('expAdd.entrySet()'), [ - set(ref('expression'), str('$expression $entry.key $entry.value')), - iff(ref('foreach.hasNext()'), set(ref('expression'), str('$expression,'))) - ]) - ])), - iff(raw('!$expRemove.isEmpty()'), compoundExpression([ - set(ref('expression'), str('$expression REMOVE')), - forEach(ref('entry'), ref('expRemove'), [ - set(ref('expression'), str('$expression $entry')), - iff(ref('foreach.hasNext()'), set(ref('expression'), str('$expression,'))) - ]) - ])), - set(ref('update'), obj({})), - qref('$update.put("expression", "$expression")'), - iff( - raw('!$expNames.isEmpty()'), - qref('$update.put("expressionNames", $expNames)') - ), - iff( - raw('!$expValues.isEmpty()'), - qref('$update.put("expressionValues", $expValues)') - ), - obj({ - version: str('2017-02-28'), - operation: str('UpdateItem'), - key, - update: ref('util.toJson($update)'), - condition - }) + /** + * Create an update item resolver template. + * @param key + */ + public static updateItem({ + key, + condition, + objectKeyVariable, + nameOverrideMap, + }: { + key: ObjectNode | Expression; + condition: ObjectNode | ReferenceNode; + objectKeyVariable: string; + nameOverrideMap?: string; + }): CompoundExpressionNode { + // const keyFields = key.attributes.map((attr: [string, Expression]) => attr[0]) + // Auto timestamp + // qref('$input.put("updatedAt", "$util.time.nowISO8601()")'), + const entryKeyAttributeNameVar = 'entryKeyAttributeName'; + const handleRename = (keyVar: string) => + ifElse( + raw(`!$util.isNull($${nameOverrideMap}) && $${nameOverrideMap}.containsKey("${keyVar}")`), + set(ref(entryKeyAttributeNameVar), raw(`$${nameOverrideMap}.get("${keyVar}")`)), + set(ref(entryKeyAttributeNameVar), raw(keyVar)) + ); + return compoundExpression([ + set(ref('expNames'), obj({})), + set(ref('expValues'), obj({})), + set(ref('expSet'), obj({})), + set(ref('expAdd'), obj({})), + set(ref('expRemove'), list([])), + ifElse( + ref(objectKeyVariable), + compoundExpression([ + set(ref('keyFields'), list([])), + forEach(ref('entry'), ref(`${objectKeyVariable}.entrySet()`), [qref('$keyFields.add("$entry.key")')]), + ]), + set(ref('keyFields'), list([str('id')])) + ), + forEach(ref('entry'), ref(`util.map.copyAndRemoveAllKeys($context.args.input, $keyFields).entrySet()`), [ + handleRename('$entry.key'), + ifElse( + ref('util.isNull($entry.value)'), + compoundExpression([ + set(ref('discard'), ref(`expRemove.add("#$${entryKeyAttributeNameVar}")`)), + qref(`$expNames.put("#$${entryKeyAttributeNameVar}", "$entry.key")`), + ]), + compoundExpression([ + qref(`$expSet.put("#$${entryKeyAttributeNameVar}", ":$${entryKeyAttributeNameVar}")`), + qref(`$expNames.put("#$${entryKeyAttributeNameVar}", "$entry.key")`), + qref(`$expValues.put(":$${entryKeyAttributeNameVar}", $util.dynamodb.toDynamoDB($entry.value))`), + ]) + ), + ]), + set(ref('expression'), str('')), + iff( + raw('!$expSet.isEmpty()'), + compoundExpression([ + set(ref('expression'), str('SET')), + forEach(ref('entry'), ref('expSet.entrySet()'), [ + set(ref('expression'), str('$expression $entry.key = $entry.value')), + iff(ref('foreach.hasNext()'), set(ref('expression'), str('$expression,'))), + ]), ]) - } + ), + iff( + raw('!$expAdd.isEmpty()'), + compoundExpression([ + set(ref('expression'), str('$expression ADD')), + forEach(ref('entry'), ref('expAdd.entrySet()'), [ + set(ref('expression'), str('$expression $entry.key $entry.value')), + iff(ref('foreach.hasNext()'), set(ref('expression'), str('$expression,'))), + ]), + ]) + ), + iff( + raw('!$expRemove.isEmpty()'), + compoundExpression([ + set(ref('expression'), str('$expression REMOVE')), + forEach(ref('entry'), ref('expRemove'), [ + set(ref('expression'), str('$expression $entry')), + iff(ref('foreach.hasNext()'), set(ref('expression'), str('$expression,'))), + ]), + ]) + ), + set(ref('update'), obj({})), + qref('$update.put("expression", "$expression")'), + iff(raw('!$expNames.isEmpty()'), qref('$update.put("expressionNames", $expNames)')), + iff(raw('!$expValues.isEmpty()'), qref('$update.put("expressionValues", $expValues)')), + obj({ + version: str('2017-02-28'), + operation: str('UpdateItem'), + key, + update: ref('util.toJson($update)'), + condition, + }), + ]); + } - public static stringAttributeValue(value: Expression): ObjectNode { - return { - kind: 'Object', attributes: [ - ['S', { kind: 'Quotes', expr: value }] - ] - }; - } + public static stringAttributeValue(value: Expression): ObjectNode { + return { + kind: 'Object', + attributes: [['S', { kind: 'Quotes', expr: value }]], + }; + } - public static numericAttributeValue(value: Expression): ObjectNode { - return { - kind: 'Object', attributes: [ - ['N', { kind: 'Quotes', expr: value }] - ] - }; - } + public static numericAttributeValue(value: Expression): ObjectNode { + return { + kind: 'Object', + attributes: [['N', { kind: 'Quotes', expr: value }]], + }; + } - public static binaryAttributeValue(value: Expression): ObjectNode { - return { - kind: 'Object', attributes: [ - ['B', { kind: 'Quotes', expr: value }] - ] - }; - } + public static binaryAttributeValue(value: Expression): ObjectNode { + return { + kind: 'Object', + attributes: [['B', { kind: 'Quotes', expr: value }]], + }; + } - public static paginatedResponse(): ObjectNode { - return obj({ - items: ref('util.toJson($ctx.result.items)'), - nextToken: ref('util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))') - }) - } + public static paginatedResponse(): ObjectNode { + return obj({ + items: ref('util.toJson($ctx.result.items)'), + nextToken: ref('util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))'), + }); + } } diff --git a/packages/graphql-mapping-template/src/elasticsearch.ts b/packages/graphql-mapping-template/src/elasticsearch.ts index cbd8057777..9a23992d5f 100644 --- a/packages/graphql-mapping-template/src/elasticsearch.ts +++ b/packages/graphql-mapping-template/src/elasticsearch.ts @@ -1,53 +1,73 @@ import { print } from './print'; import { - obj, Expression, str, ObjectNode, iff, ifElse, - ref, raw, int, CompoundExpressionNode, compoundExpression, - set, qref, ListNode, + obj, + Expression, + str, + ObjectNode, + iff, + ifElse, + ref, + raw, + int, + CompoundExpressionNode, + compoundExpression, + set, + qref, + ListNode, } from './ast'; export class ElasticsearchMappingTemplate { + /** + * Create a mapping template for ES. + */ + public static genericTemplte({ + operation, + path, + params, + }: { + operation: Expression; + path: Expression; + params: Expression | ObjectNode | CompoundExpressionNode; + }): ObjectNode { + return obj({ + version: str('2017-02-28'), + operation, + path, + params, + }); + } - /** - * Create a mapping template for ES. - */ - public static genericTemplte({ operation, path, params }: { - operation: Expression, - path: Expression, - params: Expression | ObjectNode | CompoundExpressionNode - }): ObjectNode { - return obj({ - version: str('2017-02-28'), - operation, - path, - params - }) - } - - /** - * Create a search item resolver template. - * @param size the size limit - * @param from the next token - * @param query the query - */ - public static searchItem({ query, size, search_after, path, sort }: { - path: Expression, - sort?: Expression | ObjectNode, - query?: ObjectNode | Expression, - size?: Expression, - search_after?: Expression | ListNode - }): ObjectNode { - return obj({ - version: str('2017-02-28'), - operation: str('GET'), - path, - params: obj({ - body: raw(`{ + /** + * Create a search item resolver template. + * @param size the size limit + * @param from the next token + * @param query the query + */ + public static searchItem({ + query, + size, + search_after, + path, + sort, + }: { + path: Expression; + sort?: Expression | ObjectNode; + query?: ObjectNode | Expression; + size?: Expression; + search_after?: Expression | ListNode; + }): ObjectNode { + return obj({ + version: str('2017-02-28'), + operation: str('GET'), + path, + params: obj({ + body: raw(`{ #if( $context.args.nextToken )"search_after": ${print(search_after)}, #end "size": ${print(size)}, "sort": ${print(sort)}, "query": ${print(query)} - }`) - }) - }) - } -} \ No newline at end of file + }`), + }), + }); + } +} diff --git a/packages/graphql-mapping-template/src/http.ts b/packages/graphql-mapping-template/src/http.ts index b6899e3c27..bd2261de5f 100644 --- a/packages/graphql-mapping-template/src/http.ts +++ b/packages/graphql-mapping-template/src/http.ts @@ -1,85 +1,65 @@ -import { - obj, Expression, str, ObjectNode, iff, ifElse, - ref, raw, int, CompoundExpressionNode, compoundExpression, - set, qref -} from './ast'; +import { obj, Expression, str, ObjectNode, iff, ifElse, ref, raw, int, CompoundExpressionNode, compoundExpression, set, qref } from './ast'; export class HttpMappingTemplate { + static httpVersionId = '2018-05-29'; - static httpVersionId = '2018-05-29' + /** + * Create a mapping template for HTTP GET requests. + */ + public static getRequest({ resourcePath, params }: { resourcePath: string; params: ObjectNode }): ObjectNode { + return obj({ + version: str(this.httpVersionId), + method: str('GET'), + resourcePath: str(resourcePath), + params, + }); + } - /** - * Create a mapping template for HTTP GET requests. - */ - public static getRequest({ resourcePath, params }: { - resourcePath: string, - params: ObjectNode - }): ObjectNode { - return obj({ - version: str(this.httpVersionId), - method: str('GET'), - resourcePath: str(resourcePath), - params - }) - } + /** + * Create a mapping template for HTTP POST requests. + */ + public static postRequest({ resourcePath, params }: { resourcePath: string; params: ObjectNode }): ObjectNode { + return obj({ + version: str(this.httpVersionId), + method: str('POST'), + resourcePath: str(resourcePath), + params, + }); + } - /** - * Create a mapping template for HTTP POST requests. - */ - public static postRequest({ resourcePath, params }: { - resourcePath: string, - params: ObjectNode - }): ObjectNode { - return obj({ - version: str(this.httpVersionId), - method: str('POST'), - resourcePath: str(resourcePath), - params - }) - } + /** + * Create a mapping template for HTTP PUT requests. + */ + public static putRequest({ resourcePath, params }: { resourcePath: string; params: ObjectNode }): ObjectNode { + return obj({ + version: str(this.httpVersionId), + method: str('PUT'), + resourcePath: str(resourcePath), + params, + }); + } - /** - * Create a mapping template for HTTP PUT requests. - */ - public static putRequest({ resourcePath, params }: { - resourcePath: string, - params: ObjectNode - }): ObjectNode { - return obj({ - version: str(this.httpVersionId), - method: str('PUT'), - resourcePath: str(resourcePath), - params - }) - } + /** + * Create a mapping template for HTTP DELETE requests. + */ + public static deleteRequest({ resourcePath, params }: { resourcePath: string; params: ObjectNode }): ObjectNode { + return obj({ + version: str(this.httpVersionId), + method: str('DELETE'), + resourcePath: str(resourcePath), + params, + }); + } - /** - * Create a mapping template for HTTP DELETE requests. - */ - public static deleteRequest({ resourcePath, params }: { - resourcePath: string, - params: ObjectNode - }): ObjectNode { - return obj({ - version: str(this.httpVersionId), - method: str('DELETE'), - resourcePath: str(resourcePath), - params - }) - } - - /** - * Create a mapping template for HTTP PATCH requests. - */ - public static patchRequest({ resourcePath, params }: { - resourcePath: string, - params: ObjectNode - }): ObjectNode { - return obj({ - version: str(this.httpVersionId), - method: str('PATCH'), - resourcePath: str(resourcePath), - params - }) - } -} \ No newline at end of file + /** + * Create a mapping template for HTTP PATCH requests. + */ + public static patchRequest({ resourcePath, params }: { resourcePath: string; params: ObjectNode }): ObjectNode { + return obj({ + version: str(this.httpVersionId), + method: str('PATCH'), + resourcePath: str(resourcePath), + params, + }); + } +} diff --git a/packages/graphql-mapping-template/src/print.ts b/packages/graphql-mapping-template/src/print.ts index 70563c36ea..2efdae8abf 100644 --- a/packages/graphql-mapping-template/src/print.ts +++ b/packages/graphql-mapping-template/src/print.ts @@ -1,203 +1,224 @@ import { - Expression, IfNode, IfElseNode, AndNode, OrNode, - ParensNode, EqualsNode, NotEqualsNode, ForEachNode, - StringNode, IntNode, NullNode, ReferenceNode, QuietReferenceNode, - ObjectNode, ListNode, FloatNode, QuotesNode, RawNode, SetNode, CompoundExpressionNode, - CommentNode, ToJsonNode, BooleanNode, compoundExpression, comment, NotNode, NewLineNode + Expression, + IfNode, + IfElseNode, + AndNode, + OrNode, + ParensNode, + EqualsNode, + NotEqualsNode, + ForEachNode, + StringNode, + IntNode, + NullNode, + ReferenceNode, + QuietReferenceNode, + ObjectNode, + ListNode, + FloatNode, + QuotesNode, + RawNode, + SetNode, + CompoundExpressionNode, + CommentNode, + ToJsonNode, + BooleanNode, + compoundExpression, + comment, + NotNode, + NewLineNode, } from './ast'; const TAB = ' '; function printIf(node: IfNode, indent: string = '') { - if (node.inline) { - return `#if( ${printExpr(node.predicate, '')} ) ${printExpr(node.expr, '')} #end`; - } - return `${indent}#if( ${printExpr(node.predicate, '')} )\n${printExpr(node.expr, indent + TAB)}\n${indent}#end`; + if (node.inline) { + return `#if( ${printExpr(node.predicate, '')} ) ${printExpr(node.expr, '')} #end`; + } + return `${indent}#if( ${printExpr(node.predicate, '')} )\n${printExpr(node.expr, indent + TAB)}\n${indent}#end`; } function printIfElse(node: IfElseNode, indent: string = '') { - if (node.inline) { - return `#if( ${printExpr(node.predicate)} ) ` + - `${printExpr(node.ifExpr)} ` + - `#else ` + - `${printExpr(node.elseExpr)} ` + - `#end`; - } - return `${indent}#if( ${printExpr(node.predicate)} )\n` + - `${printExpr(node.ifExpr, indent + TAB)}\n` + - `${indent}#else\n` + - `${printExpr(node.elseExpr, indent + TAB)}\n` + - `${indent}#end`; + if (node.inline) { + return `#if( ${printExpr(node.predicate)} ) ` + `${printExpr(node.ifExpr)} ` + `#else ` + `${printExpr(node.elseExpr)} ` + `#end`; + } + return ( + `${indent}#if( ${printExpr(node.predicate)} )\n` + + `${printExpr(node.ifExpr, indent + TAB)}\n` + + `${indent}#else\n` + + `${printExpr(node.elseExpr, indent + TAB)}\n` + + `${indent}#end` + ); } function printAnd(node: AndNode, indent: string = ''): string { - return indent + node.expressions.map((e: Expression) => printExpr(e)).join(' && '); + return indent + node.expressions.map((e: Expression) => printExpr(e)).join(' && '); } function printOr(node: OrNode, indent: string = ''): string { - return indent + node.expressions.map((e: Expression) => printExpr(e)).join(' || '); + return indent + node.expressions.map((e: Expression) => printExpr(e)).join(' || '); } function printParens(node: ParensNode, indent: string = ''): string { - return `${indent}(${printExpr(node.expr)})`; + return `${indent}(${printExpr(node.expr)})`; } function printEquals(node: EqualsNode, indent: string = ''): string { - return `${indent}${printExpr(node.leftExpr)} == ${printExpr(node.rightExpr)}`; + return `${indent}${printExpr(node.leftExpr)} == ${printExpr(node.rightExpr)}`; } function printNotEquals(node: NotEqualsNode, indent: string = ''): string { - return `${indent}${printExpr(node.leftExpr)} != ${printExpr(node.rightExpr)}`; + return `${indent}${printExpr(node.leftExpr)} != ${printExpr(node.rightExpr)}`; } function printForEach(node: ForEachNode, indent: string = ''): string { - return `${indent}#foreach( ${printExpr(node.key)} in ${printExpr(node.collection)} )\n` + - node.expressions.map((e: Expression) => printExpr(e, indent + TAB)).join('\n') + - `\n${indent}#end`; + return ( + `${indent}#foreach( ${printExpr(node.key)} in ${printExpr(node.collection)} )\n` + + node.expressions.map((e: Expression) => printExpr(e, indent + TAB)).join('\n') + + `\n${indent}#end` + ); } function printString(node: StringNode): string { - return `"${node.value}"`; + return `"${node.value}"`; } function printBool(node: BooleanNode): string { - return `${node.value}`; + return `${node.value}`; } function printRaw(node: RawNode, indent: string = ''): string { - return `${indent}${node.value}`; + return `${indent}${node.value}`; } function printQuotes(node: QuotesNode): string { - return `"${printExpr(node.expr)}"`; + return `"${printExpr(node.expr)}"`; } function printInt(node: IntNode): string { - return `${node.value}`; + return `${node.value}`; } function printFloat(node: FloatNode): string { - return `${node.value}`; + return `${node.value}`; } function printNull(node: NullNode): string { - return `null`; + return `null`; } function printReference(node: ReferenceNode): string { - return `\$${node.value}`; + return `\$${node.value}`; } function printQuietReference(node: QuietReferenceNode, indent: string = ''): string { - return `${indent}$util.qr(${node.value})`; + return `${indent}$util.qr(${node.value})`; } export function printObject(node: ObjectNode, indent: string = ''): string { - const attributes = node.attributes.map((attr: [string, Expression], i: number) => { - return `${indent}${TAB}"${attr[0]}": ${printExpr(attr[1], indent + TAB)}${i < node.attributes.length - 1 ? ',' : ''}`; - }); - const divider = attributes.length > 0 ? `\n${indent}` : '' - return `{${divider}${attributes.join(divider)}${divider}}`; + const attributes = node.attributes.map((attr: [string, Expression], i: number) => { + return `${indent}${TAB}"${attr[0]}": ${printExpr(attr[1], indent + TAB)}${i < node.attributes.length - 1 ? ',' : ''}`; + }); + const divider = attributes.length > 0 ? `\n${indent}` : ''; + return `{${divider}${attributes.join(divider)}${divider}}`; } function printList(node: ListNode, indent: string = ''): string { - const values = node.expressions.map((e: Expression) => printExpr(e, '')).join(', '); - return `${indent}[${values}]`; + const values = node.expressions.map((e: Expression) => printExpr(e, '')).join(', '); + return `${indent}[${values}]`; } function printSet(node: SetNode, indent: string = ''): string { - return `${indent}#set( ${printReference(node.key)} = ${printExpr(node.value, '')} )` + return `${indent}#set( ${printReference(node.key)} = ${printExpr(node.value, '')} )`; } function printComment(node: CommentNode, indent: string = ''): string { - return `${indent}## ${node.text} **` + return `${indent}## ${node.text} **`; } function printCompoundExpression(node: CompoundExpressionNode, indent: string = ''): string { - return node.expressions.map((node: Expression) => printExpr(node, indent)).join(`\n`) + return node.expressions.map((node: Expression) => printExpr(node, indent)).join(`\n`); } function printToJson(node: ToJsonNode, indent: string = ''): string { - return `${indent}$util.toJson(${printExpr(node.expr, '')})` + return `${indent}$util.toJson(${printExpr(node.expr, '')})`; } function printNot(node: NotNode, indent: string = ''): string { - return `${indent}!${printExpr(node.expr, '')}` + return `${indent}!${printExpr(node.expr, '')}`; } function printNewLine(node: NewLineNode): string { - return '\n' + return '\n'; } function printExpr(expr: Expression, indent: string = ''): string { - if (!expr) { return ''; } - switch (expr.kind) { - case 'If': - return printIf(expr, indent); - case 'IfElse': - return printIfElse(expr, indent); - case 'And': - return printAnd(expr, indent); - case 'Or': - return printOr(expr, indent); - case 'Parens': - return printParens(expr, indent); - case 'Equals': - return printEquals(expr, indent); - case 'NotEquals': - return printNotEquals(expr, indent); - case 'ForEach': - return printForEach(expr, indent); - case 'String': - return printString(expr); - case 'Raw': - return printRaw(expr, indent); - case 'Quotes': - return printQuotes(expr); - case 'Float': - return printFloat(expr); - case 'Int': - return printInt(expr); - case 'Boolean': - return printBool(expr); - case 'Null': - return printNull(expr); - case 'Reference': - return printReference(expr); - case 'QuietReference': - return printQuietReference(expr, indent); - case 'Object': - return printObject(expr, indent); - case 'List': - return printList(expr, indent); - case 'Set': - return printSet(expr, indent); - case 'Comment': - return printComment(expr, indent); - case 'CompoundExpression': - return printCompoundExpression(expr, indent) - case 'Util.ToJson': - return printToJson(expr, indent) - case 'Not': - return printNot(expr, indent) - case 'NewLine': - return printNewLine(expr) - default: - return ''; - } + if (!expr) { + return ''; + } + switch (expr.kind) { + case 'If': + return printIf(expr, indent); + case 'IfElse': + return printIfElse(expr, indent); + case 'And': + return printAnd(expr, indent); + case 'Or': + return printOr(expr, indent); + case 'Parens': + return printParens(expr, indent); + case 'Equals': + return printEquals(expr, indent); + case 'NotEquals': + return printNotEquals(expr, indent); + case 'ForEach': + return printForEach(expr, indent); + case 'String': + return printString(expr); + case 'Raw': + return printRaw(expr, indent); + case 'Quotes': + return printQuotes(expr); + case 'Float': + return printFloat(expr); + case 'Int': + return printInt(expr); + case 'Boolean': + return printBool(expr); + case 'Null': + return printNull(expr); + case 'Reference': + return printReference(expr); + case 'QuietReference': + return printQuietReference(expr, indent); + case 'Object': + return printObject(expr, indent); + case 'List': + return printList(expr, indent); + case 'Set': + return printSet(expr, indent); + case 'Comment': + return printComment(expr, indent); + case 'CompoundExpression': + return printCompoundExpression(expr, indent); + case 'Util.ToJson': + return printToJson(expr, indent); + case 'Not': + return printNot(expr, indent); + case 'NewLine': + return printNewLine(expr); + default: + return ''; + } } export function print(expr: Expression): string { - return printExpr(expr); + return printExpr(expr); } export function printBlock(name: string) { - return (expr: Expression): string => { - const wrappedExpr = compoundExpression([ - comment(`[Start] ${name}.`), - expr, - comment(`[End] ${name}.`) - ]) - return printExpr(wrappedExpr); - } + return (expr: Expression): string => { + const wrappedExpr = compoundExpression([comment(`[Start] ${name}.`), expr, comment(`[End] ${name}.`)]); + return printExpr(wrappedExpr); + }; } diff --git a/packages/graphql-relational-schema-transformer/src/AuroraDataAPIClient.ts b/packages/graphql-relational-schema-transformer/src/AuroraDataAPIClient.ts index 8694dccc1b..dbe58c4b71 100644 --- a/packages/graphql-relational-schema-transformer/src/AuroraDataAPIClient.ts +++ b/packages/graphql-relational-schema-transformer/src/AuroraDataAPIClient.ts @@ -1,121 +1,118 @@ - /** * A wrapper around the RDS data service client, forming their responses for * easier consumption. */ export class AuroraDataAPIClient { - - AWS: any - RDS: any - Params: DataApiParams - - setRDSClient(rdsClient: any) { - this.RDS = rdsClient + AWS: any; + RDS: any; + Params: DataApiParams; + + setRDSClient(rdsClient: any) { + this.RDS = rdsClient; + } + + constructor(databaseRegion: string, awsSecretStoreArn: string, dbClusterOrInstanceArn: string, database: string, aws: any) { + this.AWS = aws; + this.AWS.config.update({ + region: databaseRegion, + }); + + this.RDS = new this.AWS.RDSDataService(); + this.Params = new DataApiParams(); + + this.Params.secretArn = awsSecretStoreArn; + this.Params.resourceArn = dbClusterOrInstanceArn; + this.Params.database = database; + } + + /** + * Lists all of the tables in the set database. + * + * @return a list of tables in the database. + */ + public listTables = async () => { + this.Params.sql = 'SHOW TABLES'; + const response = await this.RDS.executeStatement(this.Params).promise(); + + let tableList = []; + const records = response['records']; + for (const record of records) { + tableList.push(record[0]['stringValue']); } - constructor(databaseRegion: string, awsSecretStoreArn: string, dbClusterOrInstanceArn: string, database: string, aws: any) { - this.AWS = aws - this.AWS.config.update({ - region: databaseRegion - }) - - this.RDS = new this.AWS.RDSDataService() - this.Params = new DataApiParams() - - this.Params.secretArn = awsSecretStoreArn - this.Params.resourceArn = dbClusterOrInstanceArn - this.Params.database = database + return tableList; + }; + + /** + * Describes the table given, by breaking it down into individual column descriptions. + * + * @param the name of the table to be described. + * @return a list of column descriptions. + */ + public describeTable = async (tableName: string) => { + this.Params.sql = `DESCRIBE ${tableName}`; + const response = await this.RDS.executeStatement(this.Params).promise(); + const listOfColumns = response['records']; + let columnDescriptions = []; + for (const column of listOfColumns) { + let colDescription = new ColumnDescription(); + + colDescription.Field = column[MYSQL_DESCRIBE_TABLE_ORDER.Field]['stringValue']; + colDescription.Type = column[MYSQL_DESCRIBE_TABLE_ORDER.Type]['stringValue']; + colDescription.Null = column[MYSQL_DESCRIBE_TABLE_ORDER.Null]['stringValue']; + colDescription.Key = column[MYSQL_DESCRIBE_TABLE_ORDER.Key]['stringValue']; + colDescription.Default = column[MYSQL_DESCRIBE_TABLE_ORDER.Default]['stringValue']; + colDescription.Extra = column[MYSQL_DESCRIBE_TABLE_ORDER.Extra]['stringValue']; + + columnDescriptions.push(colDescription); } - /** - * Lists all of the tables in the set database. - * - * @return a list of tables in the database. - */ - public listTables = async () => { - this.Params.sql = 'SHOW TABLES' - const response = await this.RDS.executeStatement(this.Params).promise() - - let tableList = [] - const records = response['records'] - for (const record of records) { - tableList.push(record[0]['stringValue']) - } - - return tableList - } - - /** - * Describes the table given, by breaking it down into individual column descriptions. - * - * @param the name of the table to be described. - * @return a list of column descriptions. - */ - public describeTable = async (tableName: string) => { - this.Params.sql = `DESCRIBE ${tableName}` - const response = await this.RDS.executeStatement(this.Params).promise() - const listOfColumns = response['records'] - let columnDescriptions = [] - for (const column of listOfColumns) { - let colDescription = new ColumnDescription() - - colDescription.Field = column[MYSQL_DESCRIBE_TABLE_ORDER.Field]['stringValue'] - colDescription.Type = column[MYSQL_DESCRIBE_TABLE_ORDER.Type]['stringValue'] - colDescription.Null = column[MYSQL_DESCRIBE_TABLE_ORDER.Null]['stringValue'] - colDescription.Key = column[MYSQL_DESCRIBE_TABLE_ORDER.Key]['stringValue'] - colDescription.Default = column[MYSQL_DESCRIBE_TABLE_ORDER.Default]['stringValue'] - colDescription.Extra = column[MYSQL_DESCRIBE_TABLE_ORDER.Extra]['stringValue'] - - columnDescriptions.push(colDescription) - } - - return columnDescriptions - } - - /** - * Gets foreign keys for the given table, if any exist. - * - * @param tableName the name of the table to be checked. - * @return a list of tables referencing the provided table, if any exist. - */ - public getTableForeignKeyReferences = async (tableName: string) => { - this.Params.sql = `SELECT TABLE_NAME FROM information_schema.key_column_usage + return columnDescriptions; + }; + + /** + * Gets foreign keys for the given table, if any exist. + * + * @param tableName the name of the table to be checked. + * @return a list of tables referencing the provided table, if any exist. + */ + public getTableForeignKeyReferences = async (tableName: string) => { + this.Params.sql = `SELECT TABLE_NAME FROM information_schema.key_column_usage WHERE referenced_table_name is not null - AND REFERENCED_TABLE_NAME = '${tableName}';` - const response = await this.RDS.executeStatement(this.Params).promise() + AND REFERENCED_TABLE_NAME = '${tableName}';`; + const response = await this.RDS.executeStatement(this.Params).promise(); - let tableList = [] - const records = response['records'] - for (const record of records) { - tableList.push(record[0]['stringValue']) - } - - return tableList + let tableList = []; + const records = response['records']; + for (const record of records) { + tableList.push(record[0]['stringValue']); } + return tableList; + }; } export class DataApiParams { - database: string - secretArn: string - resourceArn: string - sql: string + database: string; + secretArn: string; + resourceArn: string; + sql: string; } export class ColumnDescription { - Field: string - Type: string - Null: string - Key: string - Default: string - Extra: string + Field: string; + Type: string; + Null: string; + Key: string; + Default: string; + Extra: string; } enum MYSQL_DESCRIBE_TABLE_ORDER { - Field, - Type, - Null, - Key, - Default, - Extra -} \ No newline at end of file + Field, + Type, + Null, + Key, + Default, + Extra, +} diff --git a/packages/graphql-relational-schema-transformer/src/AuroraServerlessMySQLDatabaseReader.ts b/packages/graphql-relational-schema-transformer/src/AuroraServerlessMySQLDatabaseReader.ts index 85a1a34c38..1ef393222f 100644 --- a/packages/graphql-relational-schema-transformer/src/AuroraServerlessMySQLDatabaseReader.ts +++ b/packages/graphql-relational-schema-transformer/src/AuroraServerlessMySQLDatabaseReader.ts @@ -1,163 +1,174 @@ -import TemplateContext, { TableContext } from "./RelationalDBSchemaTransformer"; -import { getNamedType, getNonNullType, getInputValueDefinition, getGraphQLTypeFromMySQLType, - getTypeDefinition, getFieldDefinition, getInputTypeDefinition } from './RelationalDBSchemaTransformerUtils' -import { AuroraDataAPIClient } from "./AuroraDataAPIClient"; -import { IRelationalDBReader } from "./IRelationalDBReader"; -import { toUpper } from 'graphql-transformer-common' +import TemplateContext, { TableContext } from './RelationalDBSchemaTransformer'; +import { + getNamedType, + getNonNullType, + getInputValueDefinition, + getGraphQLTypeFromMySQLType, + getTypeDefinition, + getFieldDefinition, + getInputTypeDefinition, +} from './RelationalDBSchemaTransformerUtils'; +import { AuroraDataAPIClient } from './AuroraDataAPIClient'; +import { IRelationalDBReader } from './IRelationalDBReader'; +import { toUpper } from 'graphql-transformer-common'; /** * A class to manage interactions with a Aurora Serverless MySQL Relational Databse - * using the Aurora Data API + * using the Aurora Data API */ export class AuroraServerlessMySQLDatabaseReader implements IRelationalDBReader { - - auroraClient: AuroraDataAPIClient - dbRegion: string - awsSecretStoreArn: string - dbClusterOrInstanceArn: string - database: string - - setAuroraClient(auroraClient: AuroraDataAPIClient) { - this.auroraClient = auroraClient - } - - constructor(dbRegion: string, awsSecretStoreArn: string, dbClusterOrInstanceArn: string, database: string, aws:any) { - this.auroraClient = new AuroraDataAPIClient(dbRegion, awsSecretStoreArn, - dbClusterOrInstanceArn, database, aws) - this.dbRegion = dbRegion - this.awsSecretStoreArn = awsSecretStoreArn - this.dbClusterOrInstanceArn = dbClusterOrInstanceArn - this.database = database - } - + auroraClient: AuroraDataAPIClient; + dbRegion: string; + awsSecretStoreArn: string; + dbClusterOrInstanceArn: string; + database: string; + + setAuroraClient(auroraClient: AuroraDataAPIClient) { + this.auroraClient = auroraClient; + } + + constructor(dbRegion: string, awsSecretStoreArn: string, dbClusterOrInstanceArn: string, database: string, aws: any) { + this.auroraClient = new AuroraDataAPIClient(dbRegion, awsSecretStoreArn, dbClusterOrInstanceArn, database, aws); + this.dbRegion = dbRegion; + this.awsSecretStoreArn = awsSecretStoreArn; + this.dbClusterOrInstanceArn = dbClusterOrInstanceArn; + this.database = database; + } + + /** + * Stores some of the Aurora Serverless MySQL context into the template context, + * for later consumption. + * + * @param contextShell the basic template context, with db source independent fields set. + * @returns a fully hydrated template context, complete with Aurora Serverless MySQL context. + */ + hydrateTemplateContext = async (contextShell: TemplateContext): Promise => { /** - * Stores some of the Aurora Serverless MySQL context into the template context, - * for later consumption. - * - * @param contextShell the basic template context, with db source independent fields set. - * @returns a fully hydrated template context, complete with Aurora Serverless MySQL context. + * Information needed for creating the AppSync - RDS Data Source + * Store as part of the TemplateContext */ - hydrateTemplateContext = async(contextShell: TemplateContext): Promise => { - + contextShell.secretStoreArn = this.awsSecretStoreArn; + contextShell.rdsClusterIdentifier = this.dbClusterOrInstanceArn; + contextShell.databaseSchema = 'mysql'; + contextShell.databaseName = this.database; + contextShell.region = this.dbRegion; + return contextShell; + }; + + /** + * Gets a list of all the table names in the provided database. + * + * @returns a list of tablenames inside the database. + */ + listTables = async (): Promise => { + const results = await this.auroraClient.listTables(); + return results; + }; + + /** + * Looks up any foreign key constraints that might exist for the provided table. + * This is done to ensure our generated schema includes nested types, where possible. + * + * @param tableName the name of the table to be checked for foreign key constraints. + * @returns a list of table names that are applicable as having constraints. + */ + getTableForeignKeyReferences = async (tableName: string): Promise => { + const results = await this.auroraClient.getTableForeignKeyReferences(tableName); + return results; + }; + + /** + * For the provided table, this will create a table context. That context holds definitions for + * the base table type, the create input type, and the update input type (e.g. Post, CreatePostInput, and UpdatePostInput, respectively), + * as well as the table primary key structure for proper operation definition. + * + * Create inputs will only differ from the base table type in that any nested types will not be present. Update table + * inputs will differ in that the only required field will be the primary key/identifier, as all fields don't have to + * be updated. Instead, it assumes the proper ones were provided on create. + * + * @param tableName the name of the table to be translated into a GraphQL type. + * @returns a promise of a table context structure. + */ + describeTable = async (tableName: string): Promise => { + const columnDescriptions = await this.auroraClient.describeTable(tableName); + // Fields in the general type (e.g. Post). Both the identifying field and any others the db dictates will be required. + const fields = new Array(); + // Fields in the update input type (e.g. UpdatePostInput). Only the identifying field will be required, any others will be optional. + const updateFields = new Array(); + // Field in the create input type (e.g. CreatePostInput). + const createFields = new Array(); + + // The primary key, used to help generate queries and mutations + let primaryKey = ''; + let primaryKeyType = ''; + + // Field Lists needed as context for auto-generating the Query Resolvers + const intFieldList = new Array(); + const stringFieldList = new Array(); + + const formattedTableName = toUpper(tableName); + + for (const columnDescription of columnDescriptions) { + // If a field is the primary key, save it. + if (columnDescription.Key == 'PRI') { + primaryKey = columnDescription.Field; + primaryKeyType = getGraphQLTypeFromMySQLType(columnDescription.Type); + } else { /** - * Information needed for creating the AppSync - RDS Data Source - * Store as part of the TemplateContext + * If the field is not a key, then store it in the fields list. + * As we need this information later to generate query resolvers + * + * Currently we will only auto-gen query resolvers for the Int and String scalars */ - contextShell.secretStoreArn = this.awsSecretStoreArn - contextShell.rdsClusterIdentifier = this.dbClusterOrInstanceArn - contextShell.databaseSchema = 'mysql' - contextShell.databaseName = this.database - contextShell.region = this.dbRegion - return contextShell - } + const type = getGraphQLTypeFromMySQLType(columnDescription.Type); + if (type === 'Int') { + intFieldList.push(columnDescription.Field); + } else if (type === 'String') { + stringFieldList.push(columnDescription.Field); + } + } - /** - * Gets a list of all the table names in the provided database. - * - * @returns a list of tablenames inside the database. - */ - listTables = async (): Promise => { - const results = await this.auroraClient.listTables() - return results - } + // Create the basic field type shape, to be consumed by every field definition + const baseType = getNamedType(getGraphQLTypeFromMySQLType(columnDescription.Type)); - /** - * Looks up any foreign key constraints that might exist for the provided table. - * This is done to ensure our generated schema includes nested types, where possible. - * - * @param tableName the name of the table to be checked for foreign key constraints. - * @returns a list of table names that are applicable as having constraints. - */ - getTableForeignKeyReferences = async (tableName: string) : Promise => { - const results = await this.auroraClient.getTableForeignKeyReferences(tableName) - return results - } + const isPrimaryKey = columnDescription.Key == 'PRI'; + const isNullable = columnDescription.Null == 'YES'; - /** - * For the provided table, this will create a table context. That context holds definitions for - * the base table type, the create input type, and the update input type (e.g. Post, CreatePostInput, and UpdatePostInput, respectively), - * as well as the table primary key structure for proper operation definition. - * - * Create inputs will only differ from the base table type in that any nested types will not be present. Update table - * inputs will differ in that the only required field will be the primary key/identifier, as all fields don't have to - * be updated. Instead, it assumes the proper ones were provided on create. - * - * @param tableName the name of the table to be translated into a GraphQL type. - * @returns a promise of a table context structure. - */ - describeTable = async (tableName: string): Promise => { - const columnDescriptions = await this.auroraClient.describeTable(tableName) - // Fields in the general type (e.g. Post). Both the identifying field and any others the db dictates will be required. - const fields = new Array() - // Fields in the update input type (e.g. UpdatePostInput). Only the identifying field will be required, any others will be optional. - const updateFields = new Array() - // Field in the create input type (e.g. CreatePostInput). - const createFields = new Array() - - // The primary key, used to help generate queries and mutations - let primaryKey = "" - let primaryKeyType = "" - - // Field Lists needed as context for auto-generating the Query Resolvers - const intFieldList = new Array() - const stringFieldList = new Array() - - const formattedTableName = toUpper(tableName) - - for (const columnDescription of columnDescriptions) { - // If a field is the primary key, save it. - if (columnDescription.Key == 'PRI') { - primaryKey = columnDescription.Field - primaryKeyType = getGraphQLTypeFromMySQLType(columnDescription.Type) - } else { - /** - * If the field is not a key, then store it in the fields list. - * As we need this information later to generate query resolvers - * - * Currently we will only auto-gen query resolvers for the Int and String scalars - */ - const type = getGraphQLTypeFromMySQLType(columnDescription.Type) - if (type === 'Int') { - intFieldList.push(columnDescription.Field) - } else if (type === 'String') { - stringFieldList.push(columnDescription.Field) - } - } - - // Create the basic field type shape, to be consumed by every field definition - const baseType = getNamedType(getGraphQLTypeFromMySQLType(columnDescription.Type)) - - const isPrimaryKey = columnDescription.Key == 'PRI' - const isNullable = columnDescription.Null == 'YES' - - // Generate the field for the general type and the create input type - const type = (!isPrimaryKey && isNullable) ? baseType : getNonNullType(baseType) - fields.push(getFieldDefinition(columnDescription.Field, type)) - - createFields.push(getInputValueDefinition(type, columnDescription.Field)) - - // UpdateInput has only the primary key as required, ignoring all other that the database requests as non-nullable - const updateType = !isPrimaryKey ? baseType : getNonNullType(baseType) - updateFields.push(getInputValueDefinition(updateType, columnDescription.Field)) - } + // Generate the field for the general type and the create input type + const type = !isPrimaryKey && isNullable ? baseType : getNonNullType(baseType); + fields.push(getFieldDefinition(columnDescription.Field, type)); - // Add foreign key for this table - - // NOTE from @mikeparisstuff: It would be great to re-enable this such that foreign key relationships are - // resolver automatically. This code was breaking compilation because it was not - // creating XConnection types correctly. This package also does not yet support - // wiring up the resolvers (or ideally selection set introspection & automatic JOINs) - // so there is not point in creating these connection fields anyway. Disabling until - // supported. - // let tablesWithRef = await this.getTableForeignKeyReferences(tableName) - // for (const tableWithRef of tablesWithRef) { - // if (tableWithRef && tableWithRef.length > 0) { - // const baseType = getNamedType(`${tableWithRef}Connection`) - // fields.push(getFieldDefinition(`${tableWithRef}`, baseType)) - // } - // } - - return new TableContext(getTypeDefinition(fields, tableName), getInputTypeDefinition(createFields, `Create${formattedTableName}Input`), - getInputTypeDefinition(updateFields, `Update${formattedTableName}Input`), primaryKey, primaryKeyType, stringFieldList, intFieldList) + createFields.push(getInputValueDefinition(type, columnDescription.Field)); + + // UpdateInput has only the primary key as required, ignoring all other that the database requests as non-nullable + const updateType = !isPrimaryKey ? baseType : getNonNullType(baseType); + updateFields.push(getInputValueDefinition(updateType, columnDescription.Field)); } -} \ No newline at end of file + + // Add foreign key for this table + + // NOTE from @mikeparisstuff: It would be great to re-enable this such that foreign key relationships are + // resolver automatically. This code was breaking compilation because it was not + // creating XConnection types correctly. This package also does not yet support + // wiring up the resolvers (or ideally selection set introspection & automatic JOINs) + // so there is not point in creating these connection fields anyway. Disabling until + // supported. + // let tablesWithRef = await this.getTableForeignKeyReferences(tableName) + // for (const tableWithRef of tablesWithRef) { + // if (tableWithRef && tableWithRef.length > 0) { + // const baseType = getNamedType(`${tableWithRef}Connection`) + // fields.push(getFieldDefinition(`${tableWithRef}`, baseType)) + // } + // } + + return new TableContext( + getTypeDefinition(fields, tableName), + getInputTypeDefinition(createFields, `Create${formattedTableName}Input`), + getInputTypeDefinition(updateFields, `Update${formattedTableName}Input`), + primaryKey, + primaryKeyType, + stringFieldList, + intFieldList + ); + }; +} diff --git a/packages/graphql-relational-schema-transformer/src/IRelationalDBReader.ts b/packages/graphql-relational-schema-transformer/src/IRelationalDBReader.ts index c42447caa9..3ad382bb4a 100644 --- a/packages/graphql-relational-schema-transformer/src/IRelationalDBReader.ts +++ b/packages/graphql-relational-schema-transformer/src/IRelationalDBReader.ts @@ -1,15 +1,15 @@ -import TemplateContext, { TableContext } from "./RelationalDBSchemaTransformer"; +import TemplateContext, { TableContext } from './RelationalDBSchemaTransformer'; /** * An interface to manage interactions with a relational database across * various forms of clients. */ export interface IRelationalDBReader { - listTables(): Promise + listTables(): Promise; - getTableForeignKeyReferences(tableName: string): Promise + getTableForeignKeyReferences(tableName: string): Promise; - describeTable(tableName: string): Promise + describeTable(tableName: string): Promise; - hydrateTemplateContext(contextShell: TemplateContext): Promise -} \ No newline at end of file + hydrateTemplateContext(contextShell: TemplateContext): Promise; +} diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBMappingTemplate.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBMappingTemplate.ts index ace9422390..f041984ce8 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBMappingTemplate.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBMappingTemplate.ts @@ -1,24 +1,19 @@ -import { - obj, str, ObjectNode, ListNode -} from 'graphql-mapping-template'; +import { obj, str, ObjectNode, ListNode } from 'graphql-mapping-template'; /** * The class that contains the resolver templates for interacting * with the Relational Database data source. */ export default class RelationalDBMappingTemplate { - - /** - * Provided a SQL statement, creates the rds-query item resolver template. - * - * @param param0 - the SQL statement to use when querying the RDS cluster - */ - public static rdsQuery({statements}: { - statements: ListNode - }): ObjectNode { - return obj({ - version: str('2018-05-29'), - statements: statements - }) - } -} \ No newline at end of file + /** + * Provided a SQL statement, creates the rds-query item resolver template. + * + * @param param0 - the SQL statement to use when querying the RDS cluster + */ + public static rdsQuery({ statements }: { statements: ListNode }): ObjectNode { + return obj({ + version: str('2018-05-29'), + statements: statements, + }); + } +} diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBParsingException.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBParsingException.ts index be5c2975ec..fd4d716a39 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBParsingException.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBParsingException.ts @@ -1,8 +1,8 @@ export class RelationalDBParsingException extends Error { - constructor(message: string, stack?: string) { - super(message) + constructor(message: string, stack?: string) { + super(message); - Object.setPrototypeOf(this, RelationalDBParsingException.prototype) - this.stack = stack - } -} \ No newline at end of file + Object.setPrototypeOf(this, RelationalDBParsingException.prototype); + this.stack = stack; + } +} diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts index 36aa4bb15d..70d0916fd9 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBResolverGenerator.ts @@ -1,15 +1,15 @@ -import TemplateContext from "./RelationalDBSchemaTransformer"; -import { DocumentNode } from 'graphql' -import { Fn } from 'cloudform' -import AppSync from 'cloudform-types/types/appSync' -import { print, obj, set, str, list, forEach, ref, compoundExpression } from 'graphql-mapping-template' -import { graphqlName, toUpper, plurality } from 'graphql-transformer-common' -import { ResourceConstants } from './ResourceConstants' -import RelationalDBMappingTemplate from './RelationalDBMappingTemplate' -import * as fs from 'fs-extra' +import TemplateContext from './RelationalDBSchemaTransformer'; +import { DocumentNode } from 'graphql'; +import { Fn } from 'cloudform'; +import AppSync from 'cloudform-types/types/appSync'; +import { print, obj, set, str, list, forEach, ref, compoundExpression } from 'graphql-mapping-template'; +import { graphqlName, toUpper, plurality } from 'graphql-transformer-common'; +import { ResourceConstants } from './ResourceConstants'; +import RelationalDBMappingTemplate from './RelationalDBMappingTemplate'; +import * as fs from 'fs-extra'; -const s3BaseUrl = 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}' -const resolverFileName = 'ResolverFileName' +const s3BaseUrl = 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}'; +const resolverFileName = 'ResolverFileName'; /** * This Class is responsible for Generating the RDS Resolvers based on the * GraphQL Schema + Metadata of the RDS Cluster (i.e. Primary Keys for Tables). @@ -19,353 +19,313 @@ const resolverFileName = 'ResolverFileName' * RelationDBTemplateGenerator creates. */ export default class RelationalDBResolverGenerator { - document: DocumentNode - typePrimaryKeyMap: Map; - stringFieldMap: Map - intFieldMap: Map - resolverFilePath: string - typePrimaryKeyTypeMap: Map + document: DocumentNode; + typePrimaryKeyMap: Map; + stringFieldMap: Map; + intFieldMap: Map; + resolverFilePath: string; + typePrimaryKeyTypeMap: Map; - constructor(context: TemplateContext) { - this.document = context.schemaDoc - this.typePrimaryKeyMap = context.typePrimaryKeyMap - this.stringFieldMap = context.stringFieldMap - this.intFieldMap = context.intFieldMap - this.typePrimaryKeyTypeMap = context.typePrimaryKeyTypeMap; - } + constructor(context: TemplateContext) { + this.document = context.schemaDoc; + this.typePrimaryKeyMap = context.typePrimaryKeyMap; + this.stringFieldMap = context.stringFieldMap; + this.intFieldMap = context.intFieldMap; + this.typePrimaryKeyTypeMap = context.typePrimaryKeyTypeMap; + } - /** - * Creates the CRUDL+Q Resolvers as a Map of Cloudform Resources. The output can then be - * merged with an existing Template's map of Resources. - */ - public createRelationalResolvers(resolverFilePath: string) { - let resources = {} - this.resolverFilePath = resolverFilePath - this.typePrimaryKeyMap.forEach((value: string, key: string) => { - const resourceName = key.replace(/[^A-Za-z0-9]/g, '') - resources = { - ...resources, - ...{[resourceName + 'CreateResolver']: this.makeCreateRelationalResolver(key)}, - ...{[resourceName + 'GetResolver']: this.makeGetRelationalResolver(key)}, - ...{[resourceName + 'UpdateResolver']: this.makeUpdateRelationalResolver(key)}, - ...{[resourceName + 'DeleteResolver']: this.makeDeleteRelationalResolver(key)}, - ...{[resourceName + 'ListResolver']: this.makeListRelationalResolver(key)}, - } - // TODO: Add Guesstimate Query Resolvers - }) + /** + * Creates the CRUDL+Q Resolvers as a Map of Cloudform Resources. The output can then be + * merged with an existing Template's map of Resources. + */ + public createRelationalResolvers(resolverFilePath: string) { + let resources = {}; + this.resolverFilePath = resolverFilePath; + this.typePrimaryKeyMap.forEach((value: string, key: string) => { + const resourceName = key.replace(/[^A-Za-z0-9]/g, ''); + resources = { + ...resources, + ...{ [resourceName + 'CreateResolver']: this.makeCreateRelationalResolver(key) }, + ...{ [resourceName + 'GetResolver']: this.makeGetRelationalResolver(key) }, + ...{ [resourceName + 'UpdateResolver']: this.makeUpdateRelationalResolver(key) }, + ...{ [resourceName + 'DeleteResolver']: this.makeDeleteRelationalResolver(key) }, + ...{ [resourceName + 'ListResolver']: this.makeListRelationalResolver(key) }, + }; + // TODO: Add Guesstimate Query Resolvers + }); - return resources - } + return resources; + } - /** - * Private Helpers to Generate the CFN Spec for the Resolver Resources - */ + /** + * Private Helpers to Generate the CFN Spec for the Resolver Resources + */ + + /** + * Creates and returns the CFN Spec for the 'Create' Resolver Resource provided + * a GraphQL Type as the input + * + * @param type - the graphql type for which the create resolver will be created + * @param mutationTypeName - will be 'Mutation' + */ + private makeCreateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') { + const fieldName = graphqlName('create' + toUpper(type)); + let createSql = `INSERT INTO ${type} $colStr VALUES $valStr`; + let selectSql; + if (this.typePrimaryKeyTypeMap.get(type).includes('String')) { + selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.create${toUpper( + type + )}Input.${this.typePrimaryKeyMap.get(type)}\'`; + } else { + selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.create${toUpper( + type + )}Input.${this.typePrimaryKeyMap.get(type)}`; + } - /** - * Creates and returns the CFN Spec for the 'Create' Resolver Resource provided - * a GraphQL Type as the input - * - * @param type - the graphql type for which the create resolver will be created - * @param mutationTypeName - will be 'Mutation' - */ - private makeCreateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') { - const fieldName = graphqlName('create' + toUpper(type)) - let createSql = `INSERT INTO ${type} $colStr VALUES $valStr` - let selectSql; - if (this.typePrimaryKeyTypeMap.get(type).includes("String")) { - selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.create${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}\'`; - } else { - selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.create${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}`; - } + const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`; + const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`; - const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl` - const resFileName = `${mutationTypeName}.${fieldName}.res.vtl` + const reqTemplate = print( + compoundExpression([ + set(ref('cols'), list([])), + set(ref('vals'), list([])), + forEach(ref('entry'), ref(`ctx.args.create${toUpper(type)}Input.keySet()`), [ + set(ref('discard'), ref(`cols.add($entry)`)), + set(ref('discard'), ref(`vals.add("'$ctx.args.create${toUpper(type)}Input[$entry]'")`)), + ]), + set(ref('valStr'), ref('vals.toString().replace("[","(").replace("]",")")')), + set(ref('colStr'), ref('cols.toString().replace("[","(").replace("]",")")')), + RelationalDBMappingTemplate.rdsQuery({ + statements: list([str(createSql), str(selectSql)]), + }), + ]) + ); - const reqTemplate = print( - compoundExpression([ - set(ref('cols'), list([])), - set(ref('vals'), list([])), - forEach( - ref('entry'), - ref(`ctx.args.create${toUpper(type)}Input.keySet()`), - [ - set(ref('discard'), ref(`cols.add($entry)`)), - set(ref('discard'), ref(`vals.add("'$ctx.args.create${toUpper(type)}Input[$entry]'")`)) - ] - ), - set(ref('valStr'), ref('vals.toString().replace("[","(").replace("]",")")')), - set(ref('colStr'), ref('cols.toString().replace("[","(").replace("]",")")')), - RelationalDBMappingTemplate.rdsQuery({ - statements: list([str(createSql), str(selectSql)]) - }) - ]) - ) + const resTemplate = print(ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])')); - const resTemplate = print( - ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])') - ) + fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); + fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); + let resolver = new AppSync.Resolver({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), + TypeName: mutationTypeName, + FieldName: fieldName, + RequestMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: reqFileName, + }), + ResponseMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: resFileName, + }), + }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]); + return resolver; + } - let resolver = new AppSync.Resolver ({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), - TypeName: mutationTypeName, - FieldName: fieldName, - RequestMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: reqFileName - } - ), - ResponseMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: resFileName - } - ) - }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]) - return resolver + /** + * Creates and Returns the CFN Spec for the 'Get' Resolver Resource provided + * a GraphQL type + * + * @param type - the graphql type for which the get resolver will be created + * @param queryTypeName - will be 'Query' + */ + private makeGetRelationalResolver(type: string, queryTypeName: string = 'Query') { + const fieldName = graphqlName('get' + toUpper(type)); + let sql; + if (this.typePrimaryKeyTypeMap.get(type).includes('String')) { + sql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.${this.typePrimaryKeyMap.get(type)}\'`; + } else { + sql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`; } + const reqFileName = `${queryTypeName}.${fieldName}.req.vtl`; + const resFileName = `${queryTypeName}.${fieldName}.res.vtl`; - /** - * Creates and Returns the CFN Spec for the 'Get' Resolver Resource provided - * a GraphQL type - * - * @param type - the graphql type for which the get resolver will be created - * @param queryTypeName - will be 'Query' - */ - private makeGetRelationalResolver(type: string, queryTypeName: string = 'Query') { - const fieldName = graphqlName('get' + toUpper(type)) - let sql; - if (this.typePrimaryKeyTypeMap.get(type).includes("String")) { - sql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.${this.typePrimaryKeyMap.get(type)}\'` - } else { - sql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}` - } - const reqFileName = `${queryTypeName}.${fieldName}.req.vtl` - const resFileName = `${queryTypeName}.${fieldName}.res.vtl` + const reqTemplate = print( + compoundExpression([ + RelationalDBMappingTemplate.rdsQuery({ + statements: list([str(sql)]), + }), + ]) + ); - const reqTemplate = print( - compoundExpression([ - RelationalDBMappingTemplate.rdsQuery({ - statements: list([str(sql)]) - }) - ]) - ) + const resTemplate = print(ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])')); - const resTemplate = print( - ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])') - ) + fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); + fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); + let resolver = new AppSync.Resolver({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), + FieldName: fieldName, + TypeName: queryTypeName, + RequestMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: reqFileName, + }), + ResponseMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: resFileName, + }), + }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]); + return resolver; + } - let resolver = new AppSync.Resolver ({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), - FieldName: fieldName, - TypeName: queryTypeName, - RequestMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: reqFileName - } - ), - ResponseMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: resFileName - } - ) - }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]) - return resolver + /** + * Creates and Returns the CFN Spec for the 'Update' Resolver Resource provided + * a GraphQL type + * + * @param type - the graphql type for which the update resolver will be created + * @param mutationTypeName - will be 'Mutation' + */ + private makeUpdateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') { + const fieldName = graphqlName('update' + toUpper(type)); + const updateSql = `UPDATE ${type} SET $update WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper( + type + )}Input.${this.typePrimaryKeyMap.get(type)}`; + let selectSql; + if (this.typePrimaryKeyTypeMap.get(type).includes('String')) { + selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.update${toUpper( + type + )}Input.${this.typePrimaryKeyMap.get(type)}\'`; + } else { + selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper( + type + )}Input.${this.typePrimaryKeyMap.get(type)}`; } + const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`; + const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`; - /** - * Creates and Returns the CFN Spec for the 'Update' Resolver Resource provided - * a GraphQL type - * - * @param type - the graphql type for which the update resolver will be created - * @param mutationTypeName - will be 'Mutation' - */ - private makeUpdateRelationalResolver(type: string, mutationTypeName: string = 'Mutation') { - const fieldName = graphqlName('update' + toUpper(type)) - const updateSql = - `UPDATE ${type} SET $update WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}` - let selectSql - if (this.typePrimaryKeyTypeMap.get(type).includes("String")) { - selectSql = - `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.update${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}\'` - } else { - selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.update${toUpper(type)}Input.${this.typePrimaryKeyMap.get(type)}` - } - const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl` - const resFileName = `${mutationTypeName}.${fieldName}.res.vtl` + const reqTemplate = print( + compoundExpression([ + set(ref('updateList'), obj({})), + forEach(ref('entry'), ref(`ctx.args.update${toUpper(type)}Input.keySet()`), [ + set(ref('discard'), ref(`updateList.put($entry, "'$ctx.args.update${toUpper(type)}Input[$entry]'")`)), + ]), + set(ref('update'), ref(`updateList.toString().replace("{","").replace("}","")`)), + RelationalDBMappingTemplate.rdsQuery({ + statements: list([str(updateSql), str(selectSql)]), + }), + ]) + ); - const reqTemplate = print( - compoundExpression([ - set(ref('updateList'), obj({})), - forEach( - ref('entry'), - ref(`ctx.args.update${toUpper(type)}Input.keySet()`), - [ - set(ref('discard'), ref(`updateList.put($entry, "'$ctx.args.update${toUpper(type)}Input[$entry]'")`)) - ] - ), - set(ref('update'), ref(`updateList.toString().replace("{","").replace("}","")`)), - RelationalDBMappingTemplate.rdsQuery({ - statements: list([str(updateSql), str(selectSql)]) - }) - ]) - ) + const resTemplate = print(ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])')); - const resTemplate = print( - ref('utils.toJson($utils.parseJson($utils.rds.toJsonString($ctx.result))[1][0])') - ) + fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); + fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); + let resolver = new AppSync.Resolver({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), + TypeName: mutationTypeName, + FieldName: fieldName, + RequestMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: reqFileName, + }), + ResponseMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: resFileName, + }), + }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]); + return resolver; + } - let resolver = new AppSync.Resolver ({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), - TypeName: mutationTypeName, - FieldName: fieldName, - RequestMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: reqFileName - } - ), - ResponseMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: resFileName - } - ) - }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]) - return resolver + /** + * Creates and Returns the CFN Spec for the 'Delete' Resolver Resource provided + * a GraphQL type + * + * @param type - the graphql type for which the delete resolver will be created + * @param mutationTypeName - will be 'Mutation' + */ + private makeDeleteRelationalResolver(type: string, mutationTypeName: string = 'Mutation') { + const fieldName = graphqlName('delete' + toUpper(type)); + let selectSql; + if (this.typePrimaryKeyTypeMap.get(type).includes('String')) { + selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.${this.typePrimaryKeyMap.get(type)}\'`; + } else { + selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`; } + const deleteSql = `DELETE FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}`; + const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl`; + const resFileName = `${mutationTypeName}.${fieldName}.res.vtl`; + const reqTemplate = print( + compoundExpression([ + RelationalDBMappingTemplate.rdsQuery({ + statements: list([str(selectSql), str(deleteSql)]), + }), + ]) + ); + const resTemplate = print(ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])')); - /** - * Creates and Returns the CFN Spec for the 'Delete' Resolver Resource provided - * a GraphQL type - * - * @param type - the graphql type for which the delete resolver will be created - * @param mutationTypeName - will be 'Mutation' - */ - private makeDeleteRelationalResolver(type: string, mutationTypeName: string = 'Mutation') { - const fieldName = graphqlName('delete' + toUpper(type)) - let selectSql; - if (this.typePrimaryKeyTypeMap.get(type).includes("String")) { - selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=\'$ctx.args.${this.typePrimaryKeyMap.get(type)}\'` - } else { - selectSql = `SELECT * FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}` - } - const deleteSql = `DELETE FROM ${type} WHERE ${this.typePrimaryKeyMap.get(type)}=$ctx.args.${this.typePrimaryKeyMap.get(type)}` - const reqFileName = `${mutationTypeName}.${fieldName}.req.vtl` - const resFileName = `${mutationTypeName}.${fieldName}.res.vtl` - const reqTemplate = print( - compoundExpression([ - RelationalDBMappingTemplate.rdsQuery({ - statements: list([str(selectSql), str(deleteSql)]) - }) - ]) - ) - const resTemplate = print( - ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])') - ) + fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); + fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); + let resolver = new AppSync.Resolver({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), + TypeName: mutationTypeName, + FieldName: fieldName, + RequestMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: reqFileName, + }), + ResponseMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: resFileName, + }), + }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]); - let resolver = new AppSync.Resolver ({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), - TypeName: mutationTypeName, - FieldName: fieldName, - RequestMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: reqFileName - } - ), - ResponseMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: resFileName - } - ) - }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]) + return resolver; + } - return resolver - } - - /** - * Creates and Returns the CFN Spec for the 'List' Resolver Resource provided - * a GraphQL type - * - * @param type - the graphql type for which the list resolver will be created - * @param queryTypeName - will be 'Query' - */ - private makeListRelationalResolver(type: string, queryTypeName: string = 'Query') { - const fieldName = graphqlName('list' + plurality(toUpper(type))) - const sql = `SELECT * FROM ${type}` - const reqFileName = `${queryTypeName}.${fieldName}.req.vtl` - const resFileName = `${queryTypeName}.${fieldName}.res.vtl` - const reqTemplate = print( - RelationalDBMappingTemplate.rdsQuery({ - statements: list([str(sql)]) - }) - ) - const resTemplate = print( - ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0])') - ) + /** + * Creates and Returns the CFN Spec for the 'List' Resolver Resource provided + * a GraphQL type + * + * @param type - the graphql type for which the list resolver will be created + * @param queryTypeName - will be 'Query' + */ + private makeListRelationalResolver(type: string, queryTypeName: string = 'Query') { + const fieldName = graphqlName('list' + plurality(toUpper(type))); + const sql = `SELECT * FROM ${type}`; + const reqFileName = `${queryTypeName}.${fieldName}.req.vtl`; + const resFileName = `${queryTypeName}.${fieldName}.res.vtl`; + const reqTemplate = print( + RelationalDBMappingTemplate.rdsQuery({ + statements: list([str(sql)]), + }) + ); + const resTemplate = print(ref('utils.toJson($utils.rds.toJsonObject($ctx.result)[0])')); - fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); - fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); + fs.writeFileSync(`${this.resolverFilePath}/${reqFileName}`, reqTemplate, 'utf8'); + fs.writeFileSync(`${this.resolverFilePath}/${resFileName}`, resTemplate, 'utf8'); - let resolver = new AppSync.Resolver ({ - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), - TypeName: queryTypeName, - FieldName: fieldName, - RequestMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: reqFileName - } - ), - ResponseMappingTemplateS3Location: Fn.Sub( - s3BaseUrl, - { - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - [resolverFileName]: resFileName - } - ) - }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]) + let resolver = new AppSync.Resolver({ + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + DataSourceName: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseDataSource, 'Name'), + TypeName: queryTypeName, + FieldName: fieldName, + RequestMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: reqFileName, + }), + ResponseMappingTemplateS3Location: Fn.Sub(s3BaseUrl, { + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + [resolverFileName]: resFileName, + }), + }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseDataSource]); - return resolver - } -} \ No newline at end of file + return resolver; + } +} diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformer.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformer.ts index 2020248ef1..f826068c1f 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformer.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformer.ts @@ -1,10 +1,17 @@ -import { Kind, ObjectTypeDefinitionNode, SchemaDefinitionNode, - InputObjectTypeDefinitionNode, DocumentNode} from 'graphql' -import { getNamedType, getOperationFieldDefinition, getNonNullType, getInputValueDefinition, - getTypeDefinition, getFieldDefinition, getDirectiveNode, getOperationTypeDefinition } from './RelationalDBSchemaTransformerUtils' -import {RelationalDBParsingException} from './RelationalDBParsingException' +import { Kind, ObjectTypeDefinitionNode, SchemaDefinitionNode, InputObjectTypeDefinitionNode, DocumentNode } from 'graphql'; +import { + getNamedType, + getOperationFieldDefinition, + getNonNullType, + getInputValueDefinition, + getTypeDefinition, + getFieldDefinition, + getDirectiveNode, + getOperationTypeDefinition, +} from './RelationalDBSchemaTransformerUtils'; +import { RelationalDBParsingException } from './RelationalDBParsingException'; import { IRelationalDBReader } from './IRelationalDBReader'; -import { toUpper } from 'graphql-transformer-common' +import { toUpper } from 'graphql-transformer-common'; /** * This class is used to transition all of the columns and key metadata from a table for use @@ -12,25 +19,31 @@ import { toUpper } from 'graphql-transformer-common' * the base table, update mutation inputs, create mutation inputs, and primary key metadata. */ export class TableContext { - tableTypeDefinition: ObjectTypeDefinitionNode - createTypeDefinition: InputObjectTypeDefinitionNode - updateTypeDefinition: InputObjectTypeDefinitionNode - // Table primary key metadata, to help properly key queries and mutations. - tableKeyField: string - tableKeyFieldType: string - stringFieldList: string[] + tableTypeDefinition: ObjectTypeDefinitionNode; + createTypeDefinition: InputObjectTypeDefinitionNode; + updateTypeDefinition: InputObjectTypeDefinitionNode; + // Table primary key metadata, to help properly key queries and mutations. + tableKeyField: string; + tableKeyFieldType: string; + stringFieldList: string[]; + intFieldList: string[]; + constructor( + typeDefinition: ObjectTypeDefinitionNode, + createDefinition: InputObjectTypeDefinitionNode, + updateDefinition: InputObjectTypeDefinitionNode, + primaryKeyField: string, + primaryKeyType: string, + stringFieldList: string[], intFieldList: string[] - constructor(typeDefinition: ObjectTypeDefinitionNode, createDefinition: InputObjectTypeDefinitionNode, - updateDefinition: InputObjectTypeDefinitionNode, primaryKeyField: string, primaryKeyType: string, - stringFieldList: string[], intFieldList: string[]) { - this.tableTypeDefinition = typeDefinition - this.tableKeyField = primaryKeyField - this.createTypeDefinition = createDefinition - this.updateTypeDefinition = updateDefinition - this.tableKeyFieldType = primaryKeyType - this.stringFieldList = stringFieldList - this.intFieldList = intFieldList - } + ) { + this.tableTypeDefinition = typeDefinition; + this.tableKeyField = primaryKeyField; + this.createTypeDefinition = createDefinition; + this.updateTypeDefinition = updateDefinition; + this.tableKeyFieldType = primaryKeyType; + this.stringFieldList = stringFieldList; + this.intFieldList = intFieldList; + } } /** @@ -41,210 +54,215 @@ export class TableContext { * for DataSource Creation, as data source creation is apart of the cfn template generation. */ export default class TemplateContext { - schemaDoc: DocumentNode - typePrimaryKeyMap: Map - typePrimaryKeyTypeMap: Map - stringFieldMap: Map - intFieldMap: Map - secretStoreArn: string - rdsClusterIdentifier: string - databaseName: string - databaseSchema: string - region: string - - constructor(schemaDoc: DocumentNode, typePrimaryKeyMap: Map, - stringFieldMap: Map, intFieldMap: Map, - typePrimaryKeyTypeMap?: Map) { - this.schemaDoc = schemaDoc - this.typePrimaryKeyMap = typePrimaryKeyMap - this.stringFieldMap = stringFieldMap - this.intFieldMap = intFieldMap - this.typePrimaryKeyTypeMap = typePrimaryKeyTypeMap - } + schemaDoc: DocumentNode; + typePrimaryKeyMap: Map; + typePrimaryKeyTypeMap: Map; + stringFieldMap: Map; + intFieldMap: Map; + secretStoreArn: string; + rdsClusterIdentifier: string; + databaseName: string; + databaseSchema: string; + region: string; + + constructor( + schemaDoc: DocumentNode, + typePrimaryKeyMap: Map, + stringFieldMap: Map, + intFieldMap: Map, + typePrimaryKeyTypeMap?: Map + ) { + this.schemaDoc = schemaDoc; + this.typePrimaryKeyMap = typePrimaryKeyMap; + this.stringFieldMap = stringFieldMap; + this.intFieldMap = intFieldMap; + this.typePrimaryKeyTypeMap = typePrimaryKeyTypeMap; + } } export class RelationalDBSchemaTransformer { - dbReader: IRelationalDBReader - database: string + dbReader: IRelationalDBReader; + database: string; - constructor(dbReader: IRelationalDBReader, database: string) { - this.dbReader = dbReader - this.database = database - } + constructor(dbReader: IRelationalDBReader, database: string) { + this.dbReader = dbReader; + this.database = database; + } - public introspectDatabaseSchema = async (): Promise => { - - - // Get all of the tables within the provided db - let tableNames = null - try { - tableNames = await this.dbReader.listTables() - } catch (err) { - throw new RelationalDBParsingException(`Failed to list tables in ${this.database}`, err.stack) - } - - let typeContexts = new Array() - let types = new Array() - let pkeyMap = new Map() - let pkeyTypeMap = new Map() - let stringFieldMap = new Map() - let intFieldMap = new Map() - - for (const tableName of tableNames) { - let type: TableContext = null - try { - type = await this.dbReader.describeTable(tableName) - } catch (err) { - throw new RelationalDBParsingException(`Failed to describe table ${tableName}`, err.stack) - } - - // NOTE from @mikeparisstuff. The GraphQL schema generation breaks - // when the table does not have an explicit primary key. - if (type.tableKeyField) { - typeContexts.push(type) - // Generate the 'connection' type for each table type definition - // TODO: Determine if Connection is needed as Data API doesn't provide pagination - // TODO: As we add different db sources, we should conditionally do this even if we don't for Aurora serverless. - //types.push(this.getConnectionType(tableName)) - // Generate the create operation input for each table type definition - types.push(type.createTypeDefinition) - // Generate the default shape for the table's structure - types.push(type.tableTypeDefinition) - // Generate the update operation input for each table type definition - types.push(type.updateTypeDefinition) - - // Update the field map with the new field lists for the current table - stringFieldMap.set(tableName, type.stringFieldList) - intFieldMap.set(tableName, type.intFieldList) - pkeyMap.set(tableName, type.tableKeyField) - pkeyTypeMap.set(tableName, type.tableKeyFieldType) - } else { - console.warn(`Skipping table ${type.tableTypeDefinition.name.value} because it does not have a single PRIMARY KEY.`) - } - } - - // Generate the mutations and queries based on the table structures - types.push(this.getMutations(typeContexts)) - types.push(this.getQueries(typeContexts)) - types.push(this.getSubscriptions(typeContexts)) - types.push(this.getSchemaType()) - - let context = this.dbReader.hydrateTemplateContext(new TemplateContext({kind: Kind.DOCUMENT, - definitions: types}, pkeyMap, stringFieldMap, intFieldMap, pkeyTypeMap)) - return context + public introspectDatabaseSchema = async (): Promise => { + // Get all of the tables within the provided db + let tableNames = null; + try { + tableNames = await this.dbReader.listTables(); + } catch (err) { + throw new RelationalDBParsingException(`Failed to list tables in ${this.database}`, err.stack); } - /** - * Creates a schema type definition node, including operations for each of query, mutation, and subscriptions. - * - * @returns a basic schema definition node. - */ - getSchemaType(): SchemaDefinitionNode { - return { - kind: Kind.SCHEMA_DEFINITION, - directives: [], - operationTypes: [ - getOperationTypeDefinition('query', getNamedType('Query')), - getOperationTypeDefinition('mutation', getNamedType('Mutation')), - getOperationTypeDefinition('subscription', getNamedType('Subscription')) - ] - } - } + let typeContexts = new Array(); + let types = new Array(); + let pkeyMap = new Map(); + let pkeyTypeMap = new Map(); + let stringFieldMap = new Map(); + let intFieldMap = new Map(); + + for (const tableName of tableNames) { + let type: TableContext = null; + try { + type = await this.dbReader.describeTable(tableName); + } catch (err) { + throw new RelationalDBParsingException(`Failed to describe table ${tableName}`, err.stack); + } - /** - * Generates the basic mutation operations, given the provided table contexts. This will - * create a create, delete, and update operation for each table. - * - * @param types the table contexts from which the mutations are to be generated. - * @returns the type definition for mutations, including a create, delete, and update for each table. - */ - private getMutations(types: TableContext[]): ObjectTypeDefinitionNode { - const fields = [] - for (const typeContext of types) { - const type = typeContext.tableTypeDefinition - const formattedTypeValue = toUpper(type.name.value) - fields.push( - getOperationFieldDefinition(`delete${formattedTypeValue}`, - [getInputValueDefinition(getNonNullType(getNamedType(typeContext.tableKeyFieldType)), - typeContext.tableKeyField)], - getNamedType(`${type.name.value}`), null) - ) - fields.push( - getOperationFieldDefinition(`create${formattedTypeValue}`, - [getInputValueDefinition(getNonNullType(getNamedType(`Create${formattedTypeValue}Input`)), - `create${formattedTypeValue}Input`)], - getNamedType(`${type.name.value}`), null) - ) - fields.push( - getOperationFieldDefinition(`update${formattedTypeValue}`, - [getInputValueDefinition(getNonNullType(getNamedType(`Update${formattedTypeValue}Input`)), - `update${formattedTypeValue}Input`)], - getNamedType(`${type.name.value}`), null) - ) - } - return getTypeDefinition(fields, 'Mutation') + // NOTE from @mikeparisstuff. The GraphQL schema generation breaks + // when the table does not have an explicit primary key. + if (type.tableKeyField) { + typeContexts.push(type); + // Generate the 'connection' type for each table type definition + // TODO: Determine if Connection is needed as Data API doesn't provide pagination + // TODO: As we add different db sources, we should conditionally do this even if we don't for Aurora serverless. + //types.push(this.getConnectionType(tableName)) + // Generate the create operation input for each table type definition + types.push(type.createTypeDefinition); + // Generate the default shape for the table's structure + types.push(type.tableTypeDefinition); + // Generate the update operation input for each table type definition + types.push(type.updateTypeDefinition); + + // Update the field map with the new field lists for the current table + stringFieldMap.set(tableName, type.stringFieldList); + intFieldMap.set(tableName, type.intFieldList); + pkeyMap.set(tableName, type.tableKeyField); + pkeyTypeMap.set(tableName, type.tableKeyFieldType); + } else { + console.warn(`Skipping table ${type.tableTypeDefinition.name.value} because it does not have a single PRIMARY KEY.`); + } } - /** - * Generates the basic subscription operations, given the provided table contexts. This will - * create an onCreate subscription for each table. - * - * @param types the table contexts from which the subscriptions are to be generated. - * @returns the type definition for subscriptions, including an onCreate for each table. - */ - private getSubscriptions(types: TableContext[]): ObjectTypeDefinitionNode { - const fields = [] - for (const typeContext of types) { - const type = typeContext.tableTypeDefinition - const formattedTypeValue = toUpper(type.name.value) - fields.push( - getOperationFieldDefinition(`onCreate${formattedTypeValue}`, [], - getNamedType(`${type.name.value}`), - [getDirectiveNode(`create${formattedTypeValue}`)]) - ) - } - return getTypeDefinition(fields, 'Subscription') + // Generate the mutations and queries based on the table structures + types.push(this.getMutations(typeContexts)); + types.push(this.getQueries(typeContexts)); + types.push(this.getSubscriptions(typeContexts)); + types.push(this.getSchemaType()); + + let context = this.dbReader.hydrateTemplateContext( + new TemplateContext({ kind: Kind.DOCUMENT, definitions: types }, pkeyMap, stringFieldMap, intFieldMap, pkeyTypeMap) + ); + return context; + }; + + /** + * Creates a schema type definition node, including operations for each of query, mutation, and subscriptions. + * + * @returns a basic schema definition node. + */ + getSchemaType(): SchemaDefinitionNode { + return { + kind: Kind.SCHEMA_DEFINITION, + directives: [], + operationTypes: [ + getOperationTypeDefinition('query', getNamedType('Query')), + getOperationTypeDefinition('mutation', getNamedType('Mutation')), + getOperationTypeDefinition('subscription', getNamedType('Subscription')), + ], + }; + } + + /** + * Generates the basic mutation operations, given the provided table contexts. This will + * create a create, delete, and update operation for each table. + * + * @param types the table contexts from which the mutations are to be generated. + * @returns the type definition for mutations, including a create, delete, and update for each table. + */ + private getMutations(types: TableContext[]): ObjectTypeDefinitionNode { + const fields = []; + for (const typeContext of types) { + const type = typeContext.tableTypeDefinition; + const formattedTypeValue = toUpper(type.name.value); + fields.push( + getOperationFieldDefinition( + `delete${formattedTypeValue}`, + [getInputValueDefinition(getNonNullType(getNamedType(typeContext.tableKeyFieldType)), typeContext.tableKeyField)], + getNamedType(`${type.name.value}`), + null + ) + ); + fields.push( + getOperationFieldDefinition( + `create${formattedTypeValue}`, + [getInputValueDefinition(getNonNullType(getNamedType(`Create${formattedTypeValue}Input`)), `create${formattedTypeValue}Input`)], + getNamedType(`${type.name.value}`), + null + ) + ); + fields.push( + getOperationFieldDefinition( + `update${formattedTypeValue}`, + [getInputValueDefinition(getNonNullType(getNamedType(`Update${formattedTypeValue}Input`)), `update${formattedTypeValue}Input`)], + getNamedType(`${type.name.value}`), + null + ) + ); } + return getTypeDefinition(fields, 'Mutation'); + } - /** - * Generates the basic query operations, given the provided table contexts. This will - * create a get and list operation for each table. - * - * @param types the table contexts from which the queries are to be generated. - * @returns the type definition for queries, including a get and list for each table. - */ - private getQueries(types: TableContext[]): ObjectTypeDefinitionNode { - const fields = [] - for (const typeContext of types) { - const type = typeContext.tableTypeDefinition - const formattedTypeValue = toUpper(type.name.value) - fields.push( - getOperationFieldDefinition(`get${formattedTypeValue}`, - [getInputValueDefinition(getNonNullType(getNamedType(typeContext.tableKeyFieldType)), - typeContext.tableKeyField)], - getNamedType(`${type.name.value}`), null) - ) - fields.push( - getOperationFieldDefinition(`list${formattedTypeValue}s`, - [], - getNamedType(`[${type.name.value}]`), null) - ) - } - return getTypeDefinition(fields, 'Query') + /** + * Generates the basic subscription operations, given the provided table contexts. This will + * create an onCreate subscription for each table. + * + * @param types the table contexts from which the subscriptions are to be generated. + * @returns the type definition for subscriptions, including an onCreate for each table. + */ + private getSubscriptions(types: TableContext[]): ObjectTypeDefinitionNode { + const fields = []; + for (const typeContext of types) { + const type = typeContext.tableTypeDefinition; + const formattedTypeValue = toUpper(type.name.value); + fields.push( + getOperationFieldDefinition(`onCreate${formattedTypeValue}`, [], getNamedType(`${type.name.value}`), [ + getDirectiveNode(`create${formattedTypeValue}`), + ]) + ); } + return getTypeDefinition(fields, 'Subscription'); + } - /** - * Creates a GraphQL connection type for a given GraphQL type, corresponding to a SQL table name. - * - * @param tableName the name of the SQL table (and GraphQL type). - * @returns a type definition node defining the connection type for the provided type name. - */ - getConnectionType(tableName: string): ObjectTypeDefinitionNode { - return getTypeDefinition( - [ - getFieldDefinition('items', getNamedType(`[${tableName}]`)), - getFieldDefinition('nextToken', getNamedType('String')) - ], - `${tableName}Connection`) + /** + * Generates the basic query operations, given the provided table contexts. This will + * create a get and list operation for each table. + * + * @param types the table contexts from which the queries are to be generated. + * @returns the type definition for queries, including a get and list for each table. + */ + private getQueries(types: TableContext[]): ObjectTypeDefinitionNode { + const fields = []; + for (const typeContext of types) { + const type = typeContext.tableTypeDefinition; + const formattedTypeValue = toUpper(type.name.value); + fields.push( + getOperationFieldDefinition( + `get${formattedTypeValue}`, + [getInputValueDefinition(getNonNullType(getNamedType(typeContext.tableKeyFieldType)), typeContext.tableKeyField)], + getNamedType(`${type.name.value}`), + null + ) + ); + fields.push(getOperationFieldDefinition(`list${formattedTypeValue}s`, [], getNamedType(`[${type.name.value}]`), null)); } + return getTypeDefinition(fields, 'Query'); + } + + /** + * Creates a GraphQL connection type for a given GraphQL type, corresponding to a SQL table name. + * + * @param tableName the name of the SQL table (and GraphQL type). + * @returns a type definition node defining the connection type for the provided type name. + */ + getConnectionType(tableName: string): ObjectTypeDefinitionNode { + return getTypeDefinition( + [getFieldDefinition('items', getNamedType(`[${tableName}]`)), getFieldDefinition('nextToken', getNamedType('String'))], + `${tableName}Connection` + ); + } } diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformerUtils.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformerUtils.ts index 2f8d0ba815..ad6e5d5379 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformerUtils.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBSchemaTransformerUtils.ts @@ -1,241 +1,261 @@ -import { Kind, print, ObjectTypeDefinitionNode, NonNullTypeNode, DirectiveNode, NameNode, - OperationTypeNode, FieldDefinitionNode, NamedTypeNode, InputValueDefinitionNode, ValueNode, - OperationTypeDefinitionNode, SchemaDefinitionNode, ArgumentNode, ListValueNode, StringValueNode, - InputObjectTypeDefinitionNode, DocumentNode} from 'graphql' - -const intTypes = [`INTEGER`, `INT`, `SMALLINT`, `TINYINT`, `MEDIUMINT`, `BIGINT`, `BIT`] -const floatTypes = [`FLOAT`, `DOUBLE`, `REAL`, `REAL_AS_FLOAT`, `DOUBLE PRECISION`, `DEC`, `DECIMAL`, `FIXED`, `NUMERIC`] +import { + Kind, + print, + ObjectTypeDefinitionNode, + NonNullTypeNode, + DirectiveNode, + NameNode, + OperationTypeNode, + FieldDefinitionNode, + NamedTypeNode, + InputValueDefinitionNode, + ValueNode, + OperationTypeDefinitionNode, + SchemaDefinitionNode, + ArgumentNode, + ListValueNode, + StringValueNode, + InputObjectTypeDefinitionNode, + DocumentNode, +} from 'graphql'; +const intTypes = [`INTEGER`, `INT`, `SMALLINT`, `TINYINT`, `MEDIUMINT`, `BIGINT`, `BIT`]; +const floatTypes = [`FLOAT`, `DOUBLE`, `REAL`, `REAL_AS_FLOAT`, `DOUBLE PRECISION`, `DEC`, `DECIMAL`, `FIXED`, `NUMERIC`]; /** * Creates a non-null type, which is a node wrapped around another type that simply defines it is non-nullable. - * + * * @param typeNode the type to be marked as non-nullable. * @returns a non-null wrapper around the provided type. */ - export function getNonNullType(typeNode: NamedTypeNode): NonNullTypeNode { - return { - kind: Kind.NON_NULL_TYPE, - type: typeNode - } +export function getNonNullType(typeNode: NamedTypeNode): NonNullTypeNode { + return { + kind: Kind.NON_NULL_TYPE, + type: typeNode, + }; } /** * Creates a named type for the schema. - * + * * @param name the name of the type. * @returns a named type with the provided name. */ export function getNamedType(name: string): NamedTypeNode { - return { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: name - } - } + return { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: name, + }, + }; } /** * Creates an input value definition for the schema. - * + * * @param typeNode the type of the input node. * @param name the name of the input. * @returns an input value definition node with the provided type and name. */ export function getInputValueDefinition(typeNode: NamedTypeNode | NonNullTypeNode, name: string): InputValueDefinitionNode { - return { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: Kind.NAME, - value: name - }, - type: typeNode - } + return { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: Kind.NAME, + value: name, + }, + type: typeNode, + }; } /** * Creates an operation field definition for the schema. - * + * * @param name the name of the operation. * @param args the arguments for the operation. * @param type the type of the operation. * @param directives the directives (if any) applied to this field. In this context, only subscriptions will have this. * @returns an operation field definition with the provided name, args, type, and optionally directives. */ -export function getOperationFieldDefinition(name: string, args: InputValueDefinitionNode[], type: NamedTypeNode, directives: ReadonlyArray): FieldDefinitionNode { - return { - kind: Kind.FIELD_DEFINITION, - name: { - kind: Kind.NAME, - value: name - }, - arguments: args, - type: type, - directives: directives - } +export function getOperationFieldDefinition( + name: string, + args: InputValueDefinitionNode[], + type: NamedTypeNode, + directives: ReadonlyArray +): FieldDefinitionNode { + return { + kind: Kind.FIELD_DEFINITION, + name: { + kind: Kind.NAME, + value: name, + }, + arguments: args, + type: type, + directives: directives, + }; } /** * Creates a field definition node for the schema. - * + * * @param fieldName the name of the field to be created. * @param type the type of the field to be created. * @returns a field definition node with the provided name and type. */ export function getFieldDefinition(fieldName: string, type: NonNullTypeNode | NamedTypeNode): FieldDefinitionNode { - return { - kind: Kind.FIELD_DEFINITION, - name: { - kind: Kind.NAME, - value: fieldName - }, - type - } + return { + kind: Kind.FIELD_DEFINITION, + name: { + kind: Kind.NAME, + value: fieldName, + }, + type, + }; } /** * Creates a type definition node for the schema. - * + * * @param fields the field set to be included in the type. * @param typeName the name of the type. * @returns a type definition node defined by the provided fields and name. */ export function getTypeDefinition(fields: ReadonlyArray, typeName: string): ObjectTypeDefinitionNode { - return { - kind: Kind.OBJECT_TYPE_DEFINITION, - name: { - kind: Kind.NAME, - value: typeName - }, - fields: fields - } + return { + kind: Kind.OBJECT_TYPE_DEFINITION, + name: { + kind: Kind.NAME, + value: typeName, + }, + fields: fields, + }; } /** * Creates an input type definition node for the schema. - * + * * @param fields the fields in the input type. * @param typeName the name of the input type * @returns an input type definition node defined by the provided fields and */ export function getInputTypeDefinition(fields: ReadonlyArray, typeName: string): InputObjectTypeDefinitionNode { - return { - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, - name: { - kind: Kind.NAME, - value: typeName - }, - fields: fields - } + return { + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, + name: { + kind: Kind.NAME, + value: typeName, + }, + fields: fields, + }; } /** * Creates a name node for the schema. - * + * * @param name the name of the name node. * @returns the name node defined by the provided name. */ export function getNameNode(name: string): NameNode { - return { - kind: Kind.NAME, - value: name - } + return { + kind: Kind.NAME, + value: name, + }; } /** * Creates a list value node for the schema. - * + * * @param values the list of values to be in the list node. * @returns a list value node containing the provided values. */ export function getListValueNode(values: ReadonlyArray): ListValueNode { - return { - kind: Kind.LIST, - values: values - } + return { + kind: Kind.LIST, + values: values, + }; } /** * Creates a simple string value node for the schema. - * + * * @param value the value to be set in the string value node. * @returns a fleshed-out string value node. */ export function getStringValueNode(value: string): StringValueNode { - return { - kind: Kind.STRING, - value: value - } -} + return { + kind: Kind.STRING, + value: value, + }; +} /** * Creates a directive node for a subscription in the schema. - * + * * @param mutationName the name of the mutation the subscription directive is for. * @returns a directive node defining the subscription. */ export function getDirectiveNode(mutationName: string): DirectiveNode { - return { - kind: Kind.DIRECTIVE, - name: this.getNameNode('aws_subscribe'), - arguments: [this.getArgumentNode(mutationName)] - } + return { + kind: Kind.DIRECTIVE, + name: this.getNameNode('aws_subscribe'), + arguments: [this.getArgumentNode(mutationName)], + }; } /** * Creates an operation type definition (subscription, query, mutation) for the schema. - * + * * @param operationType the type node defining the operation type. * @param operation the named type node defining the operation type. */ export function getOperationTypeDefinition(operationType: OperationTypeNode, operation: NamedTypeNode): OperationTypeDefinitionNode { - return { - kind: Kind.OPERATION_TYPE_DEFINITION, - operation: operationType, - type: operation - } + return { + kind: Kind.OPERATION_TYPE_DEFINITION, + operation: operationType, + type: operation, + }; } /** * Creates an argument node for a subscription directive within the schema. - * + * * @param argument the argument string. * @returns the argument node. */ export function getArgumentNode(argument: string): ArgumentNode { - return { - kind: Kind.ARGUMENT, - name: this.getNameNode('mutations'), - value: this.getListValueNode([this.getStringValueNode(argument)]) - } + return { + kind: Kind.ARGUMENT, + name: this.getNameNode('mutations'), + value: this.getListValueNode([this.getStringValueNode(argument)]), + }; } /** * Given the DB type for a column, make a best effort to select the appropriate GraphQL type for * the corresponding field. - * + * * @param dbType the SQL column type. * @returns the GraphQL field type. */ export function getGraphQLTypeFromMySQLType(dbType: string): string { - const normalizedType = dbType.toUpperCase().split("(")[0] - if (`BOOL` == normalizedType) { - return `Boolean` - } else if (`JSON` == normalizedType) { - return `AWSJSON` - } else if (`TIME` == normalizedType) { - return `AWSTime` - } else if (`DATE` == normalizedType) { - return `AWSDate` - } else if (`DATETIME` == normalizedType) { - return `AWSDateTime` - } else if (`TIMESTAMP` == normalizedType) { - return `AWSTimestamp` - } else if (intTypes.indexOf(normalizedType) > -1) { - return `Int` - } else if (floatTypes.indexOf(normalizedType) > -1) { - return `Float` - } - return `String` -} \ No newline at end of file + const normalizedType = dbType.toUpperCase().split('(')[0]; + if (`BOOL` == normalizedType) { + return `Boolean`; + } else if (`JSON` == normalizedType) { + return `AWSJSON`; + } else if (`TIME` == normalizedType) { + return `AWSTime`; + } else if (`DATE` == normalizedType) { + return `AWSDate`; + } else if (`DATETIME` == normalizedType) { + return `AWSDateTime`; + } else if (`TIMESTAMP` == normalizedType) { + return `AWSTimestamp`; + } else if (intTypes.indexOf(normalizedType) > -1) { + return `Int`; + } else if (floatTypes.indexOf(normalizedType) > -1) { + return `Float`; + } + return `String`; +} diff --git a/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts b/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts index e1ac0dad8e..cfd49aef05 100644 --- a/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts +++ b/packages/graphql-relational-schema-transformer/src/RelationalDBTemplateGenerator.ts @@ -1,11 +1,11 @@ -import { ResourceConstants } from './ResourceConstants' -import DataSource from 'cloudform-types/types/appSync/dataSource' -import IAM from 'cloudform-types/types/iam' +import { ResourceConstants } from './ResourceConstants'; +import DataSource from 'cloudform-types/types/appSync/dataSource'; +import IAM from 'cloudform-types/types/iam'; -import cloudform, { Fn, StringParameter, Refs } from 'cloudform' -import Template from 'cloudform-types/types/template' -import TemplateContext from './RelationalDBSchemaTransformer' -import RelationalDBResolverGenerator from './RelationalDBResolverGenerator' +import cloudform, { Fn, StringParameter, Refs } from 'cloudform'; +import Template from 'cloudform-types/types/template'; +import TemplateContext from './RelationalDBSchemaTransformer'; +import RelationalDBResolverGenerator from './RelationalDBResolverGenerator'; /** * This is the Class responsible for generating and managing the CloudForm template @@ -15,186 +15,176 @@ import RelationalDBResolverGenerator from './RelationalDBResolverGenerator' * RDS DataSource provisioned. It also allows for adding the CRUDL+Q Resolvers upon need. */ export default class RelationalDBTemplateGenerator { - context: TemplateContext + context: TemplateContext; - constructor(context: TemplateContext) { - this.context = context - } + constructor(context: TemplateContext) { + this.context = context; + } - /** - * Creates and returns the basic Cloudform template needed for setting - * up an AppSync API pointing at the RDS DataSource. - * - * @returns the created CloudFormation template. - */ - public createTemplate(context: any): Template { - const template = { - AWSTemplateFormatVersion: "2010-09-09", - Parameters: this.makeParameters(this.context.databaseName), - Resources: { - [ResourceConstants.RESOURCES.RelationalDatabaseDataSource]: this.makeRelationalDataSource(context), - [ResourceConstants.RESOURCES.RelationalDatabaseAccessRole]: this.makeIAMDataSourceRole() - } - } + /** + * Creates and returns the basic Cloudform template needed for setting + * up an AppSync API pointing at the RDS DataSource. + * + * @returns the created CloudFormation template. + */ + public createTemplate(context: any): Template { + const template = { + AWSTemplateFormatVersion: '2010-09-09', + Parameters: this.makeParameters(this.context.databaseName), + Resources: { + [ResourceConstants.RESOURCES.RelationalDatabaseDataSource]: this.makeRelationalDataSource(context), + [ResourceConstants.RESOURCES.RelationalDatabaseAccessRole]: this.makeIAMDataSourceRole(), + }, + }; - return template - } + return template; + } - /** - * Provided a Cloudform Template, this method adds Resolver Resources to the - * Template. - * - * @param template - the Cloudform template - * @returns the given template, updated with new resolvers. - */ - public addRelationalResolvers(template: Template, resolverFilePath: string) : Template { - let resolverGenerator = new RelationalDBResolverGenerator(this.context) - template.Resources = {...template.Resources, ...resolverGenerator.createRelationalResolvers(resolverFilePath)} - return template - } + /** + * Provided a Cloudform Template, this method adds Resolver Resources to the + * Template. + * + * @param template - the Cloudform template + * @returns the given template, updated with new resolvers. + */ + public addRelationalResolvers(template: Template, resolverFilePath: string): Template { + let resolverGenerator = new RelationalDBResolverGenerator(this.context); + template.Resources = { ...template.Resources, ...resolverGenerator.createRelationalResolvers(resolverFilePath) }; + return template; + } - /** - * Provided a Cloudform Template, this method returns the cfn json template as a string - * - * @param template - the Cloudform template - * @returns the json, string form of the template given. - */ - public printCloudformationTemplate(template: Template): string { - return cloudform(template) - } + /** + * Provided a Cloudform Template, this method returns the cfn json template as a string + * + * @param template - the Cloudform template + * @returns the json, string form of the template given. + */ + public printCloudformationTemplate(template: Template): string { + return cloudform(template); + } - /* - * Private Helper Methods for Generating the Necessary CFN Specs for the CFN Template - */ + /* + * Private Helper Methods for Generating the Necessary CFN Specs for the CFN Template + */ - /** - * Creates any Parmaters needed for the CFN Template - * - * @param databaseName - the name of the database being parsed. - * @returns the parameters for the template. - */ - private makeParameters(databaseName: string) { - return { - [ResourceConstants.PARAMETERS.AppSyncApiName]: new StringParameter({ - Description: `The name of the AppSync API generated from database ${databaseName}`, - Default: `AppSyncSimpleTransform` - }), - [ResourceConstants.PARAMETERS.Env]: new StringParameter({ - Description: 'The environment name. e.g. Dev, Test, or Production', - Default: 'NONE' - }), - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: new StringParameter({ - Description: 'The S3 bucket containing all deployment assets for the project.' - }), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: new StringParameter({ - Description: 'An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory.' - }), - [ResourceConstants.PARAMETERS.AppSyncApiId]: new StringParameter({ - Description: 'The id of the AppSync API associated with this project.' - }), - [ResourceConstants.PARAMETERS.rdsRegion]: new StringParameter({ - Description: 'The region that the RDS Cluster is located in.' - }), - [ResourceConstants.PARAMETERS.rdsClusterIdentifier]: new StringParameter({ - Description: 'The ARN identifier denoting the RDS cluster.' - }), - [ResourceConstants.PARAMETERS.rdsSecretStoreArn]: new StringParameter({ - Description: 'The ARN for the Secret containing the access for the RDS cluster.' - }), - [ResourceConstants.PARAMETERS.rdsDatabaseName]: new StringParameter({ - Description: 'The name of the database within the RDS cluster to use.' - }) - } - } + /** + * Creates any Parmaters needed for the CFN Template + * + * @param databaseName - the name of the database being parsed. + * @returns the parameters for the template. + */ + private makeParameters(databaseName: string) { + return { + [ResourceConstants.PARAMETERS.AppSyncApiName]: new StringParameter({ + Description: `The name of the AppSync API generated from database ${databaseName}`, + Default: `AppSyncSimpleTransform`, + }), + [ResourceConstants.PARAMETERS.Env]: new StringParameter({ + Description: 'The environment name. e.g. Dev, Test, or Production', + Default: 'NONE', + }), + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: new StringParameter({ + Description: 'The S3 bucket containing all deployment assets for the project.', + }), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: new StringParameter({ + Description: 'An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory.', + }), + [ResourceConstants.PARAMETERS.AppSyncApiId]: new StringParameter({ + Description: 'The id of the AppSync API associated with this project.', + }), + [ResourceConstants.PARAMETERS.rdsRegion]: new StringParameter({ + Description: 'The region that the RDS Cluster is located in.', + }), + [ResourceConstants.PARAMETERS.rdsClusterIdentifier]: new StringParameter({ + Description: 'The ARN identifier denoting the RDS cluster.', + }), + [ResourceConstants.PARAMETERS.rdsSecretStoreArn]: new StringParameter({ + Description: 'The ARN for the Secret containing the access for the RDS cluster.', + }), + [ResourceConstants.PARAMETERS.rdsDatabaseName]: new StringParameter({ + Description: 'The name of the database within the RDS cluster to use.', + }), + }; + } - /* - * Resources - */ + /* + * Resources + */ - /** - * Creates the IAM Role CFN Spec to allow AppSync to interact with the RDS cluster - * - * @returns the IAM role CloudFormation resource. - */ - private makeIAMDataSourceRole() { - return new IAM.Role ({ - RoleName: Fn.Join('-', [ - 'role', - Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - Fn.Ref(ResourceConstants.PARAMETERS.Env) - ]), + /** + * Creates the IAM Role CFN Spec to allow AppSync to interact with the RDS cluster + * + * @returns the IAM role CloudFormation resource. + */ + private makeIAMDataSourceRole() { + return new IAM.Role({ + RoleName: Fn.Join('-', ['role', Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), Fn.Ref(ResourceConstants.PARAMETERS.Env)]), - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'appsync.amazonaws.com' - }, - Action: 'sts:AssumeRole' - } - ] + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'appsync.amazonaws.com', }, - Policies: [ - new IAM.Role.Policy ({ - PolicyName: 'RelationalDatabaseAccessPolicy', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'rds-data:ExecuteSql', - 'rds-data:ExecuteStatement', - 'rds-data:DeleteItems', - 'rds-data:GetItems', - 'rds-data:InsertItems', - 'rds-data:UpdateItems' - ], - Resource: [ - Fn.Ref(ResourceConstants.PARAMETERS.rdsClusterIdentifier) - ] - }, - { - Effect: 'Allow', - Action: [ - 'secretsmanager:GetSecretValue' - ], - Resource: [ - Fn.Ref(ResourceConstants.PARAMETERS.rdsSecretStoreArn) - ] - } - ] - } - }) - ] - }) - } + Action: 'sts:AssumeRole', + }, + ], + }, + Policies: [ + new IAM.Role.Policy({ + PolicyName: 'RelationalDatabaseAccessPolicy', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: [ + 'rds-data:ExecuteSql', + 'rds-data:ExecuteStatement', + 'rds-data:DeleteItems', + 'rds-data:GetItems', + 'rds-data:InsertItems', + 'rds-data:UpdateItems', + ], + Resource: [Fn.Ref(ResourceConstants.PARAMETERS.rdsClusterIdentifier)], + }, + { + Effect: 'Allow', + Action: ['secretsmanager:GetSecretValue'], + Resource: [Fn.Ref(ResourceConstants.PARAMETERS.rdsSecretStoreArn)], + }, + ], + }, + }), + ], + }); + } - /** - * Creates the AppSync DataSource CFN Spec pointing at the provided RDS Cluster - * - * @param cliContext - the Amplify context, used to load environment variables. - * @returns the data source CloudFormation resource. - */ - private makeRelationalDataSource(cliContext: any): DataSource { - return new DataSource ({ - Type: 'RELATIONAL_DATABASE', - Name: `${this.context.databaseName}_rds_DataSource`, - Description: `RDS Data Source Provisioned for ${this.context.databaseName}`, - ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - ServiceRoleArn: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseAccessRole, 'Arn'), - RelationalDatabaseConfig: { - RelationalDatabaseSourceType: 'RDS_HTTP_ENDPOINT', - RdsHttpEndpointConfig: { - AwsRegion: Fn.Ref(ResourceConstants.PARAMETERS.rdsRegion), - DbClusterIdentifier: Fn.Ref(ResourceConstants.PARAMETERS.rdsClusterIdentifier), - DatabaseName: Fn.Ref(ResourceConstants.PARAMETERS.rdsDatabaseName), - Schema: this.context.databaseSchema, - AwsSecretStoreArn: Fn.Ref(ResourceConstants.PARAMETERS.rdsSecretStoreArn) - } - } - }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseAccessRole]) - } + /** + * Creates the AppSync DataSource CFN Spec pointing at the provided RDS Cluster + * + * @param cliContext - the Amplify context, used to load environment variables. + * @returns the data source CloudFormation resource. + */ + private makeRelationalDataSource(cliContext: any): DataSource { + return new DataSource({ + Type: 'RELATIONAL_DATABASE', + Name: `${this.context.databaseName}_rds_DataSource`, + Description: `RDS Data Source Provisioned for ${this.context.databaseName}`, + ApiId: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + ServiceRoleArn: Fn.GetAtt(ResourceConstants.RESOURCES.RelationalDatabaseAccessRole, 'Arn'), + RelationalDatabaseConfig: { + RelationalDatabaseSourceType: 'RDS_HTTP_ENDPOINT', + RdsHttpEndpointConfig: { + AwsRegion: Fn.Ref(ResourceConstants.PARAMETERS.rdsRegion), + DbClusterIdentifier: Fn.Ref(ResourceConstants.PARAMETERS.rdsClusterIdentifier), + DatabaseName: Fn.Ref(ResourceConstants.PARAMETERS.rdsDatabaseName), + Schema: this.context.databaseSchema, + AwsSecretStoreArn: Fn.Ref(ResourceConstants.PARAMETERS.rdsSecretStoreArn), + }, + }, + }).dependsOn([ResourceConstants.RESOURCES.RelationalDatabaseAccessRole]); + } } diff --git a/packages/graphql-relational-schema-transformer/src/ResourceConstants.ts b/packages/graphql-relational-schema-transformer/src/ResourceConstants.ts index a173e45b26..fcbd8bf1df 100644 --- a/packages/graphql-relational-schema-transformer/src/ResourceConstants.ts +++ b/packages/graphql-relational-schema-transformer/src/ResourceConstants.ts @@ -2,42 +2,41 @@ * Resource Constants that are specific to the Relation Database Transform */ export class ResourceConstants { + public static readonly ENVIRONMENT_CONTEXT_KEYS = { + // Aurora Serverless Imports + RDSRegion: 'rdsRegion', + RDSClusterIdentifier: 'rdsClusterIdentifier', + RDSSecretStoreArn: 'rdsSecretStoreArn', + RDSDatabaseName: 'rdsDatabaseName', + }; - public static readonly ENVIRONMENT_CONTEXT_KEYS = { - // Aurora Serverless Imports - RDSRegion: 'rdsRegion', - RDSClusterIdentifier: 'rdsClusterIdentifier', - RDSSecretStoreArn: 'rdsSecretStoreArn', - RDSDatabaseName: 'rdsDatabaseName', - } + public static readonly RESOURCES = { + // AppSync + GraphQLAPILogicalID: 'GraphQLAPI', + GraphQLSchemaLogicalID: 'GraphQLSchema', + APIKeyLogicalID: 'GraphQLAPIKey', - public static readonly RESOURCES = { - // AppSync - GraphQLAPILogicalID: 'GraphQLAPI', - GraphQLSchemaLogicalID: 'GraphQLSchema', - APIKeyLogicalID: 'GraphQLAPIKey', + // Relational Database + ResolverFileName: 'ResolverFileName', + RelationalDatabaseDataSource: 'RelationalDatabaseDataSource', + RelationalDatabaseAccessRole: 'RelationalDatabaseAccessRole', + }; - // Relational Database - ResolverFileName: 'ResolverFileName', - RelationalDatabaseDataSource: 'RelationalDatabaseDataSource', - RelationalDatabaseAccessRole: 'RelationalDatabaseAccessRole', - } + public static PARAMETERS = { + // cli + Env: 'env', + S3DeploymentBucket: 'S3DeploymentBucket', + S3DeploymentRootKey: 'S3DeploymentRootKey', - public static PARAMETERS = { - // cli - Env: 'env', - S3DeploymentBucket: 'S3DeploymentBucket', - S3DeploymentRootKey: 'S3DeploymentRootKey', + // AppSync + AppSyncApiName: 'AppSyncApiName', + AppSyncApiId: 'AppSyncApiId', + APIKeyExpirationEpoch: 'APIKeyExpirationEpoch', - // AppSync - AppSyncApiName: 'AppSyncApiName', - AppSyncApiId: 'AppSyncApiId', - APIKeyExpirationEpoch: 'APIKeyExpirationEpoch', - - // Aurora Serverless - rdsRegion: 'rdsRegion', - rdsClusterIdentifier: 'rdsClusterIdentifier', - rdsSecretStoreArn: 'rdsSecretStoreArn', - rdsDatabaseName: 'rdsDatabaseName' - } -} \ No newline at end of file + // Aurora Serverless + rdsRegion: 'rdsRegion', + rdsClusterIdentifier: 'rdsClusterIdentifier', + rdsSecretStoreArn: 'rdsSecretStoreArn', + rdsDatabaseName: 'rdsDatabaseName', + }; +} diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/AuroraDataAPIClient.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/AuroraDataAPIClient.test.ts index f38556c2b9..705cfe8eed 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/AuroraDataAPIClient.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/AuroraDataAPIClient.test.ts @@ -1,464 +1,464 @@ -import { DataApiParams, AuroraDataAPIClient } from "../AuroraDataAPIClient"; +import { DataApiParams, AuroraDataAPIClient } from '../AuroraDataAPIClient'; -const region = 'us-east-1' -const secretStoreArn = 'secretStoreArn' -const clusterArn = 'clusterArn' -const databaseName = 'Animals' -const tableAName = 'Dog' -const tableBName = 'Owners' +const region = 'us-east-1'; +const secretStoreArn = 'secretStoreArn'; +const clusterArn = 'clusterArn'; +const databaseName = 'Animals'; +const tableAName = 'Dog'; +const tableBName = 'Owners'; test('Test list tables', async () => { - const rdsPromise = { - promise: jest.fn().mockImplementation(() => { - return new Promise((resolve, reject) => { - const response = { - "numberOfRecordsUpdated": -1, - "records": [ - [ - { - "bigIntValue": null, - "bitValue": null, - "blobValue": null, - "doubleValue": null, - "intValue": null, - "isNull": null, - "realValue": null, - "stringValue": `${tableAName}` - } - ] - ], - "columnMetadata": [ - { - "arrayBaseColumnType": 0, - "isAutoIncrement": false, - "isCaseSensitive": false, - "isCurrency": false, - "isSigned": false, - "label": `Tables_in_${databaseName}`, - "name": "TABLE_NAME", - "nullable": 0, - "precision": 64, - "scale": 0, - "schemaName": "", - "tableName": "TABLE_NAMES", - "type": 12, - "typeName": "VARCHAR" - } - ] - } - resolve(response) - }) - }) - } - const MockRDSClient = jest.fn(() => ({ - executeStatement: jest.fn((params: DataApiParams) => { - if (params.sql == 'SHOW TABLES') { - return rdsPromise - } - throw new Error('Incorrect SQL given.') - }) - })) + const rdsPromise = { + promise: jest.fn().mockImplementation(() => { + return new Promise((resolve, reject) => { + const response = { + numberOfRecordsUpdated: -1, + records: [ + [ + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: `${tableAName}`, + }, + ], + ], + columnMetadata: [ + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: `Tables_in_${databaseName}`, + name: 'TABLE_NAME', + nullable: 0, + precision: 64, + scale: 0, + schemaName: '', + tableName: 'TABLE_NAMES', + type: 12, + typeName: 'VARCHAR', + }, + ], + }; + resolve(response); + }); + }), + }; + const MockRDSClient = jest.fn(() => ({ + executeStatement: jest.fn((params: DataApiParams) => { + if (params.sql == 'SHOW TABLES') { + return rdsPromise; + } + throw new Error('Incorrect SQL given.'); + }), + })); - const aws = require('aws-sdk') + const aws = require('aws-sdk'); - const testClient = new AuroraDataAPIClient(region, secretStoreArn, clusterArn, databaseName, aws) - const mockRDS = new MockRDSClient() - testClient.setRDSClient(mockRDS) + const testClient = new AuroraDataAPIClient(region, secretStoreArn, clusterArn, databaseName, aws); + const mockRDS = new MockRDSClient(); + testClient.setRDSClient(mockRDS); - const tables = await testClient.listTables() - const Params = new DataApiParams() - Params.secretArn = secretStoreArn - Params.resourceArn = clusterArn - Params.database = databaseName - Params.sql = 'SHOW TABLES' - expect(mockRDS.executeStatement).toHaveBeenCalledWith(Params) - expect(tables.length).toEqual(1) - expect(tables[0]).toEqual(tableAName) -}) + const tables = await testClient.listTables(); + const Params = new DataApiParams(); + Params.secretArn = secretStoreArn; + Params.resourceArn = clusterArn; + Params.database = databaseName; + Params.sql = 'SHOW TABLES'; + expect(mockRDS.executeStatement).toHaveBeenCalledWith(Params); + expect(tables.length).toEqual(1); + expect(tables[0]).toEqual(tableAName); +}); -test('Test foreign key lookup', async() => { - const rdsPromise = { - promise: jest.fn().mockImplementation(() => { - return new Promise((resolve, reject) => { - const response = { - "numberOfRecordsUpdated": -1, - "records": [ - [ - { - "bigIntValue": null, - "bitValue": null, - "blobValue": null, - "doubleValue": null, - "intValue": null, - "isNull": null, - "realValue": null, - "stringValue": `${tableAName}` - } - ] - ], - "columnMetadata": [ - { - "arrayBaseColumnType": 0, - "isAutoIncrement": false, - "isCaseSensitive": false, - "isCurrency": false, - "isSigned": false, - "label": `Tables_in_${databaseName}`, - "name": "TABLE_NAME", - "nullable": 0, - "precision": 64, - "scale": 0, - "schemaName": "", - "tableName": "TABLE_NAMES", - "type": 12, - "typeName": "VARCHAR" - } - ] - } - resolve(response) - }) - }) - } - - const MockRDSClient = jest.fn(() => ({ - executeStatement: jest.fn((params: DataApiParams) => { - if (params.sql.indexOf(`AND REFERENCED_TABLE_NAME = '${tableBName}'`) > -1) { - return rdsPromise - } - throw new Error('Incorrect SQL given.') - }) - })) +test('Test foreign key lookup', async () => { + const rdsPromise = { + promise: jest.fn().mockImplementation(() => { + return new Promise((resolve, reject) => { + const response = { + numberOfRecordsUpdated: -1, + records: [ + [ + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: `${tableAName}`, + }, + ], + ], + columnMetadata: [ + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: `Tables_in_${databaseName}`, + name: 'TABLE_NAME', + nullable: 0, + precision: 64, + scale: 0, + schemaName: '', + tableName: 'TABLE_NAMES', + type: 12, + typeName: 'VARCHAR', + }, + ], + }; + resolve(response); + }); + }), + }; - const aws = require('aws-sdk') - const testClient = new AuroraDataAPIClient(region, secretStoreArn, clusterArn, databaseName, aws) - const mockRDS = new MockRDSClient() - testClient.setRDSClient(mockRDS) + const MockRDSClient = jest.fn(() => ({ + executeStatement: jest.fn((params: DataApiParams) => { + if (params.sql.indexOf(`AND REFERENCED_TABLE_NAME = '${tableBName}'`) > -1) { + return rdsPromise; + } + throw new Error('Incorrect SQL given.'); + }), + })); - const tables = await testClient.getTableForeignKeyReferences(tableBName) - expect(tables.length).toEqual(1) - expect(tables[0]).toEqual(tableAName) -}) + const aws = require('aws-sdk'); + const testClient = new AuroraDataAPIClient(region, secretStoreArn, clusterArn, databaseName, aws); + const mockRDS = new MockRDSClient(); + testClient.setRDSClient(mockRDS); -test('Test describe table', async() => { - const rdsPromise = { - promise: jest.fn().mockImplementation(() => { - return new Promise((resolve, reject) => { - const response = { - "numberOfRecordsUpdated":-1, - "records":[ - [ - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"id" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"int(11)" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"NO" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"PRI" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":true, - "realValue":null, - "stringValue":null - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"" - } - ], - [ - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"name" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"varchar(255)" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"NO" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":true, - "realValue":null, - "stringValue":null - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"" - } - ], - [ - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"age" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"int(11)" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"NO" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"" - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":true, - "realValue":null, - "stringValue":null - }, - { - "bigIntValue":null, - "bitValue":null, - "blobValue":null, - "doubleValue":null, - "intValue":null, - "isNull":null, - "realValue":null, - "stringValue":"" - } - ] - ], - - "columnMetadata":[ - { - "arrayBaseColumnType":0, - "isAutoIncrement":false, - "isCaseSensitive":false, - "isCurrency":false, - "isSigned":false, - "label":"Field", - "name":"COLUMN_NAME", - "nullable":0, - "precision":64, - "scale":0, - "schemaName":"", - "tableName":"COLUMNS", - "type":12, - "typeName":"VARCHAR" - }, - { - "arrayBaseColumnType":0, - "isAutoIncrement":false, - "isCaseSensitive":false, - "isCurrency":false, - "isSigned":false, - "label":"Type", - "name":"COLUMN_TYPE", - "nullable":0, - "precision":65535, - "scale":0, - "schemaName":"", - "tableName":"COLUMNS", - "type":-1, - "typeName":"MEDIUMTEXT" - }, - { - "arrayBaseColumnType":0, - "isAutoIncrement":false, - "isCaseSensitive":false, - "isCurrency":false, - "isSigned":false, - "label":"Null", - "name":"IS_NULLABLE", - "nullable":0, - "precision":3, - "scale":0, - "schemaName":"", - "tableName":"COLUMNS", - "type":12, - "typeName":"VARCHAR" - }, - { - "arrayBaseColumnType":0, - "isAutoIncrement":false, - "isCaseSensitive":false, - "isCurrency":false, - "isSigned":false, - "label":"Key", - "name":"COLUMN_KEY", - "nullable":0, - "precision":3, - "scale":0, - "schemaName":"", - "tableName":"COLUMNS", - "type":12, - "typeName":"VARCHAR" - }, - { - "arrayBaseColumnType":0, - "isAutoIncrement":false, - "isCaseSensitive":false, - "isCurrency":false, - "isSigned":false, - "label":"Default", - "name":"COLUMN_DEFAULT", - "nullable":1, - "precision":65535, - "scale":0, - "schemaName":"", - "tableName":"COLUMNS", - "type":-1, - "typeName":"MEDIUMTEXT" - }, - { - "arrayBaseColumnType":0, - "isAutoIncrement":false, - "isCaseSensitive":false, - "isCurrency":false, - "isSigned":false, - "label":"Extra", - "name":"EXTRA", - "nullable":0, - "precision":30, - "scale":0, - "schemaName":"", - "tableName":"COLUMNS", - "type":12, - "typeName":"VARCHAR" - } - ] - } - resolve(response) - }) - }) - } + const tables = await testClient.getTableForeignKeyReferences(tableBName); + expect(tables.length).toEqual(1); + expect(tables[0]).toEqual(tableAName); +}); - const MockRDSClient = jest.fn(() => ({ - executeStatement: jest.fn((params: DataApiParams) => { - if (params.sql == `DESCRIBE ${tableAName}`) { - return rdsPromise - } - throw new Error('Incorrect SQL given.') - }) - })) +test('Test describe table', async () => { + const rdsPromise = { + promise: jest.fn().mockImplementation(() => { + return new Promise((resolve, reject) => { + const response = { + numberOfRecordsUpdated: -1, + records: [ + [ + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'id', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'int(11)', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'NO', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'PRI', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: true, + realValue: null, + stringValue: null, + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: '', + }, + ], + [ + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'name', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'varchar(255)', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'NO', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: '', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: true, + realValue: null, + stringValue: null, + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: '', + }, + ], + [ + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'age', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'int(11)', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: 'NO', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: '', + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: true, + realValue: null, + stringValue: null, + }, + { + bigIntValue: null, + bitValue: null, + blobValue: null, + doubleValue: null, + intValue: null, + isNull: null, + realValue: null, + stringValue: '', + }, + ], + ], - const aws = require('aws-sdk') - const testClient = new AuroraDataAPIClient(region, secretStoreArn, clusterArn, databaseName, aws) - const mockRDS = new MockRDSClient() - testClient.setRDSClient(mockRDS) + columnMetadata: [ + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: 'Field', + name: 'COLUMN_NAME', + nullable: 0, + precision: 64, + scale: 0, + schemaName: '', + tableName: 'COLUMNS', + type: 12, + typeName: 'VARCHAR', + }, + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: 'Type', + name: 'COLUMN_TYPE', + nullable: 0, + precision: 65535, + scale: 0, + schemaName: '', + tableName: 'COLUMNS', + type: -1, + typeName: 'MEDIUMTEXT', + }, + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: 'Null', + name: 'IS_NULLABLE', + nullable: 0, + precision: 3, + scale: 0, + schemaName: '', + tableName: 'COLUMNS', + type: 12, + typeName: 'VARCHAR', + }, + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: 'Key', + name: 'COLUMN_KEY', + nullable: 0, + precision: 3, + scale: 0, + schemaName: '', + tableName: 'COLUMNS', + type: 12, + typeName: 'VARCHAR', + }, + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: 'Default', + name: 'COLUMN_DEFAULT', + nullable: 1, + precision: 65535, + scale: 0, + schemaName: '', + tableName: 'COLUMNS', + type: -1, + typeName: 'MEDIUMTEXT', + }, + { + arrayBaseColumnType: 0, + isAutoIncrement: false, + isCaseSensitive: false, + isCurrency: false, + isSigned: false, + label: 'Extra', + name: 'EXTRA', + nullable: 0, + precision: 30, + scale: 0, + schemaName: '', + tableName: 'COLUMNS', + type: 12, + typeName: 'VARCHAR', + }, + ], + }; + resolve(response); + }); + }), + }; - const columnDescriptions = await testClient.describeTable(tableAName) - const Params = new DataApiParams() - Params.secretArn = secretStoreArn - Params.resourceArn = clusterArn - Params.database = databaseName - Params.sql = `DESCRIBE ${tableAName}` - expect(mockRDS.executeStatement).toHaveBeenCalledWith(Params) - expect(columnDescriptions.length).toEqual(3) - // TODO: the rest of these tests -}) \ No newline at end of file + const MockRDSClient = jest.fn(() => ({ + executeStatement: jest.fn((params: DataApiParams) => { + if (params.sql == `DESCRIBE ${tableAName}`) { + return rdsPromise; + } + throw new Error('Incorrect SQL given.'); + }), + })); + + const aws = require('aws-sdk'); + const testClient = new AuroraDataAPIClient(region, secretStoreArn, clusterArn, databaseName, aws); + const mockRDS = new MockRDSClient(); + testClient.setRDSClient(mockRDS); + + const columnDescriptions = await testClient.describeTable(tableAName); + const Params = new DataApiParams(); + Params.secretArn = secretStoreArn; + Params.resourceArn = clusterArn; + Params.database = databaseName; + Params.sql = `DESCRIBE ${tableAName}`; + expect(mockRDS.executeStatement).toHaveBeenCalledWith(Params); + expect(columnDescriptions.length).toEqual(3); + // TODO: the rest of these tests +}); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/AuroraServerlessMySQLDBReader.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/AuroraServerlessMySQLDBReader.test.ts index 484f1c8b15..9ff988f400 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/AuroraServerlessMySQLDBReader.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/AuroraServerlessMySQLDBReader.test.ts @@ -1,160 +1,161 @@ -import TemplateContext, { TableContext } from '../RelationalDBSchemaTransformer' -import { Kind} from 'graphql' +import TemplateContext, { TableContext } from '../RelationalDBSchemaTransformer'; +import { Kind } from 'graphql'; import { AuroraServerlessMySQLDatabaseReader } from '../AuroraServerlessMySQLDatabaseReader'; import { AuroraDataAPIClient, ColumnDescription } from '../AuroraDataAPIClient'; -import { toUpper } from 'graphql-transformer-common' +import { toUpper } from 'graphql-transformer-common'; -const dbRegion = 'us-east-1' -const secretStoreArn = 'secretStoreArn' -const clusterArn = 'clusterArn' -const testDBName = 'testdb' -const tableAName = 'a' -const tableBName = 'b' -const tableCName = 'c' -const tableDName = 'd' -const aws = require('aws-sdk') +const dbRegion = 'us-east-1'; +const secretStoreArn = 'secretStoreArn'; +const clusterArn = 'clusterArn'; +const testDBName = 'testdb'; +const tableAName = 'a'; +const tableBName = 'b'; +const tableCName = 'c'; +const tableDName = 'd'; +const aws = require('aws-sdk'); -const dummyReader = new AuroraServerlessMySQLDatabaseReader(dbRegion, secretStoreArn, clusterArn, testDBName, aws) +const dummyReader = new AuroraServerlessMySQLDatabaseReader(dbRegion, secretStoreArn, clusterArn, testDBName, aws); test('Test describe table', async () => { - const MockAuroraClient = jest.fn(() => ({ - describeTable: jest.fn((tableName: string) => { - const tableColumns = [] - const idColDescription = new ColumnDescription() - const nameColDescription = new ColumnDescription() + const MockAuroraClient = jest.fn(() => ({ + describeTable: jest.fn((tableName: string) => { + const tableColumns = []; + const idColDescription = new ColumnDescription(); + const nameColDescription = new ColumnDescription(); - idColDescription.Field = 'id' - idColDescription.Type = 'int' - idColDescription.Null = 'NO' - idColDescription.Key = 'PRI' - idColDescription.Default = null - idColDescription.Extra = '' + idColDescription.Field = 'id'; + idColDescription.Type = 'int'; + idColDescription.Null = 'NO'; + idColDescription.Key = 'PRI'; + idColDescription.Default = null; + idColDescription.Extra = ''; - nameColDescription.Field = 'name' - nameColDescription.Type = 'varchar(100)' - nameColDescription.Null = 'YES' - nameColDescription.Key = '' - nameColDescription.Default = null - nameColDescription.Extra = '' + nameColDescription.Field = 'name'; + nameColDescription.Type = 'varchar(100)'; + nameColDescription.Null = 'YES'; + nameColDescription.Key = ''; + nameColDescription.Default = null; + nameColDescription.Extra = ''; - tableColumns.push(idColDescription) - tableColumns.push(nameColDescription) - if (tableName == tableBName) { - const foreignKeyId = new ColumnDescription() - foreignKeyId.Field = 'aId' - foreignKeyId.Type = 'int' - foreignKeyId.Null = 'YES' - foreignKeyId.Key = 'MUL' - foreignKeyId.Default = null - foreignKeyId.Extra = '' + tableColumns.push(idColDescription); + tableColumns.push(nameColDescription); + if (tableName == tableBName) { + const foreignKeyId = new ColumnDescription(); + foreignKeyId.Field = 'aId'; + foreignKeyId.Type = 'int'; + foreignKeyId.Null = 'YES'; + foreignKeyId.Key = 'MUL'; + foreignKeyId.Default = null; + foreignKeyId.Extra = ''; - tableColumns.push(foreignKeyId) - } - return tableColumns - }), - getTableForeignKeyReferences: jest.fn((tableName: string) => { - if (tableName == tableBName) { - return [tableAName] - } - return [] - }) - })) - const mockClient = new MockAuroraClient() - dummyReader.setAuroraClient(mockClient) + tableColumns.push(foreignKeyId); + } + return tableColumns; + }), + getTableForeignKeyReferences: jest.fn((tableName: string) => { + if (tableName == tableBName) { + return [tableAName]; + } + return []; + }), + })); + const mockClient = new MockAuroraClient(); + dummyReader.setAuroraClient(mockClient); - describeTableTestCommon(tableAName, 2, false, await dummyReader.describeTable(tableAName)) - describeTableTestCommon(tableBName, 3, true, await dummyReader.describeTable(tableBName)) - describeTableTestCommon(tableCName, 2, false, await dummyReader.describeTable(tableCName)) - describeTableTestCommon(tableDName, 2, false, await dummyReader.describeTable(tableDName)) -}) + describeTableTestCommon(tableAName, 2, false, await dummyReader.describeTable(tableAName)); + describeTableTestCommon(tableBName, 3, true, await dummyReader.describeTable(tableBName)); + describeTableTestCommon(tableCName, 2, false, await dummyReader.describeTable(tableCName)); + describeTableTestCommon(tableDName, 2, false, await dummyReader.describeTable(tableDName)); +}); function describeTableTestCommon(tableName: string, fieldLength: number, isForeignKey: boolean, tableContext: TableContext) { - const formattedTableName = toUpper(tableName) - expect(tableContext.tableKeyField).toEqual('id') - expect(tableContext.tableKeyFieldType).toEqual('Int') - expect(tableContext.createTypeDefinition).toBeDefined() - expect(tableContext.updateTypeDefinition).toBeDefined() - expect(tableContext.tableTypeDefinition).toBeDefined() - expect(tableContext.tableTypeDefinition.kind).toEqual(Kind.OBJECT_TYPE_DEFINITION) - expect(tableContext.updateTypeDefinition.kind).toEqual(Kind.INPUT_OBJECT_TYPE_DEFINITION) - expect(tableContext.createTypeDefinition.kind).toEqual(Kind.INPUT_OBJECT_TYPE_DEFINITION) - expect(tableContext.tableTypeDefinition.name.value).toEqual(tableName) - expect(tableContext.tableTypeDefinition.name.kind).toEqual(Kind.NAME) - expect(tableContext.updateTypeDefinition.name.value).toEqual(`Update${formattedTableName}Input`) - expect(tableContext.updateTypeDefinition.name.kind).toEqual(Kind.NAME) - expect(tableContext.createTypeDefinition.name.value).toEqual(`Create${formattedTableName}Input`) - expect(tableContext.createTypeDefinition.name.kind).toEqual(Kind.NAME) - /** - * If it's a table with a foreign key constraint, the base type will have one additional element - * for the nested type. e.g. if type Posts had fields of id/int, content/string, and author/string - * but comments had a foreign key constraint on it, then it would look like this (whereas the - * create and update inputs would not have the additional field): - * type Post { - * id: Int! - * author: String! - * content: String! - * comments: CommentConnection - * } - */ - expect(tableContext.tableTypeDefinition.fields.length).toEqual(fieldLength) - expect(tableContext.updateTypeDefinition.fields.length).toEqual(fieldLength) - expect(tableContext.createTypeDefinition.fields.length).toEqual(fieldLength) + const formattedTableName = toUpper(tableName); + expect(tableContext.tableKeyField).toEqual('id'); + expect(tableContext.tableKeyFieldType).toEqual('Int'); + expect(tableContext.createTypeDefinition).toBeDefined(); + expect(tableContext.updateTypeDefinition).toBeDefined(); + expect(tableContext.tableTypeDefinition).toBeDefined(); + expect(tableContext.tableTypeDefinition.kind).toEqual(Kind.OBJECT_TYPE_DEFINITION); + expect(tableContext.updateTypeDefinition.kind).toEqual(Kind.INPUT_OBJECT_TYPE_DEFINITION); + expect(tableContext.createTypeDefinition.kind).toEqual(Kind.INPUT_OBJECT_TYPE_DEFINITION); + expect(tableContext.tableTypeDefinition.name.value).toEqual(tableName); + expect(tableContext.tableTypeDefinition.name.kind).toEqual(Kind.NAME); + expect(tableContext.updateTypeDefinition.name.value).toEqual(`Update${formattedTableName}Input`); + expect(tableContext.updateTypeDefinition.name.kind).toEqual(Kind.NAME); + expect(tableContext.createTypeDefinition.name.value).toEqual(`Create${formattedTableName}Input`); + expect(tableContext.createTypeDefinition.name.kind).toEqual(Kind.NAME); + /** + * If it's a table with a foreign key constraint, the base type will have one additional element + * for the nested type. e.g. if type Posts had fields of id/int, content/string, and author/string + * but comments had a foreign key constraint on it, then it would look like this (whereas the + * create and update inputs would not have the additional field): + * type Post { + * id: Int! + * author: String! + * content: String! + * comments: CommentConnection + * } + */ + + expect(tableContext.tableTypeDefinition.fields.length).toEqual(fieldLength); + expect(tableContext.updateTypeDefinition.fields.length).toEqual(fieldLength); + expect(tableContext.createTypeDefinition.fields.length).toEqual(fieldLength); } -test('Test hydrate template context', async() => { - const context = await dummyReader.hydrateTemplateContext(new TemplateContext(null, null, null, null)) - expect(context.secretStoreArn).toEqual(secretStoreArn) - expect(context.databaseName).toEqual(testDBName) - expect(context.rdsClusterIdentifier).toEqual(clusterArn) - expect(context.region).toEqual(dbRegion) - expect(context.databaseSchema).toEqual('mysql') -}) +test('Test hydrate template context', async () => { + const context = await dummyReader.hydrateTemplateContext(new TemplateContext(null, null, null, null)); + expect(context.secretStoreArn).toEqual(secretStoreArn); + expect(context.databaseName).toEqual(testDBName); + expect(context.rdsClusterIdentifier).toEqual(clusterArn); + expect(context.region).toEqual(dbRegion); + expect(context.databaseSchema).toEqual('mysql'); +}); test('Test list tables', async () => { - const MockAuroraClient = jest.fn(() => ({ - listTables: jest.fn(() => { - return [ tableAName, tableBName, tableCName, tableDName] - }) - })) - const mockClient = new MockAuroraClient() - dummyReader.setAuroraClient(mockClient) + const MockAuroraClient = jest.fn(() => ({ + listTables: jest.fn(() => { + return [tableAName, tableBName, tableCName, tableDName]; + }), + })); + const mockClient = new MockAuroraClient(); + dummyReader.setAuroraClient(mockClient); - const tableNames = await dummyReader.listTables() - expect(mockClient.listTables).toHaveBeenCalled() - expect(tableNames.length).toBe(4) - expect(tableNames.indexOf(tableAName) > -1).toBe(true) - expect(tableNames.indexOf(tableBName) > -1).toBe(true) - expect(tableNames.indexOf(tableCName) > -1).toBe(true) - expect(tableNames.indexOf(tableDName) > -1).toBe(true) -}) + const tableNames = await dummyReader.listTables(); + expect(mockClient.listTables).toHaveBeenCalled(); + expect(tableNames.length).toBe(4); + expect(tableNames.indexOf(tableAName) > -1).toBe(true); + expect(tableNames.indexOf(tableBName) > -1).toBe(true); + expect(tableNames.indexOf(tableCName) > -1).toBe(true); + expect(tableNames.indexOf(tableDName) > -1).toBe(true); +}); test('Test lookup foreign key', async () => { - const MockAuroraClient = jest.fn(() => ({ - getTableForeignKeyReferences: jest.fn((tableName: string) => { - if (tableName == tableBName) { - return [tableAName] - } - return [] - }) - })) - const mockClient = new MockAuroraClient() - dummyReader.setAuroraClient(mockClient) + const MockAuroraClient = jest.fn(() => ({ + getTableForeignKeyReferences: jest.fn((tableName: string) => { + if (tableName == tableBName) { + return [tableAName]; + } + return []; + }), + })); + const mockClient = new MockAuroraClient(); + dummyReader.setAuroraClient(mockClient); - const aKeys = await dummyReader.getTableForeignKeyReferences(tableAName) - const bKeys = await dummyReader.getTableForeignKeyReferences(tableBName) - const cKeys = await dummyReader.getTableForeignKeyReferences(tableCName) - const dKeys = await dummyReader.getTableForeignKeyReferences(tableDName) - expect(aKeys).toBeDefined() - expect(bKeys).toBeDefined() - expect(cKeys).toBeDefined() - expect(dKeys).toBeDefined() - expect(aKeys.length).toBe(0) - expect(bKeys.length).toBe(1) - expect(cKeys.length).toBe(0) - expect(dKeys.length).toBe(0) - expect(bKeys[0]).toBe(tableAName) - expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableAName) - expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableBName) - expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableCName) - expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableDName) -}) \ No newline at end of file + const aKeys = await dummyReader.getTableForeignKeyReferences(tableAName); + const bKeys = await dummyReader.getTableForeignKeyReferences(tableBName); + const cKeys = await dummyReader.getTableForeignKeyReferences(tableCName); + const dKeys = await dummyReader.getTableForeignKeyReferences(tableDName); + expect(aKeys).toBeDefined(); + expect(bKeys).toBeDefined(); + expect(cKeys).toBeDefined(); + expect(dKeys).toBeDefined(); + expect(aKeys.length).toBe(0); + expect(bKeys.length).toBe(1); + expect(cKeys.length).toBe(0); + expect(dKeys.length).toBe(0); + expect(bKeys[0]).toBe(tableAName); + expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableAName); + expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableBName); + expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableCName); + expect(mockClient.getTableForeignKeyReferences).toHaveBeenCalledWith(tableDName); +}); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBMappingTemplate.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBMappingTemplate.test.ts index 6a7d370bfd..cb6f265678 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBMappingTemplate.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBMappingTemplate.test.ts @@ -1,32 +1,32 @@ -import RelationalDBMappingTemplate from '../RelationalDBMappingTemplate' -import { list, str, ObjectNode, print } from 'graphql-mapping-template' +import RelationalDBMappingTemplate from '../RelationalDBMappingTemplate'; +import { list, str, ObjectNode, print } from 'graphql-mapping-template'; -const sql = 'SELECT * FROM Pets' +const sql = 'SELECT * FROM Pets'; /** * Test for verifying that provided a sql statement, we succesfully create * the rds query mapping template */ test('Test RDS Query Mapping Template Creation', () => { - const queryObj: ObjectNode = RelationalDBMappingTemplate.rdsQuery({ - statements: list([str(sql)]) - }) + const queryObj: ObjectNode = RelationalDBMappingTemplate.rdsQuery({ + statements: list([str(sql)]), + }); - expect(queryObj).toBeDefined() - expect(queryObj.kind).toBe('Object') - expect(queryObj.attributes).toBeDefined() + expect(queryObj).toBeDefined(); + expect(queryObj.kind).toBe('Object'); + expect(queryObj.attributes).toBeDefined(); - // Verify the Version was created succesfully - const versionAttr = queryObj.attributes[0] - expect(versionAttr).toBeDefined() - expect(versionAttr[0]).toBe('version') - expect(versionAttr[1].kind).toBe('String') - expect(print(versionAttr[1])).toBe('"2018-05-29"') + // Verify the Version was created succesfully + const versionAttr = queryObj.attributes[0]; + expect(versionAttr).toBeDefined(); + expect(versionAttr[0]).toBe('version'); + expect(versionAttr[1].kind).toBe('String'); + expect(print(versionAttr[1])).toBe('"2018-05-29"'); - // Verify the sql statement was created succesfully - const statementsAttr = queryObj.attributes[1] - expect(statementsAttr).toBeDefined() - expect(statementsAttr[0]).toBe('statements') - expect(statementsAttr[1].kind).toBe('List') - expect(print(statementsAttr[1])).toBe('["SELECT * FROM Pets"]') -}) + // Verify the sql statement was created succesfully + const statementsAttr = queryObj.attributes[1]; + expect(statementsAttr).toBeDefined(); + expect(statementsAttr[0]).toBe('statements'); + expect(statementsAttr[1].kind).toBe('List'); + expect(print(statementsAttr[1])).toBe('["SELECT * FROM Pets"]'); +}); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts index 89e19c1332..c809cffc50 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBResolverGenerator.test.ts @@ -1,6 +1,6 @@ -import RelationalDBResolverGenerator from '../RelationalDBResolverGenerator' +import RelationalDBResolverGenerator from '../RelationalDBResolverGenerator'; import TemplateContext from '../RelationalDBSchemaTransformer'; -import { parse } from 'graphql' +import { parse } from 'graphql'; import { JSONMappingParameters } from 'cloudform-types/types/kinesisAnalyticsV2/applicationReferenceDataSource'; jest.mock('fs-extra'); @@ -16,38 +16,38 @@ const schema = parse(` name: String } `); -let simpleStringFieldMap = new Map() -let simpleIntFieldMap = new Map() -let simplePrimaryKeyMap = new Map() -let simplePrimaryKeyTypeMap = new Map() - -simplePrimaryKeyMap.set('Pet', 'Id') -simplePrimaryKeyMap.set('Owner', 'Id') -simplePrimaryKeyTypeMap.set('Pet', 'String') -simplePrimaryKeyTypeMap.set('Owner', 'Int') -const context = new TemplateContext(schema, simplePrimaryKeyMap, simpleStringFieldMap, simpleIntFieldMap, simplePrimaryKeyTypeMap) -const generator = new RelationalDBResolverGenerator(context) +let simpleStringFieldMap = new Map(); +let simpleIntFieldMap = new Map(); +let simplePrimaryKeyMap = new Map(); +let simplePrimaryKeyTypeMap = new Map(); + +simplePrimaryKeyMap.set('Pet', 'Id'); +simplePrimaryKeyMap.set('Owner', 'Id'); +simplePrimaryKeyTypeMap.set('Pet', 'String'); +simplePrimaryKeyTypeMap.set('Owner', 'Int'); +const context = new TemplateContext(schema, simplePrimaryKeyMap, simpleStringFieldMap, simpleIntFieldMap, simplePrimaryKeyTypeMap); +const generator = new RelationalDBResolverGenerator(context); /** * Test for verifying that provided a template context, the resolver generator * creates the CRUDL AppSync Resolver resources. */ test('Test Basic CRUDL Resolver Generation', () => { - const resources: { [key: string]: any } = generator.createRelationalResolvers("someFilePath") - expect(resources).toBeDefined() - - // Verify all CRUDL resolvers were created for the Pet Type - expect(resources).toHaveProperty('PetCreateResolver') - expect(resources).toHaveProperty('PetGetResolver') - expect(resources).toHaveProperty('PetUpdateResolver') - expect(resources).toHaveProperty('PetDeleteResolver') - expect(resources).toHaveProperty('PetListResolver') - - // Verify for the GetResolver the elements are present - let resolverMap = Object.keys(resources).map(key => resources[key]) - expect(resolverMap[1]).toHaveProperty('Type') - expect(resolverMap[1]).toHaveProperty('Properties') - - // Verify a resolver was created for the owner type as well - expect(resources).toHaveProperty('OwnerCreateResolver') -}) \ No newline at end of file + const resources: { [key: string]: any } = generator.createRelationalResolvers('someFilePath'); + expect(resources).toBeDefined(); + + // Verify all CRUDL resolvers were created for the Pet Type + expect(resources).toHaveProperty('PetCreateResolver'); + expect(resources).toHaveProperty('PetGetResolver'); + expect(resources).toHaveProperty('PetUpdateResolver'); + expect(resources).toHaveProperty('PetDeleteResolver'); + expect(resources).toHaveProperty('PetListResolver'); + + // Verify for the GetResolver the elements are present + let resolverMap = Object.keys(resources).map(key => resources[key]); + expect(resolverMap[1]).toHaveProperty('Type'); + expect(resolverMap[1]).toHaveProperty('Properties'); + + // Verify a resolver was created for the owner type as well + expect(resources).toHaveProperty('OwnerCreateResolver'); +}); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformer.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformer.test.ts index afd6f0217b..b7493a4673 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformer.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformer.test.ts @@ -1,182 +1,184 @@ import TemplateContext, { RelationalDBSchemaTransformer, TableContext } from '../RelationalDBSchemaTransformer'; -import { Kind, print } from 'graphql' +import { Kind, print } from 'graphql'; import { RelationalDBParsingException } from '../RelationalDBParsingException'; import { IRelationalDBReader } from '../IRelationalDBReader'; -import { getNamedType, getNonNullType, getInputValueDefinition, getGraphQLTypeFromMySQLType, - getTypeDefinition, getFieldDefinition, getInputTypeDefinition } from '../RelationalDBSchemaTransformerUtils' -import { toUpper } from 'graphql-transformer-common' - - - -const testDBName = 'testdb' -const mockTableAName = 'a' -const mockTableBName = 'b' -const mockTableCName = 'c' -const mockTableDName = 'd' -const region = 'us-east-1' -const secretStoreArn = 'secretStoreArn' -const clusterArn = 'clusterArn' +import { + getNamedType, + getNonNullType, + getInputValueDefinition, + getGraphQLTypeFromMySQLType, + getTypeDefinition, + getFieldDefinition, + getInputTypeDefinition, +} from '../RelationalDBSchemaTransformerUtils'; +import { toUpper } from 'graphql-transformer-common'; + +const testDBName = 'testdb'; +const mockTableAName = 'a'; +const mockTableBName = 'b'; +const mockTableCName = 'c'; +const mockTableDName = 'd'; +const region = 'us-east-1'; +const secretStoreArn = 'secretStoreArn'; +const clusterArn = 'clusterArn'; function getTableContext(tableName: string): TableContext { - const fields = new Array() - const updateFields = new Array() - const createFields = new Array() - const primaryKey = 'id' - const primaryKeyType = 'VarChar(128)' - const stringFieldList = ['name', 'description'] - const formattedTableName = toUpper(tableName) - - for (const fieldName of stringFieldList) { - - const baseType = getNamedType(getGraphQLTypeFromMySQLType(primaryKeyType)) - - const type = getNonNullType(baseType) - fields.push(getFieldDefinition(fieldName, type)) - - createFields.push(getInputValueDefinition(type, fieldName)) - - let updateType = null - if (primaryKey === fieldName) { - updateType = getNonNullType(baseType) - } else { - updateType = baseType - } - updateFields.push(getInputValueDefinition(updateType, fieldName)) + const fields = new Array(); + const updateFields = new Array(); + const createFields = new Array(); + const primaryKey = 'id'; + const primaryKeyType = 'VarChar(128)'; + const stringFieldList = ['name', 'description']; + const formattedTableName = toUpper(tableName); + + for (const fieldName of stringFieldList) { + const baseType = getNamedType(getGraphQLTypeFromMySQLType(primaryKeyType)); + + const type = getNonNullType(baseType); + fields.push(getFieldDefinition(fieldName, type)); + + createFields.push(getInputValueDefinition(type, fieldName)); + + let updateType = null; + if (primaryKey === fieldName) { + updateType = getNonNullType(baseType); + } else { + updateType = baseType; } - return new TableContext(getTypeDefinition(fields, tableName), - getInputTypeDefinition(createFields, `Create${formattedTableName}Input`), - getInputTypeDefinition(updateFields, `Update${formattedTableName}Input`), primaryKey, - primaryKeyType, stringFieldList, []) + updateFields.push(getInputValueDefinition(updateType, fieldName)); + } + return new TableContext( + getTypeDefinition(fields, tableName), + getInputTypeDefinition(createFields, `Create${formattedTableName}Input`), + getInputTypeDefinition(updateFields, `Update${formattedTableName}Input`), + primaryKey, + primaryKeyType, + stringFieldList, + [] + ); } -test('Test schema generation end to end', async() => { - - const MockRelationalDBReader = jest.fn(() => ({ - listTables: jest.fn(() => { - return [ mockTableAName, mockTableBName, mockTableCName, mockTableDName] - }), - describeTable: jest.fn((tableName: string) => { - return getTableContext(tableName) - }), - hydrateTemplateContext: jest.fn((contextShell: TemplateContext) => { - contextShell.secretStoreArn = secretStoreArn - contextShell.rdsClusterIdentifier = clusterArn - contextShell.databaseSchema = 'mysql' - contextShell.databaseName = testDBName - contextShell.region = region - return contextShell - }) - })) - - const mockReader = new MockRelationalDBReader() - const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName) - - const templateContext = await dummyTransformer.introspectDatabaseSchema() - - expect(mockReader.listTables).toHaveBeenCalled() - expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableAName) - expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableBName) - expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableCName) - expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableDName) - expect(templateContext.schemaDoc).toBeDefined() - expect(templateContext.schemaDoc.kind).toBe(Kind.DOCUMENT) - // 4 tables * (base, update input, and create input) + schema, queries, mutations, and subs - // (4 * 3) + 4 = 16 - expect(templateContext.schemaDoc.definitions.length).toBe(16) - let objectTypeCount = 0 - let inputTypeCount = 0 - let schemaTypeCount = 0 - for (const node of templateContext.schemaDoc.definitions) { - if (node.kind === Kind.OBJECT_TYPE_DEFINITION) { - objectTypeCount++ - } else if (node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { - inputTypeCount++ - } else if (node.kind === Kind.SCHEMA_DEFINITION) { - schemaTypeCount++ - } +test('Test schema generation end to end', async () => { + const MockRelationalDBReader = jest.fn(() => ({ + listTables: jest.fn(() => { + return [mockTableAName, mockTableBName, mockTableCName, mockTableDName]; + }), + describeTable: jest.fn((tableName: string) => { + return getTableContext(tableName); + }), + hydrateTemplateContext: jest.fn((contextShell: TemplateContext) => { + contextShell.secretStoreArn = secretStoreArn; + contextShell.rdsClusterIdentifier = clusterArn; + contextShell.databaseSchema = 'mysql'; + contextShell.databaseName = testDBName; + contextShell.region = region; + return contextShell; + }), + })); + + const mockReader = new MockRelationalDBReader(); + const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName); + + const templateContext = await dummyTransformer.introspectDatabaseSchema(); + + expect(mockReader.listTables).toHaveBeenCalled(); + expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableAName); + expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableBName); + expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableCName); + expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableDName); + expect(templateContext.schemaDoc).toBeDefined(); + expect(templateContext.schemaDoc.kind).toBe(Kind.DOCUMENT); + // 4 tables * (base, update input, and create input) + schema, queries, mutations, and subs + // (4 * 3) + 4 = 16 + expect(templateContext.schemaDoc.definitions.length).toBe(16); + let objectTypeCount = 0; + let inputTypeCount = 0; + let schemaTypeCount = 0; + for (const node of templateContext.schemaDoc.definitions) { + if (node.kind === Kind.OBJECT_TYPE_DEFINITION) { + objectTypeCount++; + } else if (node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { + inputTypeCount++; + } else if (node.kind === Kind.SCHEMA_DEFINITION) { + schemaTypeCount++; } - expect(schemaTypeCount).toEqual(1) // Singular schema node - expect(inputTypeCount).toEqual(8) // 4 tables * (update input + create input) - expect(objectTypeCount).toEqual(7) // (4 tables * base shape) + queries + mutations + subs - const schemaString = print(templateContext.schemaDoc) - expect(schemaString).toBeDefined() - console.log(schemaString) -}) - -test('Test list tables fails', async() => { - const MockRelationalDBReader = jest.fn(() => ({ - listTables: jest.fn(() => { - throw new Error('Mocked failure on list tables.') - }), - describeTable: jest.fn(() => { - throw new Error('Mocked failure on describe. THIS SHOULD NOT HAPPEN.') - }) - })) - const mockReader = new MockRelationalDBReader() - const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName) - - - try { - await dummyTransformer.introspectDatabaseSchema() - throw new Error('Request should have failed.') - } catch (err) { - if (err instanceof RelationalDBParsingException) { - // expected - } else { - throw new Error('Unexpected exception thrown.') - } - + } + expect(schemaTypeCount).toEqual(1); // Singular schema node + expect(inputTypeCount).toEqual(8); // 4 tables * (update input + create input) + expect(objectTypeCount).toEqual(7); // (4 tables * base shape) + queries + mutations + subs + const schemaString = print(templateContext.schemaDoc); + expect(schemaString).toBeDefined(); + console.log(schemaString); +}); + +test('Test list tables fails', async () => { + const MockRelationalDBReader = jest.fn(() => ({ + listTables: jest.fn(() => { + throw new Error('Mocked failure on list tables.'); + }), + describeTable: jest.fn(() => { + throw new Error('Mocked failure on describe. THIS SHOULD NOT HAPPEN.'); + }), + })); + const mockReader = new MockRelationalDBReader(); + const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName); + + try { + await dummyTransformer.introspectDatabaseSchema(); + throw new Error('Request should have failed.'); + } catch (err) { + if (err instanceof RelationalDBParsingException) { + // expected + } else { + throw new Error('Unexpected exception thrown.'); } - expect(mockReader.listTables).toHaveBeenCalled() - expect(mockReader.describeTable).not.toHaveBeenCalled() -}) - -test('Test describe table fails', async() => { - const MockRelationalDBReader = jest.fn(() => ({ - listTables: jest.fn(() => { - return [ mockTableAName, mockTableBName, mockTableCName, mockTableDName] - }), - describeTable: jest.fn(() => { - throw new Error('Mocked failure on describe.') - }) - })) - const mockReader = new MockRelationalDBReader() - const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName) - - try { - await dummyTransformer.introspectDatabaseSchema() - throw new Error('Request should have failed.') - } catch (err) { - if (err instanceof RelationalDBParsingException) { - // expected - } else { - throw new Error('Unexpected exception thrown.') - } - + } + expect(mockReader.listTables).toHaveBeenCalled(); + expect(mockReader.describeTable).not.toHaveBeenCalled(); +}); + +test('Test describe table fails', async () => { + const MockRelationalDBReader = jest.fn(() => ({ + listTables: jest.fn(() => { + return [mockTableAName, mockTableBName, mockTableCName, mockTableDName]; + }), + describeTable: jest.fn(() => { + throw new Error('Mocked failure on describe.'); + }), + })); + const mockReader = new MockRelationalDBReader(); + const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName); + + try { + await dummyTransformer.introspectDatabaseSchema(); + throw new Error('Request should have failed.'); + } catch (err) { + if (err instanceof RelationalDBParsingException) { + // expected + } else { + throw new Error('Unexpected exception thrown.'); } + } - expect(mockReader.listTables).toHaveBeenCalled() - expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableAName) -}) + expect(mockReader.listTables).toHaveBeenCalled(); + expect(mockReader.describeTable).toHaveBeenCalledWith(mockTableAName); +}); test('Test connection type shape', () => { - const testType = 'type name' - const MockRelationalDBReader = jest.fn(() => ({ - })) - const mockReader = new MockRelationalDBReader() - const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName) - const connectionType = dummyTransformer.getConnectionType(testType) - expect(connectionType.fields.length).toEqual(2) - expect(connectionType.name.value).toEqual(`${testType}Connection`) -}) - + const testType = 'type name'; + const MockRelationalDBReader = jest.fn(() => ({})); + const mockReader = new MockRelationalDBReader(); + const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName); + const connectionType = dummyTransformer.getConnectionType(testType); + expect(connectionType.fields.length).toEqual(2); + expect(connectionType.name.value).toEqual(`${testType}Connection`); +}); test('Test schema type node creation', () => { - const MockRelationalDBReader = jest.fn(() => ({ - })) - const mockReader = new MockRelationalDBReader() - const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName) - const schemaNode = dummyTransformer.getSchemaType() - expect(schemaNode.kind).toEqual(Kind.SCHEMA_DEFINITION) - expect(schemaNode.operationTypes.length).toEqual(3) -}) + const MockRelationalDBReader = jest.fn(() => ({})); + const mockReader = new MockRelationalDBReader(); + const dummyTransformer = new RelationalDBSchemaTransformer(mockReader, testDBName); + const schemaNode = dummyTransformer.getSchemaType(); + expect(schemaNode.kind).toEqual(Kind.SCHEMA_DEFINITION); + expect(schemaNode.operationTypes.length).toEqual(3); +}); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformerUtils.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformerUtils.test.ts index 59721d1948..be169232a6 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformerUtils.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBSchemaTransformerUtils.test.ts @@ -1,181 +1,183 @@ -import { Kind } from 'graphql' -import { getNamedType, getOperationFieldDefinition, getNonNullType, getInputValueDefinition, - getTypeDefinition, getFieldDefinition, getDirectiveNode, getOperationTypeDefinition, - getNameNode, getListValueNode, getStringValueNode, getInputTypeDefinition, - getArgumentNode, getGraphQLTypeFromMySQLType } from '../RelationalDBSchemaTransformerUtils' - +import { Kind } from 'graphql'; +import { + getNamedType, + getOperationFieldDefinition, + getNonNullType, + getInputValueDefinition, + getTypeDefinition, + getFieldDefinition, + getDirectiveNode, + getOperationTypeDefinition, + getNameNode, + getListValueNode, + getStringValueNode, + getInputTypeDefinition, + getArgumentNode, + getGraphQLTypeFromMySQLType, +} from '../RelationalDBSchemaTransformerUtils'; test('Test operation type node creation', () => { - const operationType = 'query' - const namedNode = getNamedType('Query') - const operationTypeNode = getOperationTypeDefinition(operationType, namedNode) - expect(operationTypeNode.kind).toEqual(Kind.OPERATION_TYPE_DEFINITION) - expect(operationTypeNode.operation).toEqual(operationType) - expect(operationTypeNode.type).toEqual(namedNode) -}) + const operationType = 'query'; + const namedNode = getNamedType('Query'); + const operationTypeNode = getOperationTypeDefinition(operationType, namedNode); + expect(operationTypeNode.kind).toEqual(Kind.OPERATION_TYPE_DEFINITION); + expect(operationTypeNode.operation).toEqual(operationType); + expect(operationTypeNode.type).toEqual(namedNode); +}); test('Test non null type node creation', () => { - const namedTypeNode = getNamedType('test name') - const nonNullNamedTypeNode = getNonNullType(namedTypeNode) - expect(nonNullNamedTypeNode.kind).toEqual(Kind.NON_NULL_TYPE) - expect(nonNullNamedTypeNode.type).toEqual(namedTypeNode) -}) + const namedTypeNode = getNamedType('test name'); + const nonNullNamedTypeNode = getNonNullType(namedTypeNode); + expect(nonNullNamedTypeNode.kind).toEqual(Kind.NON_NULL_TYPE); + expect(nonNullNamedTypeNode.type).toEqual(namedTypeNode); +}); test('Test named type node creation', () => { - const name = 'test name' - const namedTypeNode = getNamedType(name) - expect(namedTypeNode.kind).toEqual(Kind.NAMED_TYPE) - expect(namedTypeNode.name.value).toEqual(name) -}) + const name = 'test name'; + const namedTypeNode = getNamedType(name); + expect(namedTypeNode.kind).toEqual(Kind.NAMED_TYPE); + expect(namedTypeNode.name.value).toEqual(name); +}); test('Test input value definition node creation', () => { - const name = 'input name' - const nameNode = getNamedType('type name') - const inputDefinitionNode = getInputValueDefinition(nameNode, name) - expect(inputDefinitionNode.kind).toEqual(Kind.INPUT_VALUE_DEFINITION) - expect(inputDefinitionNode.type).toEqual(nameNode) - expect(inputDefinitionNode.name.value).toEqual(name) -}) + const name = 'input name'; + const nameNode = getNamedType('type name'); + const inputDefinitionNode = getInputValueDefinition(nameNode, name); + expect(inputDefinitionNode.kind).toEqual(Kind.INPUT_VALUE_DEFINITION); + expect(inputDefinitionNode.type).toEqual(nameNode); + expect(inputDefinitionNode.name.value).toEqual(name); +}); test('Test operation field definition node creation', () => { - const name = 'field name' - const args = [ - getInputValueDefinition(null, 'test name') - ] - const namedNode = getNamedType('test name') - const operationFieldDefinitionNode = getOperationFieldDefinition(name, args, namedNode, null) - expect(operationFieldDefinitionNode.kind).toEqual(Kind.FIELD_DEFINITION) - expect(operationFieldDefinitionNode.type).toEqual(namedNode) - expect(operationFieldDefinitionNode.arguments).toEqual(args) - -}) + const name = 'field name'; + const args = [getInputValueDefinition(null, 'test name')]; + const namedNode = getNamedType('test name'); + const operationFieldDefinitionNode = getOperationFieldDefinition(name, args, namedNode, null); + expect(operationFieldDefinitionNode.kind).toEqual(Kind.FIELD_DEFINITION); + expect(operationFieldDefinitionNode.type).toEqual(namedNode); + expect(operationFieldDefinitionNode.arguments).toEqual(args); +}); test('Test field definition node creation', () => { - const fieldName = 'field name' - const namedNode = getNamedType('type name') - const fieldDefinitionNode = getFieldDefinition(fieldName, namedNode) - expect(fieldDefinitionNode.kind).toEqual(Kind.FIELD_DEFINITION) - expect(fieldDefinitionNode.type).toEqual(namedNode) - expect(fieldDefinitionNode.name.value).toEqual(fieldName) -}) + const fieldName = 'field name'; + const namedNode = getNamedType('type name'); + const fieldDefinitionNode = getFieldDefinition(fieldName, namedNode); + expect(fieldDefinitionNode.kind).toEqual(Kind.FIELD_DEFINITION); + expect(fieldDefinitionNode.type).toEqual(namedNode); + expect(fieldDefinitionNode.name.value).toEqual(fieldName); +}); test('Test type definition node creation', () => { - const fieldList = [ - getFieldDefinition('field name', null) - ] - const typeName = 'type name' - const typeDefinitionNode = getTypeDefinition(fieldList, typeName) - expect(typeDefinitionNode.kind).toEqual(Kind.OBJECT_TYPE_DEFINITION) - expect(typeDefinitionNode.name.value).toEqual(typeName) - expect(typeDefinitionNode.fields).toEqual(fieldList) -}) + const fieldList = [getFieldDefinition('field name', null)]; + const typeName = 'type name'; + const typeDefinitionNode = getTypeDefinition(fieldList, typeName); + expect(typeDefinitionNode.kind).toEqual(Kind.OBJECT_TYPE_DEFINITION); + expect(typeDefinitionNode.name.value).toEqual(typeName); + expect(typeDefinitionNode.fields).toEqual(fieldList); +}); test('Test name node creaton', () => { - const name = 'name string' - const nameNode = getNameNode(name) - expect(nameNode.kind).toEqual(Kind.NAME) - expect(nameNode.value).toEqual(name) -}) + const name = 'name string'; + const nameNode = getNameNode(name); + expect(nameNode.kind).toEqual(Kind.NAME); + expect(nameNode.value).toEqual(name); +}); test('Test list value node creation', () => { - const valueList = [ - getStringValueNode('string a'), - getStringValueNode('string b') - ] - const listValueNode = getListValueNode(valueList) - expect(listValueNode.kind).toEqual(Kind.LIST) - expect(listValueNode.values).toEqual(valueList) -}) + const valueList = [getStringValueNode('string a'), getStringValueNode('string b')]; + const listValueNode = getListValueNode(valueList); + expect(listValueNode.kind).toEqual(Kind.LIST); + expect(listValueNode.values).toEqual(valueList); +}); test('Test object type node creation', () => { - const name = 'name' - const inputNode = getInputTypeDefinition([], name) - expect(inputNode.kind).toEqual(Kind.INPUT_OBJECT_TYPE_DEFINITION) - expect(inputNode.fields.length).toEqual(0) - expect(inputNode.name.value).toEqual(name) -}) + const name = 'name'; + const inputNode = getInputTypeDefinition([], name); + expect(inputNode.kind).toEqual(Kind.INPUT_OBJECT_TYPE_DEFINITION); + expect(inputNode.fields.length).toEqual(0); + expect(inputNode.name.value).toEqual(name); +}); test('Test string value node creation', () => { - const stringValue = 'string value' - const stringValueNode = getStringValueNode(stringValue) - expect(stringValueNode.kind).toEqual(Kind.STRING) - expect(stringValueNode.value).toEqual(stringValue) -}) + const stringValue = 'string value'; + const stringValueNode = getStringValueNode(stringValue); + expect(stringValueNode.kind).toEqual(Kind.STRING); + expect(stringValueNode.value).toEqual(stringValue); +}); test('Test directive node creation', () => { - const directiveNode = getDirectiveNode('directive name') - expect(directiveNode.kind).toEqual(Kind.DIRECTIVE) - expect(directiveNode.name).toBeDefined() - expect(directiveNode.arguments.length).toEqual(1) - -}) + const directiveNode = getDirectiveNode('directive name'); + expect(directiveNode.kind).toEqual(Kind.DIRECTIVE); + expect(directiveNode.name).toBeDefined(); + expect(directiveNode.arguments.length).toEqual(1); +}); test('Test argument node creation', () => { - const argumentNode = getArgumentNode('argument name') - expect(argumentNode.kind).toEqual(Kind.ARGUMENT) - expect(argumentNode.name).toBeDefined() - expect(argumentNode.value).toBeDefined() - expect(argumentNode.value.kind).toEqual(Kind.LIST) -}) + const argumentNode = getArgumentNode('argument name'); + expect(argumentNode.kind).toEqual(Kind.ARGUMENT); + expect(argumentNode.name).toBeDefined(); + expect(argumentNode.value).toBeDefined(); + expect(argumentNode.value.kind).toEqual(Kind.LIST); +}); test('Test type conversion to AWSDateTime', () => { - expect(getGraphQLTypeFromMySQLType('datetime')).toEqual('AWSDateTime') -}) + expect(getGraphQLTypeFromMySQLType('datetime')).toEqual('AWSDateTime'); +}); test('Test type conversion to AWSDate', () => { - expect(getGraphQLTypeFromMySQLType('date')).toEqual('AWSDate') -}) + expect(getGraphQLTypeFromMySQLType('date')).toEqual('AWSDate'); +}); test('Test type conversion to AWSTime', () => { - expect(getGraphQLTypeFromMySQLType('time')).toEqual('AWSTime') -}) + expect(getGraphQLTypeFromMySQLType('time')).toEqual('AWSTime'); +}); test('Test type conversion to AWSTimestamp', () => { - expect(getGraphQLTypeFromMySQLType('timestamp')).toEqual('AWSTimestamp') -}) + expect(getGraphQLTypeFromMySQLType('timestamp')).toEqual('AWSTimestamp'); +}); test('Test type conversion to AWSJSON', () => { - expect(getGraphQLTypeFromMySQLType('jSoN')).toEqual('AWSJSON') -}) + expect(getGraphQLTypeFromMySQLType('jSoN')).toEqual('AWSJSON'); +}); test('Test type conversion to Boolean', () => { - expect(getGraphQLTypeFromMySQLType('BOOl')).toEqual('Boolean') -}) + expect(getGraphQLTypeFromMySQLType('BOOl')).toEqual('Boolean'); +}); test('Test type conversion to Int', () => { - expect(getGraphQLTypeFromMySQLType('Int')).toEqual('Int') - expect(getGraphQLTypeFromMySQLType('Int(100)')).toEqual('Int') - expect(getGraphQLTypeFromMySQLType('inteGER')).toEqual('Int') - expect(getGraphQLTypeFromMySQLType('SmaLLInT')).toEqual('Int') - expect(getGraphQLTypeFromMySQLType('TINYint')).toEqual('Int') - expect(getGraphQLTypeFromMySQLType('mediumInt')).toEqual('Int') - expect(getGraphQLTypeFromMySQLType('BIGINT')).toEqual('Int') - expect(getGraphQLTypeFromMySQLType('BIT')).toEqual('Int') -}) + expect(getGraphQLTypeFromMySQLType('Int')).toEqual('Int'); + expect(getGraphQLTypeFromMySQLType('Int(100)')).toEqual('Int'); + expect(getGraphQLTypeFromMySQLType('inteGER')).toEqual('Int'); + expect(getGraphQLTypeFromMySQLType('SmaLLInT')).toEqual('Int'); + expect(getGraphQLTypeFromMySQLType('TINYint')).toEqual('Int'); + expect(getGraphQLTypeFromMySQLType('mediumInt')).toEqual('Int'); + expect(getGraphQLTypeFromMySQLType('BIGINT')).toEqual('Int'); + expect(getGraphQLTypeFromMySQLType('BIT')).toEqual('Int'); +}); test('Test type conversion to Float', () => { - expect(getGraphQLTypeFromMySQLType('FloAT')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('DOUBle')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('REAL')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('REAL_as_FLOAT')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('DOUBLE precision')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('DEC')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('DeciMAL')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('FIXED')).toEqual('Float') - expect(getGraphQLTypeFromMySQLType('Numeric')).toEqual('Float') -}) + expect(getGraphQLTypeFromMySQLType('FloAT')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('DOUBle')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('REAL')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('REAL_as_FLOAT')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('DOUBLE precision')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('DEC')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('DeciMAL')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('FIXED')).toEqual('Float'); + expect(getGraphQLTypeFromMySQLType('Numeric')).toEqual('Float'); +}); test('Test type conversion defaults to String', () => { - expect(getGraphQLTypeFromMySQLType('gibberish random stuff')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('timesta')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('boo')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('jso')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('tim')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('ate')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('atetime')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('Inte')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('Bigin')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('DECI')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('floatt')).toEqual('String') - expect(getGraphQLTypeFromMySQLType('FIXE')).toEqual('String') -}) \ No newline at end of file + expect(getGraphQLTypeFromMySQLType('gibberish random stuff')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('timesta')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('boo')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('jso')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('tim')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('ate')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('atetime')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('Inte')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('Bigin')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('DECI')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('floatt')).toEqual('String'); + expect(getGraphQLTypeFromMySQLType('FIXE')).toEqual('String'); +}); diff --git a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts index 708a84b187..ebec3a4bd6 100644 --- a/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts +++ b/packages/graphql-relational-schema-transformer/src/__tests__/RelationalDBTemplateGenerator.test.ts @@ -1,12 +1,10 @@ -jest.mock('../RelationalDBResolverGenerator') +jest.mock('../RelationalDBResolverGenerator'); -import RelationalDBTemplateGenerator from '../RelationalDBTemplateGenerator' +import RelationalDBTemplateGenerator from '../RelationalDBTemplateGenerator'; import TemplateContext from '../RelationalDBSchemaTransformer'; import { parse } from 'graphql'; -import { ResourceConstants as CommonResourceConstants } from 'graphql-transformer-common' -import { ResourceConstants } from '../ResourceConstants' - - +import { ResourceConstants as CommonResourceConstants } from 'graphql-transformer-common'; +import { ResourceConstants } from '../ResourceConstants'; const schema = parse(` type User { @@ -19,69 +17,68 @@ const schema = parse(` } `); -let simplePrimaryKeyMap = new Map() -let simpleStringFieldMap = new Map() -let simpleIntFieldMap = new Map() -let context = new TemplateContext(schema, simplePrimaryKeyMap, simpleStringFieldMap, simpleIntFieldMap) +let simplePrimaryKeyMap = new Map(); +let simpleStringFieldMap = new Map(); +let simpleIntFieldMap = new Map(); +let context = new TemplateContext(schema, simplePrimaryKeyMap, simpleStringFieldMap, simpleIntFieldMap); context.secretStoreArn = - 'arn:aws:secretsmanager:us-east-1:123456789012:secret:rds-db-credentials/cluster-ABCDEFXHGNMSRTNK2A75532FKL/ashwin-aqwery' -context.rdsClusterIdentifier = 'arn:aws:rds:us-east-1:123456789012:cluster:pets' -context.databaseSchema = 'mysql' -context.databaseName = 'pets' -context.region = 'us-east-1' + 'arn:aws:secretsmanager:us-east-1:123456789012:secret:rds-db-credentials/cluster-ABCDEFXHGNMSRTNK2A75532FKL/ashwin-aqwery'; +context.rdsClusterIdentifier = 'arn:aws:rds:us-east-1:123456789012:cluster:pets'; +context.databaseSchema = 'mysql'; +context.databaseName = 'pets'; +context.region = 'us-east-1'; const emptyCliContext = { - amplify: { - loadEnvResourceParameters(context: any, category: string, service: string) { - return { - } - } - } -} + amplify: { + loadEnvResourceParameters(context: any, category: string, service: string) { + return {}; + }, + }, +}; -const templateGenerator = new RelationalDBTemplateGenerator(context) +const templateGenerator = new RelationalDBTemplateGenerator(context); /** * Test for verifying that provided a valid TemplateContext, we are * generating the base cloudform template (the cfn specs sans resolvers) */ test('Test Base CloudForm Template Generation', () => { - const template = templateGenerator.createTemplate(emptyCliContext) + const template = templateGenerator.createTemplate(emptyCliContext); - expect(template).toBeDefined() - expect(template.AWSTemplateFormatVersion).toBeDefined() - expect(template.AWSTemplateFormatVersion).toBe('2010-09-09') - expect(template.Parameters).toBeDefined() - expect(template.Parameters).toHaveProperty(ResourceConstants.PARAMETERS.AppSyncApiName) + expect(template).toBeDefined(); + expect(template.AWSTemplateFormatVersion).toBeDefined(); + expect(template.AWSTemplateFormatVersion).toBe('2010-09-09'); + expect(template.Parameters).toBeDefined(); + expect(template.Parameters).toHaveProperty(ResourceConstants.PARAMETERS.AppSyncApiName); - // Verify Resources were created as expected - expect(template.Resources).toBeDefined() - expect(template.Resources).toHaveProperty(ResourceConstants.RESOURCES.RelationalDatabaseAccessRole) - expect(template.Resources).toHaveProperty(ResourceConstants.RESOURCES.RelationalDatabaseAccessRole) -}) + // Verify Resources were created as expected + expect(template.Resources).toBeDefined(); + expect(template.Resources).toHaveProperty(ResourceConstants.RESOURCES.RelationalDatabaseAccessRole); + expect(template.Resources).toHaveProperty(ResourceConstants.RESOURCES.RelationalDatabaseAccessRole); +}); /** * Test for verifying that provided a base template, we are generating * a template with the Relational Resolvers attached. */ test('Test Adding Resolvers to CloudForm Template', () => { - const baseTemplate = templateGenerator.createTemplate(emptyCliContext) - expect(baseTemplate).toBeDefined() - expect(baseTemplate.Resources).toBeDefined() - expect(baseTemplate.Resources).not.toHaveProperty('') + const baseTemplate = templateGenerator.createTemplate(emptyCliContext); + expect(baseTemplate).toBeDefined(); + expect(baseTemplate.Resources).toBeDefined(); + expect(baseTemplate.Resources).not.toHaveProperty(''); - const finalTemplate = templateGenerator.addRelationalResolvers(baseTemplate, 'someFilePath') - expect(finalTemplate).toBeDefined() -}) + const finalTemplate = templateGenerator.addRelationalResolvers(baseTemplate, 'someFilePath'); + expect(finalTemplate).toBeDefined(); +}); /** * Test for verifying that provided a base template, we are generating the * cfn template as a JSON string. */ test('Test Printing the cloudform template as a JSON', () => { - const baseTemplate = templateGenerator.createTemplate(emptyCliContext) - expect(baseTemplate).toBeDefined() + const baseTemplate = templateGenerator.createTemplate(emptyCliContext); + expect(baseTemplate).toBeDefined(); - const templateJSON = templateGenerator.printCloudformationTemplate(baseTemplate) - expect(templateJSON).toBeDefined() -}) \ No newline at end of file + const templateJSON = templateGenerator.printCloudformationTemplate(baseTemplate); + expect(templateJSON).toBeDefined(); +}); diff --git a/packages/graphql-relational-schema-transformer/src/index.ts b/packages/graphql-relational-schema-transformer/src/index.ts index 591d923dcf..13433f0135 100644 --- a/packages/graphql-relational-schema-transformer/src/index.ts +++ b/packages/graphql-relational-schema-transformer/src/index.ts @@ -1,5 +1,5 @@ -import { RelationalDBSchemaTransformer } from './RelationalDBSchemaTransformer' -import RelationalDBTemplateGenerator from './RelationalDBTemplateGenerator' +import { RelationalDBSchemaTransformer } from './RelationalDBSchemaTransformer'; +import RelationalDBTemplateGenerator from './RelationalDBTemplateGenerator'; import { DataApiParams } from './AuroraDataAPIClient'; import { AuroraServerlessMySQLDatabaseReader } from './AuroraServerlessMySQLDatabaseReader'; -export { RelationalDBSchemaTransformer, RelationalDBTemplateGenerator, DataApiParams, AuroraServerlessMySQLDatabaseReader} \ No newline at end of file +export { RelationalDBSchemaTransformer, RelationalDBTemplateGenerator, DataApiParams, AuroraServerlessMySQLDatabaseReader }; diff --git a/packages/graphql-transformer-common/src/FunctionResourceIDs.ts b/packages/graphql-transformer-common/src/FunctionResourceIDs.ts index 8afafc92f1..f53f2721be 100644 --- a/packages/graphql-transformer-common/src/FunctionResourceIDs.ts +++ b/packages/graphql-transformer-common/src/FunctionResourceIDs.ts @@ -1,16 +1,15 @@ import { simplifyName } from './util'; export class FunctionResourceIDs { + static FunctionDataSourceID(name: string, region?: string): string { + return `${simplifyName(name)}${simplifyName(region || '')}LambdaDataSource`; + } - static FunctionDataSourceID(name: string, region?: string): string { - return `${simplifyName(name)}${simplifyName(region || '')}LambdaDataSource` - } + static FunctionIAMRoleID(name: string, region?: string): string { + return `${FunctionResourceIDs.FunctionDataSourceID(name, region)}Role`; + } - static FunctionIAMRoleID(name: string, region?: string): string { - return `${FunctionResourceIDs.FunctionDataSourceID(name, region)}Role` - } - - static FunctionAppSyncFunctionConfigurationID(name: string, region?: string): string { - return `Invoke${FunctionResourceIDs.FunctionDataSourceID(name, region)}` - } + static FunctionAppSyncFunctionConfigurationID(name: string, region?: string): string { + return `Invoke${FunctionResourceIDs.FunctionDataSourceID(name, region)}`; + } } diff --git a/packages/graphql-transformer-common/src/HttpResourceIDs.ts b/packages/graphql-transformer-common/src/HttpResourceIDs.ts index 2ee541616c..18aa172d77 100644 --- a/packages/graphql-transformer-common/src/HttpResourceIDs.ts +++ b/packages/graphql-transformer-common/src/HttpResourceIDs.ts @@ -1,8 +1,8 @@ import { graphqlName } from './util'; export class HttpResourceIDs { - static HttpDataSourceID(baseURL: string): string { - // strip the special characters out of baseURL to make the data source ID - return `${graphqlName(baseURL)}DataSource` - } + static HttpDataSourceID(baseURL: string): string { + // strip the special characters out of baseURL to make the data source ID + return `${graphqlName(baseURL)}DataSource`; + } } diff --git a/packages/graphql-transformer-common/src/ModelResourceIDs.ts b/packages/graphql-transformer-common/src/ModelResourceIDs.ts index ad66ca9c5d..247d160b40 100644 --- a/packages/graphql-transformer-common/src/ModelResourceIDs.ts +++ b/packages/graphql-transformer-common/src/ModelResourceIDs.ts @@ -1,91 +1,90 @@ -import { graphqlName, toUpper, toCamelCase, simplifyName } from './util' -import { DEFAULT_SCALARS } from './definition' +import { graphqlName, toUpper, toCamelCase, simplifyName } from './util'; +import { DEFAULT_SCALARS } from './definition'; export class ModelResourceIDs { + static ModelTableResourceID(typeName: string): string { + return `${typeName}Table`; + } + static ModelTableStreamArn(typeName: string): string { + return `${typeName}TableStreamArn`; + } + static ModelTableDataSourceID(typeName: string): string { + return `${typeName}DataSource`; + } + static ModelTableIAMRoleID(typeName: string): string { + return `${typeName}IAMRole`; + } + static ModelFilterInputTypeName(name: string): string { + const nameOverride = DEFAULT_SCALARS[name]; + if (nameOverride) { + return `Model${nameOverride}FilterInput`; + } + return `Model${name}FilterInput`; + } + static ModelKeyConditionInputTypeName(name: string): string { + const nameOverride = DEFAULT_SCALARS[name]; + if (nameOverride) { + return `Model${nameOverride}KeyConditionInput`; + } + return `Model${name}KeyConditionInput`; + } + static ModelCompositeKeyArgumentName(keyFieldNames: string[]) { + return toCamelCase(keyFieldNames.map(n => graphqlName(n))); + } + static ModelCompositeKeySeparator() { + return '#'; + } + static ModelCompositeAttributeName(keyFieldNames: string[]) { + return keyFieldNames.join(ModelResourceIDs.ModelCompositeKeySeparator()); + } + static ModelCompositeKeyConditionInputTypeName(modelName: string, keyName: string): string { + return `Model${modelName}${keyName}CompositeKeyConditionInput`; + } + static ModelCompositeKeyInputTypeName(modelName: string, keyName: string): string { + return `Model${modelName}${keyName}CompositeKeyInput`; + } + static ModelFilterListInputTypeName(name: string): string { + const nameOverride = DEFAULT_SCALARS[name]; + if (nameOverride) { + return `Model${nameOverride}ListFilterInput`; + } + return `Model${name}ListFilterInput`; + } - static ModelTableResourceID(typeName: string): string { - return `${typeName}Table` - } - static ModelTableStreamArn(typeName: string): string { - return `${typeName}TableStreamArn` - } - static ModelTableDataSourceID(typeName: string): string { - return `${typeName}DataSource` - } - static ModelTableIAMRoleID(typeName: string): string { - return `${typeName}IAMRole` - } - static ModelFilterInputTypeName(name: string): string { - const nameOverride = DEFAULT_SCALARS[name] - if (nameOverride) { - return `Model${nameOverride}FilterInput` - } - return `Model${name}FilterInput` - } - static ModelKeyConditionInputTypeName(name: string): string { - const nameOverride = DEFAULT_SCALARS[name] - if (nameOverride) { - return `Model${nameOverride}KeyConditionInput` - } - return `Model${name}KeyConditionInput` - } - static ModelCompositeKeyArgumentName(keyFieldNames: string[]) { - return toCamelCase(keyFieldNames.map(n => graphqlName(n))); - } - static ModelCompositeKeySeparator() { - return '#'; - } - static ModelCompositeAttributeName(keyFieldNames: string[]) { - return keyFieldNames.join(ModelResourceIDs.ModelCompositeKeySeparator()); - } - static ModelCompositeKeyConditionInputTypeName(modelName: string, keyName: string): string { - return `Model${modelName}${keyName}CompositeKeyConditionInput` - } - static ModelCompositeKeyInputTypeName(modelName: string, keyName: string): string { - return `Model${modelName}${keyName}CompositeKeyInput` - } - static ModelFilterListInputTypeName(name: string): string { - const nameOverride = DEFAULT_SCALARS[name] - if (nameOverride) { - return `Model${nameOverride}ListFilterInput` - } - return `Model${name}ListFilterInput` - } - - static ModelScalarFilterInputTypeName(name: string): string { - return `Model${name}FilterInput` - } - static ModelConnectionTypeName(typeName: string): string { - return `Model${typeName}Connection` - } - static ModelDeleteInputObjectName(typeName: string): string { - return graphqlName('Delete' + toUpper(typeName) + 'Input') - } - static ModelUpdateInputObjectName(typeName: string): string { - return graphqlName('Update' + toUpper(typeName) + 'Input') - } - static ModelCreateInputObjectName(typeName: string): string { - return graphqlName(`Create` + toUpper(typeName) + 'Input') - } - static ModelOnCreateSubscriptionName(typeName: string): string { - return graphqlName(`onCreate` + toUpper(typeName)) - } - static ModelOnUpdateSubscriptionName(typeName: string): string { - return graphqlName(`onUpdate` + toUpper(typeName)) - } - static ModelOnDeleteSubscriptionName(typeName: string): string { - return graphqlName(`onDelete` + toUpper(typeName)) - } - static NonModelInputObjectName(typeName: string): string { - return graphqlName(toUpper(typeName) + 'Input') - } - static UrlParamsInputObjectName(typeName: string, fieldName: string) { - return graphqlName(toUpper(typeName) + toUpper(fieldName) + 'ParamsInput') - } - static HttpQueryInputObjectName(typeName: string, fieldName: string) { - return graphqlName(toUpper(typeName) + toUpper(fieldName) + 'QueryInput') - } - static HttpBodyInputObjectName(typeName: string, fieldName: string) { - return graphqlName(toUpper(typeName) + toUpper(fieldName) + 'BodyInput') - } -} \ No newline at end of file + static ModelScalarFilterInputTypeName(name: string): string { + return `Model${name}FilterInput`; + } + static ModelConnectionTypeName(typeName: string): string { + return `Model${typeName}Connection`; + } + static ModelDeleteInputObjectName(typeName: string): string { + return graphqlName('Delete' + toUpper(typeName) + 'Input'); + } + static ModelUpdateInputObjectName(typeName: string): string { + return graphqlName('Update' + toUpper(typeName) + 'Input'); + } + static ModelCreateInputObjectName(typeName: string): string { + return graphqlName(`Create` + toUpper(typeName) + 'Input'); + } + static ModelOnCreateSubscriptionName(typeName: string): string { + return graphqlName(`onCreate` + toUpper(typeName)); + } + static ModelOnUpdateSubscriptionName(typeName: string): string { + return graphqlName(`onUpdate` + toUpper(typeName)); + } + static ModelOnDeleteSubscriptionName(typeName: string): string { + return graphqlName(`onDelete` + toUpper(typeName)); + } + static NonModelInputObjectName(typeName: string): string { + return graphqlName(toUpper(typeName) + 'Input'); + } + static UrlParamsInputObjectName(typeName: string, fieldName: string) { + return graphqlName(toUpper(typeName) + toUpper(fieldName) + 'ParamsInput'); + } + static HttpQueryInputObjectName(typeName: string, fieldName: string) { + return graphqlName(toUpper(typeName) + toUpper(fieldName) + 'QueryInput'); + } + static HttpBodyInputObjectName(typeName: string, fieldName: string) { + return graphqlName(toUpper(typeName) + toUpper(fieldName) + 'BodyInput'); + } +} diff --git a/packages/graphql-transformer-common/src/ResolverResourceIDs.ts b/packages/graphql-transformer-common/src/ResolverResourceIDs.ts index 1456c72fa5..4d313977f5 100644 --- a/packages/graphql-transformer-common/src/ResolverResourceIDs.ts +++ b/packages/graphql-transformer-common/src/ResolverResourceIDs.ts @@ -1,25 +1,25 @@ -import { graphqlName, toUpper } from "./util"; +import { graphqlName, toUpper } from './util'; export class ResolverResourceIDs { - static DynamoDBCreateResolverResourceID(typeName: string): string { - return `Create${typeName}Resolver` - } - static DynamoDBUpdateResolverResourceID(typeName: string): string { - return `Update${typeName}Resolver` - } - static DynamoDBDeleteResolverResourceID(typeName: string): string { - return `Delete${typeName}Resolver` - } - static DynamoDBGetResolverResourceID(typeName: string): string { - return `Get${typeName}Resolver` - } - static DynamoDBListResolverResourceID(typeName: string): string { - return `List${typeName}Resolver` - } - static ElasticsearchSearchResolverResourceID(typeName: string): string { - return `Search${typeName}Resolver` - } - static ResolverResourceID(typeName: string, fieldName: string): string { - return `${typeName}${fieldName}Resolver` - } -} \ No newline at end of file + static DynamoDBCreateResolverResourceID(typeName: string): string { + return `Create${typeName}Resolver`; + } + static DynamoDBUpdateResolverResourceID(typeName: string): string { + return `Update${typeName}Resolver`; + } + static DynamoDBDeleteResolverResourceID(typeName: string): string { + return `Delete${typeName}Resolver`; + } + static DynamoDBGetResolverResourceID(typeName: string): string { + return `Get${typeName}Resolver`; + } + static DynamoDBListResolverResourceID(typeName: string): string { + return `List${typeName}Resolver`; + } + static ElasticsearchSearchResolverResourceID(typeName: string): string { + return `Search${typeName}Resolver`; + } + static ResolverResourceID(typeName: string, fieldName: string): string { + return `${typeName}${fieldName}Resolver`; + } +} diff --git a/packages/graphql-transformer-common/src/ResourceConstants.ts b/packages/graphql-transformer-common/src/ResourceConstants.ts index f665ea53fb..05be934bfa 100644 --- a/packages/graphql-transformer-common/src/ResourceConstants.ts +++ b/packages/graphql-transformer-common/src/ResourceConstants.ts @@ -1,111 +1,109 @@ export class ResourceConstants { + public static NONE = 'NONE'; - public static NONE = "NONE" + public static readonly RESOURCES = { + // AppSync + GraphQLAPILogicalID: 'GraphQLAPI', + GraphQLSchemaLogicalID: 'GraphQLSchema', + APIKeyLogicalID: 'GraphQLAPIKey', + AuthRolePolicy: 'AuthRolePolicy', + UnauthRolePolicy: 'UnauthRolePolicy', - public static readonly RESOURCES = { - // AppSync - GraphQLAPILogicalID: 'GraphQLAPI', - GraphQLSchemaLogicalID: 'GraphQLSchema', - APIKeyLogicalID: 'GraphQLAPIKey', - AuthRolePolicy: 'AuthRolePolicy', - UnauthRolePolicy: 'UnauthRolePolicy', + // Elasticsearch + ElasticsearchAccessIAMRoleLogicalID: 'ElasticSearchAccessIAMRole', + ElasticsearchDomainLogicalID: 'ElasticSearchDomain', + ElasticsearchStreamingLambdaIAMRoleLogicalID: 'ElasticSearchStreamingLambdaIAMRole', + ElasticsearchStreamingLambdaFunctionLogicalID: 'ElasticSearchStreamingLambdaFunction', + ElasticsearchDataSourceLogicalID: 'ElasticSearchDataSource', - // Elasticsearch - ElasticsearchAccessIAMRoleLogicalID: 'ElasticSearchAccessIAMRole', - ElasticsearchDomainLogicalID: 'ElasticSearchDomain', - ElasticsearchStreamingLambdaIAMRoleLogicalID: 'ElasticSearchStreamingLambdaIAMRole', - ElasticsearchStreamingLambdaFunctionLogicalID: 'ElasticSearchStreamingLambdaFunction', - ElasticsearchDataSourceLogicalID: 'ElasticSearchDataSource', + // Local. Try not to collide with model data sources. + NoneDataSource: 'NoneDataSource', - // Local. Try not to collide with model data sources. - NoneDataSource: 'NoneDataSource', + // Auth + AuthCognitoUserPoolLogicalID: 'AuthCognitoUserPool', + AuthCognitoUserPoolNativeClientLogicalID: 'AuthCognitoUserPoolNativeClient', + AuthCognitoUserPoolJSClientLogicalID: 'AuthCognitoUserPoolJSClient', + }; + public static PARAMETERS = { + // cli + Env: 'env', + S3DeploymentBucket: 'S3DeploymentBucket', + S3DeploymentRootKey: 'S3DeploymentRootKey', - // Auth - AuthCognitoUserPoolLogicalID: 'AuthCognitoUserPool', - AuthCognitoUserPoolNativeClientLogicalID: 'AuthCognitoUserPoolNativeClient', - AuthCognitoUserPoolJSClientLogicalID: 'AuthCognitoUserPoolJSClient', - } - public static PARAMETERS = { - // cli - Env: 'env', - S3DeploymentBucket: 'S3DeploymentBucket', - S3DeploymentRootKey: 'S3DeploymentRootKey', + // AppSync + AppSyncApiName: 'AppSyncApiName', + AppSyncApiId: 'AppSyncApiId', + CreateAPIKey: 'CreateAPIKey', + AuthRoleName: 'authRoleName', + UnauthRoleName: 'unauthRoleName', + APIKeyExpirationEpoch: 'APIKeyExpirationEpoch', - // AppSync - AppSyncApiName: 'AppSyncApiName', - AppSyncApiId: 'AppSyncApiId', - CreateAPIKey: 'CreateAPIKey', - AuthRoleName: 'authRoleName', - UnauthRoleName: 'unauthRoleName', - APIKeyExpirationEpoch: 'APIKeyExpirationEpoch', + // DynamoDB + DynamoDBBillingMode: 'DynamoDBBillingMode', + DynamoDBModelTableReadIOPS: 'DynamoDBModelTableReadIOPS', + DynamoDBModelTableWriteIOPS: 'DynamoDBModelTableWriteIOPS', + DynamoDBEnablePointInTimeRecovery: 'DynamoDBEnablePointInTimeRecovery', + DynamoDBEnableServerSideEncryption: 'DynamoDBEnableServerSideEncryption', - // DynamoDB - DynamoDBBillingMode: 'DynamoDBBillingMode', - DynamoDBModelTableReadIOPS: 'DynamoDBModelTableReadIOPS', - DynamoDBModelTableWriteIOPS: 'DynamoDBModelTableWriteIOPS', - DynamoDBEnablePointInTimeRecovery: 'DynamoDBEnablePointInTimeRecovery', - DynamoDBEnableServerSideEncryption: 'DynamoDBEnableServerSideEncryption', + // Elasticsearch + ElasticsearchAccessIAMRoleName: 'ElasticSearchAccessIAMRoleName', + ElasticsearchDebugStreamingLambda: 'ElasticSearchDebugStreamingLambda', + ElasticsearchStreamingIAMRoleName: 'ElasticSearchStreamingIAMRoleName', + ElasticsearchStreamingFunctionName: 'ElasticSearchStreamingFunctionName', + ElasticsearchInstanceCount: 'ElasticSearchInstanceCount', + ElasticsearchInstanceType: 'ElasticSearchInstanceType', + ElasticsearchEBSVolumeGB: 'ElasticSearchEBSVolumeGB', + ElasticsearchStreamingLambdaHandlerName: 'ElasticSearchStreamingLambdaHandlerName', + ElasticsearchStreamingLambdaRuntime: 'ElasticSearchStreamingLambdaRuntime', - // Elasticsearch - ElasticsearchAccessIAMRoleName: 'ElasticSearchAccessIAMRoleName', - ElasticsearchDebugStreamingLambda: 'ElasticSearchDebugStreamingLambda', - ElasticsearchStreamingIAMRoleName: 'ElasticSearchStreamingIAMRoleName', - ElasticsearchStreamingFunctionName: 'ElasticSearchStreamingFunctionName', - ElasticsearchInstanceCount: 'ElasticSearchInstanceCount', - ElasticsearchInstanceType: 'ElasticSearchInstanceType', - ElasticsearchEBSVolumeGB: 'ElasticSearchEBSVolumeGB', - ElasticsearchStreamingLambdaHandlerName: 'ElasticSearchStreamingLambdaHandlerName', - ElasticsearchStreamingLambdaRuntime: 'ElasticSearchStreamingLambdaRuntime', + // Auth + AuthCognitoUserPoolId: 'AuthCognitoUserPoolId', + }; + public static MAPPINGS = {}; + public static CONDITIONS = { + // Environment + HasEnvironmentParameter: 'HasEnvironmentParameter', - // Auth - AuthCognitoUserPoolId: 'AuthCognitoUserPoolId', - } - public static MAPPINGS = {} - public static CONDITIONS = { - // Environment - HasEnvironmentParameter: 'HasEnvironmentParameter', + // DynamoDB + ShouldUsePayPerRequestBilling: 'ShouldUsePayPerRequestBilling', + ShouldUsePointInTimeRecovery: 'ShouldUsePointInTimeRecovery', + ShouldUseServerSideEncryption: 'ShouldUseServerSideEncryption', - // DynamoDB - ShouldUsePayPerRequestBilling: 'ShouldUsePayPerRequestBilling', - ShouldUsePointInTimeRecovery: 'ShouldUsePointInTimeRecovery', - ShouldUseServerSideEncryption: 'ShouldUseServerSideEncryption', + // Auth + ShouldCreateAPIKey: 'ShouldCreateAPIKey', + APIKeyExpirationEpochIsPositive: 'APIKeyExpirationEpochIsPositive', + }; + public static OUTPUTS = { + // AppSync + GraphQLAPIEndpointOutput: 'GraphQLAPIEndpointOutput', + GraphQLAPIApiKeyOutput: 'GraphQLAPIKeyOutput', + GraphQLAPIIdOutput: 'GraphQLAPIIdOutput', - // Auth - ShouldCreateAPIKey: 'ShouldCreateAPIKey', - APIKeyExpirationEpochIsPositive: 'APIKeyExpirationEpochIsPositive', - } - public static OUTPUTS = { + // Elasticsearch + ElasticsearchStreamingLambdaIAMRoleArn: 'ElasticsearchStreamingLambdaIAMRoleArn', + ElasticsearchAccessIAMRoleArn: 'ElasticsearchAccessIAMRoleArn', + ElasticsearchDomainArn: 'ElasticsearchDomainArn', + ElasticsearchDomainEndpoint: 'ElasticsearchDomainEndpoint', - // AppSync - GraphQLAPIEndpointOutput: 'GraphQLAPIEndpointOutput', - GraphQLAPIApiKeyOutput: 'GraphQLAPIKeyOutput', - GraphQLAPIIdOutput: 'GraphQLAPIIdOutput', + // Auth + AuthCognitoUserPoolIdOutput: 'AuthCognitoUserPoolIdOutput', + AuthCognitoUserPoolNativeClientOutput: 'AuthCognitoUserPoolNativeClientId', + AuthCognitoUserPoolJSClientOutput: 'AuthCognitoUserPoolJSClientId', + }; + public static METADATA = {}; - // Elasticsearch - ElasticsearchStreamingLambdaIAMRoleArn: 'ElasticsearchStreamingLambdaIAMRoleArn', - ElasticsearchAccessIAMRoleArn: 'ElasticsearchAccessIAMRoleArn', - ElasticsearchDomainArn: 'ElasticsearchDomainArn', - ElasticsearchDomainEndpoint: 'ElasticsearchDomainEndpoint', - - // Auth - AuthCognitoUserPoolIdOutput: 'AuthCognitoUserPoolIdOutput', - AuthCognitoUserPoolNativeClientOutput: 'AuthCognitoUserPoolNativeClientId', - AuthCognitoUserPoolJSClientOutput: 'AuthCognitoUserPoolJSClientId' - } - public static METADATA = {} - - public static readonly SNIPPETS = { - AuthCondition: "authCondition", - AuthMode: "authMode", - VersionedCondition: "versionedCondition", - ModelObjectKey: "modelObjectKey", - DynamoDBNameOverrideMap: "dynamodbNameOverrideMap", - ModelQueryExpression: "modelQueryExpression", - ModelQueryIndex: "modelQueryIndex", - IsDynamicGroupAuthorizedVariable: "isDynamicGroupAuthorized", - IsLocalDynamicGroupAuthorizedVariable: "isLocalDynamicGroupAuthorized", - IsStaticGroupAuthorizedVariable: "isStaticGroupAuthorized", - IsOwnerAuthorizedVariable: "isOwnerAuthorized", - IsLocalOwnerAuthorizedVariable: "isLocalOwnerAuthorized" - } + public static readonly SNIPPETS = { + AuthCondition: 'authCondition', + AuthMode: 'authMode', + VersionedCondition: 'versionedCondition', + ModelObjectKey: 'modelObjectKey', + DynamoDBNameOverrideMap: 'dynamodbNameOverrideMap', + ModelQueryExpression: 'modelQueryExpression', + ModelQueryIndex: 'modelQueryIndex', + IsDynamicGroupAuthorizedVariable: 'isDynamicGroupAuthorized', + IsLocalDynamicGroupAuthorizedVariable: 'isLocalDynamicGroupAuthorized', + IsStaticGroupAuthorizedVariable: 'isStaticGroupAuthorized', + IsOwnerAuthorizedVariable: 'isOwnerAuthorized', + IsLocalOwnerAuthorizedVariable: 'isLocalOwnerAuthorized', + }; } diff --git a/packages/graphql-transformer-common/src/SearchableResourceIDs.ts b/packages/graphql-transformer-common/src/SearchableResourceIDs.ts index 9ddd9dfe97..8ffaf72340 100644 --- a/packages/graphql-transformer-common/src/SearchableResourceIDs.ts +++ b/packages/graphql-transformer-common/src/SearchableResourceIDs.ts @@ -1,14 +1,14 @@ -import { DEFAULT_SCALARS } from './definition' +import { DEFAULT_SCALARS } from './definition'; export class SearchableResourceIDs { - static SearchableEventSourceMappingID(typeName: string): string { - return `Searchable${typeName}LambdaMapping` + static SearchableEventSourceMappingID(typeName: string): string { + return `Searchable${typeName}LambdaMapping`; + } + static SearchableFilterInputTypeName(name: string): string { + const nameOverride = DEFAULT_SCALARS[name]; + if (nameOverride) { + return `Searchable${nameOverride}FilterInput`; } - static SearchableFilterInputTypeName(name: string): string { - const nameOverride = DEFAULT_SCALARS[name] - if (nameOverride) { - return `Searchable${nameOverride}FilterInput` - } - return `Searchable${name}FilterInput` - } -} \ No newline at end of file + return `Searchable${name}FilterInput`; + } +} diff --git a/packages/graphql-transformer-common/src/__tests__/common.test.ts b/packages/graphql-transformer-common/src/__tests__/common.test.ts index 937eda30ec..b54e0c0170 100644 --- a/packages/graphql-transformer-common/src/__tests__/common.test.ts +++ b/packages/graphql-transformer-common/src/__tests__/common.test.ts @@ -1 +1 @@ -test('TODO: Common tests', () => expect(true).toEqual(true)) \ No newline at end of file +test('TODO: Common tests', () => expect(true).toEqual(true)); diff --git a/packages/graphql-transformer-common/src/connectionUtils.ts b/packages/graphql-transformer-common/src/connectionUtils.ts index e8ac955e9a..a3ff0ac733 100644 --- a/packages/graphql-transformer-common/src/connectionUtils.ts +++ b/packages/graphql-transformer-common/src/connectionUtils.ts @@ -2,14 +2,14 @@ import { makeField, makeInputValueDefinition, makeNamedType } from './definition import { ModelResourceIDs } from './ModelResourceIDs'; import { FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; export function makeConnectionField(fieldName: string, returnTypeName: string, args: InputValueDefinitionNode[] = []): FieldDefinitionNode { - return makeField( - fieldName, - [ - ...args, - makeInputValueDefinition('filter', makeNamedType(ModelResourceIDs.ModelFilterInputTypeName(returnTypeName))), - makeInputValueDefinition('limit', makeNamedType('Int')), - makeInputValueDefinition('nextToken', makeNamedType('String')) - ], - makeNamedType(ModelResourceIDs.ModelConnectionTypeName(returnTypeName)) - ) + return makeField( + fieldName, + [ + ...args, + makeInputValueDefinition('filter', makeNamedType(ModelResourceIDs.ModelFilterInputTypeName(returnTypeName))), + makeInputValueDefinition('limit', makeNamedType('Int')), + makeInputValueDefinition('nextToken', makeNamedType('String')), + ], + makeNamedType(ModelResourceIDs.ModelConnectionTypeName(returnTypeName)) + ); } diff --git a/packages/graphql-transformer-common/src/definition.ts b/packages/graphql-transformer-common/src/definition.ts index ad5631a0be..615d3002b6 100644 --- a/packages/graphql-transformer-common/src/definition.ts +++ b/packages/graphql-transformer-common/src/definition.ts @@ -1,359 +1,373 @@ import { - ObjectTypeDefinitionNode, InputValueDefinitionNode, FieldDefinitionNode, - TypeNode, SchemaDefinitionNode, OperationTypeNode, OperationTypeDefinitionNode, - ObjectTypeExtensionNode, NamedTypeNode, Kind, NonNullTypeNode, ListTypeNode, - valueFromASTUntyped, ArgumentNode, DirectiveNode, EnumTypeDefinitionNode, - ValueNode, - ListValueNode, - ObjectValueNode, - InputObjectTypeDefinitionNode -} from 'graphql' + ObjectTypeDefinitionNode, + InputValueDefinitionNode, + FieldDefinitionNode, + TypeNode, + SchemaDefinitionNode, + OperationTypeNode, + OperationTypeDefinitionNode, + ObjectTypeExtensionNode, + NamedTypeNode, + Kind, + NonNullTypeNode, + ListTypeNode, + valueFromASTUntyped, + ArgumentNode, + DirectiveNode, + EnumTypeDefinitionNode, + ValueNode, + ListValueNode, + ObjectValueNode, + InputObjectTypeDefinitionNode, +} from 'graphql'; import { access } from 'fs'; type ScalarMap = { - [k: string]: 'String' | 'Int' | 'Float' | 'Boolean' | 'ID' -} + [k: string]: 'String' | 'Int' | 'Float' | 'Boolean' | 'ID'; +}; export const STANDARD_SCALARS: ScalarMap = { - String: 'String', - Int: 'Int', - Float: 'Float', - Boolean: 'Boolean', - ID: 'ID' + String: 'String', + Int: 'Int', + Float: 'Float', + Boolean: 'Boolean', + ID: 'ID', }; const OTHER_SCALARS: ScalarMap = { - BigInt: 'Int', - Double: 'Float' + BigInt: 'Int', + Double: 'Float', }; export const APPSYNC_DEFINED_SCALARS: ScalarMap = { - AWSDate: 'String', - AWSTime: 'String', - AWSDateTime: 'String', - AWSTimestamp: 'Int', - AWSEmail: 'String', - AWSJSON: 'String', - AWSURL: 'String', - AWSPhone: 'String', - AWSIPAddress: 'String' + AWSDate: 'String', + AWSTime: 'String', + AWSDateTime: 'String', + AWSTimestamp: 'Int', + AWSEmail: 'String', + AWSJSON: 'String', + AWSURL: 'String', + AWSPhone: 'String', + AWSIPAddress: 'String', }; export const DEFAULT_SCALARS: ScalarMap = { - ...STANDARD_SCALARS, - ...OTHER_SCALARS, - ...APPSYNC_DEFINED_SCALARS + ...STANDARD_SCALARS, + ...OTHER_SCALARS, + ...APPSYNC_DEFINED_SCALARS, }; export const NUMERIC_SCALARS: { [k: string]: boolean } = { - BigInt: true, - Int: true, - Float: true, - Double: true, - AWSTimestamp: true + BigInt: true, + Int: true, + Float: true, + Double: true, + AWSTimestamp: true, }; export const MAP_SCALARS: { [k: string]: boolean } = { - AWSJSON: true + AWSJSON: true, }; export function attributeTypeFromScalar(scalar: TypeNode) { - const baseType = getBaseType(scalar); - const baseScalar = DEFAULT_SCALARS[baseType]; - if (!baseScalar) { - throw new Error(`Expected scalar and got ${baseType}`); - } - switch (baseScalar) { - case 'String': - case 'ID': - return 'S'; - case 'Int': - case 'Float': - return 'N'; - case 'Boolean': - throw new Error(`Boolean values cannot be used as sort keys.`) - default: - throw new Error(`There is no valid DynamoDB attribute type for scalar ${baseType}`) - } + const baseType = getBaseType(scalar); + const baseScalar = DEFAULT_SCALARS[baseType]; + if (!baseScalar) { + throw new Error(`Expected scalar and got ${baseType}`); + } + switch (baseScalar) { + case 'String': + case 'ID': + return 'S'; + case 'Int': + case 'Float': + return 'N'; + case 'Boolean': + throw new Error(`Boolean values cannot be used as sort keys.`); + default: + throw new Error(`There is no valid DynamoDB attribute type for scalar ${baseType}`); + } } export function isScalar(type: TypeNode) { - if (type.kind === Kind.NON_NULL_TYPE) { - return isScalar(type.type) - } else if (type.kind === Kind.LIST_TYPE) { - return isScalar(type.type) - } else { - return Boolean(DEFAULT_SCALARS[type.name.value]) - } + if (type.kind === Kind.NON_NULL_TYPE) { + return isScalar(type.type); + } else if (type.kind === Kind.LIST_TYPE) { + return isScalar(type.type); + } else { + return Boolean(DEFAULT_SCALARS[type.name.value]); + } } export function isScalarOrEnum(type: TypeNode, enums: EnumTypeDefinitionNode[]) { - if (type.kind === Kind.NON_NULL_TYPE) { - return isScalarOrEnum(type.type, enums) - } else if (type.kind === Kind.LIST_TYPE) { - return isScalarOrEnum(type.type, enums) - } else { - for (const e of enums) { - if (e.name.value === type.name.value) { - return true - } - } - return Boolean(DEFAULT_SCALARS[type.name.value]) + if (type.kind === Kind.NON_NULL_TYPE) { + return isScalarOrEnum(type.type, enums); + } else if (type.kind === Kind.LIST_TYPE) { + return isScalarOrEnum(type.type, enums); + } else { + for (const e of enums) { + if (e.name.value === type.name.value) { + return true; + } } + return Boolean(DEFAULT_SCALARS[type.name.value]); + } } export function getBaseType(type: TypeNode): string { - if (type.kind === Kind.NON_NULL_TYPE) { - return getBaseType(type.type) - } else if (type.kind === Kind.LIST_TYPE) { - return getBaseType(type.type) - } else { - return type.name.value; - } + if (type.kind === Kind.NON_NULL_TYPE) { + return getBaseType(type.type); + } else if (type.kind === Kind.LIST_TYPE) { + return getBaseType(type.type); + } else { + return type.name.value; + } } export function isListType(type: TypeNode): boolean { - if (type.kind === Kind.NON_NULL_TYPE) { - return isListType(type.type) - } else if (type.kind === Kind.LIST_TYPE) { - return true - } else { - return false; - } + if (type.kind === Kind.NON_NULL_TYPE) { + return isListType(type.type); + } else if (type.kind === Kind.LIST_TYPE) { + return true; + } else { + return false; + } } export function isNonNullType(type: TypeNode): boolean { - return type.kind === Kind.NON_NULL_TYPE; + return type.kind === Kind.NON_NULL_TYPE; } export function getDirectiveArgument(directive: DirectiveNode, arg: string, dflt?: any) { - const argument = directive.arguments.find(a => a.name.value === arg); - return argument ? valueFromASTUntyped(argument.value) : dflt; + const argument = directive.arguments.find(a => a.name.value === arg); + return argument ? valueFromASTUntyped(argument.value) : dflt; } export function unwrapNonNull(type: TypeNode) { - if (type.kind === 'NonNullType') { - return unwrapNonNull(type.type) - } - return type + if (type.kind === 'NonNullType') { + return unwrapNonNull(type.type); + } + return type; } export function wrapNonNull(type: TypeNode) { - if (type.kind !== 'NonNullType') { - return makeNonNullType(type) - } - return type + if (type.kind !== 'NonNullType') { + return makeNonNullType(type); + } + return type; } -export function makeOperationType( - operation: OperationTypeNode, - type: string -): OperationTypeDefinitionNode { - return { - kind: 'OperationTypeDefinition', - operation, - type: { - kind: 'NamedType', - name: { - kind: 'Name', - value: type - } - } - } +export function makeOperationType(operation: OperationTypeNode, type: string): OperationTypeDefinitionNode { + return { + kind: 'OperationTypeDefinition', + operation, + type: { + kind: 'NamedType', + name: { + kind: 'Name', + value: type, + }, + }, + }; } export function makeSchema(operationTypes: OperationTypeDefinitionNode[]): SchemaDefinitionNode { - return { - kind: Kind.SCHEMA_DEFINITION, - operationTypes, - directives: [] - } + return { + kind: Kind.SCHEMA_DEFINITION, + operationTypes, + directives: [], + }; } export function blankObject(name: string): ObjectTypeDefinitionNode { - return { - kind: 'ObjectTypeDefinition', - name: { - kind: 'Name', - value: name - }, - fields: [], - directives: [], - interfaces: [] - } + return { + kind: 'ObjectTypeDefinition', + name: { + kind: 'Name', + value: name, + }, + fields: [], + directives: [], + interfaces: [], + }; } export function blankObjectExtension(name: string): ObjectTypeExtensionNode { - return { - kind: Kind.OBJECT_TYPE_EXTENSION, - name: { - kind: 'Name', - value: name - }, - fields: [], - directives: [], - interfaces: [] - } + return { + kind: Kind.OBJECT_TYPE_EXTENSION, + name: { + kind: 'Name', + value: name, + }, + fields: [], + directives: [], + interfaces: [], + }; } export function extensionWithFields(object: ObjectTypeExtensionNode, fields: FieldDefinitionNode[]): ObjectTypeExtensionNode { - return { - ...object, - fields: [...object.fields, ...fields] - } + return { + ...object, + fields: [...object.fields, ...fields], + }; } export function extensionWithDirectives(object: ObjectTypeExtensionNode, directives: DirectiveNode[]): ObjectTypeExtensionNode { - if (directives && directives.length > 0) { - const newDirectives = []; - - for (const directive of directives) { - if (!object.directives.find((d) => d.name.value === directive.name.value)) { - newDirectives.push(directive); - } - } - - if (newDirectives.length > 0) { - return { - ...object, - directives: [...object.directives, ...newDirectives] - } - } + if (directives && directives.length > 0) { + const newDirectives = []; + + for (const directive of directives) { + if (!object.directives.find(d => d.name.value === directive.name.value)) { + newDirectives.push(directive); + } + } + + if (newDirectives.length > 0) { + return { + ...object, + directives: [...object.directives, ...newDirectives], + }; } + } - return object; + return object; } export function extendFieldWithDirectives(field: FieldDefinitionNode, directives: DirectiveNode[]): FieldDefinitionNode { - if (directives && directives.length > 0) { - const newDirectives = []; - - for (const directive of directives) { - if (!field.directives.find((d) => d.name.value === directive.name.value)) { - newDirectives.push(directive); - } - } - - if (newDirectives.length > 0) { - return { - ...field, - directives: [...field.directives, ...newDirectives] - } - } + if (directives && directives.length > 0) { + const newDirectives = []; + + for (const directive of directives) { + if (!field.directives.find(d => d.name.value === directive.name.value)) { + newDirectives.push(directive); + } + } + + if (newDirectives.length > 0) { + return { + ...field, + directives: [...field.directives, ...newDirectives], + }; } + } - return field; + return field; } export function makeInputObjectDefinition(name: string, inputs: InputValueDefinitionNode[]): InputObjectTypeDefinitionNode { - return { - kind: 'InputObjectTypeDefinition', - name: { - kind: 'Name', - value: name - }, - fields: inputs, - directives: [] - } + return { + kind: 'InputObjectTypeDefinition', + name: { + kind: 'Name', + value: name, + }, + fields: inputs, + directives: [], + }; } -export function makeField(name: string, args: InputValueDefinitionNode[], type: TypeNode, directives: DirectiveNode[] = []): FieldDefinitionNode { - return { - kind: Kind.FIELD_DEFINITION, - name: { - kind: 'Name', - value: name - }, - arguments: args, - type, - directives - } +export function makeField( + name: string, + args: InputValueDefinitionNode[], + type: TypeNode, + directives: DirectiveNode[] = [] +): FieldDefinitionNode { + return { + kind: Kind.FIELD_DEFINITION, + name: { + kind: 'Name', + value: name, + }, + arguments: args, + type, + directives, + }; } export function makeDirective(name: string, args: ArgumentNode[]): DirectiveNode { - return { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: name - }, - arguments: args - } + return { + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: name, + }, + arguments: args, + }; } export function makeArgument(name: string, value: ValueNode): ArgumentNode { - return { - kind: Kind.ARGUMENT, - name: { - kind: 'Name', - value: name - }, - value - } + return { + kind: Kind.ARGUMENT, + name: { + kind: 'Name', + value: name, + }, + value, + }; } export function makeValueNode(value: any): ValueNode { - if (typeof value === 'string') { - return { kind: Kind.STRING, value: value } - } else if (Number.isInteger(value)) { - return { kind: Kind.INT, value: value } - } else if (typeof value === 'number') { - return { kind: Kind.FLOAT, value: String(value) } - } else if (typeof value === 'boolean') { - return { kind: Kind.BOOLEAN, value: value } - } else if (value === null) { - return { kind: Kind.NULL } - } else if (Array.isArray(value)) { - return { - kind: Kind.LIST, - values: value.map(v => makeValueNode(v)) - } - } else if (typeof value === 'object') { + if (typeof value === 'string') { + return { kind: Kind.STRING, value: value }; + } else if (Number.isInteger(value)) { + return { kind: Kind.INT, value: value }; + } else if (typeof value === 'number') { + return { kind: Kind.FLOAT, value: String(value) }; + } else if (typeof value === 'boolean') { + return { kind: Kind.BOOLEAN, value: value }; + } else if (value === null) { + return { kind: Kind.NULL }; + } else if (Array.isArray(value)) { + return { + kind: Kind.LIST, + values: value.map(v => makeValueNode(v)), + }; + } else if (typeof value === 'object') { + return { + kind: Kind.OBJECT, + fields: Object.keys(value).map((key: string) => { + const keyValNode = makeValueNode(value[key]); return { - kind: Kind.OBJECT, - fields: Object.keys(value).map((key: string) => { - const keyValNode = makeValueNode(value[key]) - return { - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: key }, - value: keyValNode - } - }) - } - } + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: key }, + value: keyValNode, + }; + }), + }; + } } export function makeInputValueDefinition(name: string, type: TypeNode): InputValueDefinitionNode { - return { - kind: Kind.INPUT_VALUE_DEFINITION, - name: { - kind: 'Name', - value: name - }, - type, - directives: [] - } + return { + kind: Kind.INPUT_VALUE_DEFINITION, + name: { + kind: 'Name', + value: name, + }, + type, + directives: [], + }; } export function makeNamedType(name: string): NamedTypeNode { - return { - kind: 'NamedType', - name: { - kind: 'Name', - value: name - } - } + return { + kind: 'NamedType', + name: { + kind: 'Name', + value: name, + }, + }; } export function makeNonNullType(type: NamedTypeNode | ListTypeNode): NonNullTypeNode { - return { - kind: Kind.NON_NULL_TYPE, - type - } + return { + kind: Kind.NON_NULL_TYPE, + type, + }; } export function makeListType(type: TypeNode): TypeNode { - return { - kind: 'ListType', - type - } + return { + kind: 'ListType', + type, + }; } diff --git a/packages/graphql-transformer-common/src/dynamodbUtils.ts b/packages/graphql-transformer-common/src/dynamodbUtils.ts index c1551121d8..2710b776cf 100644 --- a/packages/graphql-transformer-common/src/dynamodbUtils.ts +++ b/packages/graphql-transformer-common/src/dynamodbUtils.ts @@ -1,40 +1,64 @@ import { InputObjectTypeDefinitionNode, InputValueDefinitionNode, Kind, TypeNode, FieldDefinitionNode } from 'graphql'; -import { makeListType, makeNamedType, getBaseType, makeInputValueDefinition, DEFAULT_SCALARS, makeInputObjectDefinition, isScalar } from './definition'; +import { + makeListType, + makeNamedType, + getBaseType, + makeInputValueDefinition, + DEFAULT_SCALARS, + makeInputObjectDefinition, + isScalar, +} from './definition'; import { ModelResourceIDs } from './ModelResourceIDs'; -import { compoundExpression, block, iff, raw, set, ref, qref, obj, str, printBlock, list, forEach, Expression, newline, ReferenceNode, ifElse } from 'graphql-mapping-template'; +import { + compoundExpression, + block, + iff, + raw, + set, + ref, + qref, + obj, + str, + printBlock, + list, + forEach, + Expression, + newline, + ReferenceNode, + ifElse, +} from 'graphql-mapping-template'; import { toCamelCase } from './util'; // Key conditions -const STRING_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between', 'beginsWith'] -const ID_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between', 'beginsWith'] -const INT_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between'] -const FLOAT_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between'] +const STRING_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between', 'beginsWith']; +const ID_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between', 'beginsWith']; +const INT_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between']; +const FLOAT_KEY_CONDITIONS = ['eq', 'le', 'lt', 'ge', 'gt', 'between']; function getScalarKeyConditions(type: string): string[] { - switch (type) { - case 'String': - return STRING_KEY_CONDITIONS - case 'ID': - return ID_KEY_CONDITIONS - case 'Int': - return INT_KEY_CONDITIONS - case 'Float': - return FLOAT_KEY_CONDITIONS - default: - throw 'Valid types are String, ID, Int, Float, Boolean' - } + switch (type) { + case 'String': + return STRING_KEY_CONDITIONS; + case 'ID': + return ID_KEY_CONDITIONS; + case 'Int': + return INT_KEY_CONDITIONS; + case 'Float': + return FLOAT_KEY_CONDITIONS; + default: + throw 'Valid types are String, ID, Int, Float, Boolean'; + } } export function makeModelScalarKeyConditionInputObject(type: string): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.ModelKeyConditionInputTypeName(type) - const conditions = getScalarKeyConditions(type) - const fields: InputValueDefinitionNode[] = conditions - .map((condition: string) => ({ - kind: Kind.INPUT_VALUE_DEFINITION, - name: { kind: "Name" as "Name", value: condition }, - type: condition === 'between' ? makeListType(makeNamedType(type)) : makeNamedType(type), - directives: [] - })) - return makeInputObjectDefinition(name, fields); + const name = ModelResourceIDs.ModelKeyConditionInputTypeName(type); + const conditions = getScalarKeyConditions(type); + const fields: InputValueDefinitionNode[] = conditions.map((condition: string) => ({ + kind: Kind.INPUT_VALUE_DEFINITION, + name: { kind: 'Name' as 'Name', value: condition }, + type: condition === 'between' ? makeListType(makeNamedType(type)) : makeNamedType(type), + directives: [], + })); + return makeInputObjectDefinition(name, fields); } const STRING_KEY_CONDITION = makeModelScalarKeyConditionInputObject('String'); @@ -43,29 +67,31 @@ const INT_KEY_CONDITION = makeModelScalarKeyConditionInputObject('Int'); const FLOAT_KEY_CONDITION = makeModelScalarKeyConditionInputObject('Float'); const SCALAR_KEY_CONDITIONS = [STRING_KEY_CONDITION, ID_KEY_CONDITION, INT_KEY_CONDITION, FLOAT_KEY_CONDITION]; export function makeScalarKeyConditionInputs(): InputObjectTypeDefinitionNode[] { - return SCALAR_KEY_CONDITIONS; + return SCALAR_KEY_CONDITIONS; } -export function makeScalarKeyConditionForType(type: TypeNode, - nonScalarTypeResolver: (baseType: string) => string = undefined): InputObjectTypeDefinitionNode { - const baseType = getBaseType(type); - let resolvedScalarName: string; - if (isScalar(type)) { - resolvedScalarName = baseType; - } else if (nonScalarTypeResolver) { - resolvedScalarName = nonScalarTypeResolver(baseType); - } +export function makeScalarKeyConditionForType( + type: TypeNode, + nonScalarTypeResolver: (baseType: string) => string = undefined +): InputObjectTypeDefinitionNode { + const baseType = getBaseType(type); + let resolvedScalarName: string; + if (isScalar(type)) { + resolvedScalarName = baseType; + } else if (nonScalarTypeResolver) { + resolvedScalarName = nonScalarTypeResolver(baseType); + } - const inputName = ModelResourceIDs.ModelKeyConditionInputTypeName(resolvedScalarName); - for (const key of SCALAR_KEY_CONDITIONS) { - if (key.name.value === inputName) { - return key; - } + const inputName = ModelResourceIDs.ModelKeyConditionInputTypeName(resolvedScalarName); + for (const key of SCALAR_KEY_CONDITIONS) { + if (key.name.value === inputName) { + return key; } + } } /** * Given a list of key fields, create a composite key input type for the sort key condition. - * Given, + * Given, * type User @model @key(fields: ["a", "b", "c"]) { a: String, b: String, c: String } * a composite key will be formed over "a" and "b". This will output: * input UserPrimaryCompositeKeyConditionInput { @@ -78,334 +104,393 @@ export function makeScalarKeyConditionForType(type: TypeNode, * c: String * } */ -export function makeCompositeKeyConditionInputForKey(modelName: string, keyName: string, fields: FieldDefinitionNode[]): InputObjectTypeDefinitionNode { - const name = ModelResourceIDs.ModelCompositeKeyConditionInputTypeName(modelName, keyName) - const conditions = STRING_KEY_CONDITIONS; - const inputValues: InputValueDefinitionNode[] = conditions - .map((condition: string) => { - // Between takes a list of comosite key nodes. - const typeNode = condition === 'between' ? - makeListType(makeNamedType(ModelResourceIDs.ModelCompositeKeyInputTypeName(modelName, keyName))) : - makeNamedType(ModelResourceIDs.ModelCompositeKeyInputTypeName(modelName, keyName)); - return makeInputValueDefinition(condition, typeNode); - }); - return makeInputObjectDefinition(name, inputValues); +export function makeCompositeKeyConditionInputForKey( + modelName: string, + keyName: string, + fields: FieldDefinitionNode[] +): InputObjectTypeDefinitionNode { + const name = ModelResourceIDs.ModelCompositeKeyConditionInputTypeName(modelName, keyName); + const conditions = STRING_KEY_CONDITIONS; + const inputValues: InputValueDefinitionNode[] = conditions.map((condition: string) => { + // Between takes a list of comosite key nodes. + const typeNode = + condition === 'between' + ? makeListType(makeNamedType(ModelResourceIDs.ModelCompositeKeyInputTypeName(modelName, keyName))) + : makeNamedType(ModelResourceIDs.ModelCompositeKeyInputTypeName(modelName, keyName)); + return makeInputValueDefinition(condition, typeNode); + }); + return makeInputObjectDefinition(name, inputValues); } -export function makeCompositeKeyInputForKey(modelName: string, keyName: string, fields: FieldDefinitionNode[]): InputObjectTypeDefinitionNode { - const inputValues = fields.map( - (field: FieldDefinitionNode, idx) => { - const baseTypeName = getBaseType(field.type); - const nameOverride = DEFAULT_SCALARS[baseTypeName] - let typeNode = null; - if (idx === fields.length -1 && nameOverride) { - typeNode = makeNamedType(nameOverride) - } else { - typeNode = makeNamedType(baseTypeName) - } - return makeInputValueDefinition(field.name.value, typeNode); - }); - const inputName = ModelResourceIDs.ModelCompositeKeyInputTypeName(modelName, keyName); - return makeInputObjectDefinition(inputName, inputValues); +export function makeCompositeKeyInputForKey( + modelName: string, + keyName: string, + fields: FieldDefinitionNode[] +): InputObjectTypeDefinitionNode { + const inputValues = fields.map((field: FieldDefinitionNode, idx) => { + const baseTypeName = getBaseType(field.type); + const nameOverride = DEFAULT_SCALARS[baseTypeName]; + let typeNode = null; + if (idx === fields.length - 1 && nameOverride) { + typeNode = makeNamedType(nameOverride); + } else { + typeNode = makeNamedType(baseTypeName); + } + return makeInputValueDefinition(field.name.value, typeNode); + }); + const inputName = ModelResourceIDs.ModelCompositeKeyInputTypeName(modelName, keyName); + return makeInputObjectDefinition(inputName, inputValues); } /** -* Key conditions materialize as instances of ModelXKeyConditionInput passed via $ctx.args. -* If the arguments with the given sortKey name exists, create a DynamoDB expression that -* implements its logic. Possible operators: eq, le, lt, ge, gt, beginsWith, and between. -* @param argName The name of the argument containing the sort key condition object. -* @param attributeType The type of the DynamoDB attribute in the table. -* @param queryExprReference The name of the variable containing the query expression in the template. -*/ -export function applyKeyConditionExpression(argName: string, attributeType: 'S' | 'N' | 'B' = 'S', queryExprReference: string = 'query', sortKeyName?: string, prefixVariableName?: string) { - const prefixValue = (value: string): string => prefixVariableName ? `$${prefixVariableName}#${value}` : value; - const _sortKeyName = sortKeyName ? sortKeyName : argName; - return block("Applying Key Condition", [ - iff( - raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.beginsWith)`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND begins_with(#sortKey, :sortKey)"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.beginsWith`)}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.between)`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey BETWEEN :sortKey0 AND :sortKey1"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey0", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.between[0]`)}" })`), - qref(`$${queryExprReference}.expressionValues.put(":sortKey1", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.between[1]`)}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.eq)`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey = :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.eq`)}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.lt)`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey < :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.lt`)}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.le)`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey <= :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.le`)}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.gt)`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey > :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.gt`)}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.ge)`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey >= :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.ge`)}" })`) - ]) - ) - ]); + * Key conditions materialize as instances of ModelXKeyConditionInput passed via $ctx.args. + * If the arguments with the given sortKey name exists, create a DynamoDB expression that + * implements its logic. Possible operators: eq, le, lt, ge, gt, beginsWith, and between. + * @param argName The name of the argument containing the sort key condition object. + * @param attributeType The type of the DynamoDB attribute in the table. + * @param queryExprReference The name of the variable containing the query expression in the template. + */ +export function applyKeyConditionExpression( + argName: string, + attributeType: 'S' | 'N' | 'B' = 'S', + queryExprReference: string = 'query', + sortKeyName?: string, + prefixVariableName?: string +) { + const prefixValue = (value: string): string => (prefixVariableName ? `$${prefixVariableName}#${value}` : value); + const _sortKeyName = sortKeyName ? sortKeyName : argName; + return block('Applying Key Condition', [ + iff( + raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.beginsWith)`), + compoundExpression([ + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND begins_with(#sortKey, :sortKey)"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), + // TODO: Handle N & B. + qref( + `$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue( + `$ctx.args.${argName}.beginsWith` + )}" })` + ), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.between)`), + compoundExpression([ + set( + ref(`${queryExprReference}.expression`), + raw(`"$${queryExprReference}.expression AND #sortKey BETWEEN :sortKey0 AND :sortKey1"`) + ), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), + // TODO: Handle N & B. + qref( + `$${queryExprReference}.expressionValues.put(":sortKey0", { "${attributeType}": "${prefixValue( + `$ctx.args.${argName}.between[0]` + )}" })` + ), + qref( + `$${queryExprReference}.expressionValues.put(":sortKey1", { "${attributeType}": "${prefixValue( + `$ctx.args.${argName}.between[1]` + )}" })` + ), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.eq)`), + compoundExpression([ + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey = :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), + // TODO: Handle N & B. + qref( + `$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.eq`)}" })` + ), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.lt)`), + compoundExpression([ + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey < :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), + // TODO: Handle N & B. + qref( + `$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.lt`)}" })` + ), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.le)`), + compoundExpression([ + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey <= :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), + // TODO: Handle N & B. + qref( + `$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.le`)}" })` + ), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.gt)`), + compoundExpression([ + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey > :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), + // TODO: Handle N & B. + qref( + `$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.gt`)}" })` + ), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${argName}) && !$util.isNull($ctx.args.${argName}.ge)`), + compoundExpression([ + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey >= :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${_sortKeyName}")`), + // TODO: Handle N & B. + qref( + `$${queryExprReference}.expressionValues.put(":sortKey", { "${attributeType}": "${prefixValue(`$ctx.args.${argName}.ge`)}" })` + ), + ]) + ), + ]); } /** -* Key conditions materialize as instances of ModelXKeyConditionInput passed via $ctx.args. -* If the arguments with the given sortKey name exists, create a DynamoDB expression that -* implements its logic. Possible operators: eq, le, lt, ge, gt, beginsWith, and between. -* @param argName The name of the argument containing the sort key condition object. -* @param attributeType The type of the DynamoDB attribute in the table. -* @param queryExprReference The name of the variable containing the query expression in the template. -*/ -export function applyCompositeKeyConditionExpression(keyNames: string[], queryExprReference: string = 'query', sortKeyArgumentName: string, sortKeyAttributeName: string) { - const accumulatorVar1 = 'sortKeyValue'; - const accumulatorVar2 = 'sortKeyValue2'; - const sep = ModelResourceIDs.ModelCompositeKeySeparator(); - return block("Applying Key Condition", [ - set(ref(accumulatorVar1), str("")), - set(ref(accumulatorVar2), str("")), - iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.beginsWith)`), - compoundExpression([ - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.beginsWith.${keyName})`), - idx === 0 ? - set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.beginsWith.${keyName}`)) : - set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.beginsWith.${keyName}`)), - true - ) - ), - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND begins_with(#sortKey, :sortKey)"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.between)`), - compoundExpression([ - iff( - raw(`$ctx.args.${sortKeyArgumentName}.between.size() != 2`), - raw(`$util.error("Argument ${sortKeyArgumentName}.between expects exactly 2 elements.")`) - ), - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.between[0].${keyName})`), - idx === 0 ? - set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.between[0].${keyName}`)) : - set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.between[0].${keyName}`)), - true - )), - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.between[1].${keyName})`), - idx === 0 ? - set(ref(accumulatorVar2), str(`$ctx.args.${sortKeyArgumentName}.between[1].${keyName}`)) : - set(ref(accumulatorVar2), str(`$${accumulatorVar2}${sep}$ctx.args.${sortKeyArgumentName}.between[1].${keyName}`)), - true - )), - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey BETWEEN :sortKey0 AND :sortKey1"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey0", { "S": "$${accumulatorVar1}" })`), - qref(`$${queryExprReference}.expressionValues.put(":sortKey1", { "S": "$${accumulatorVar2}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.eq)`), - compoundExpression([ - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.eq.${keyName})`), - idx === 0 ? - set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.eq.${keyName}`)) : - set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.eq.${keyName}`)), - true - )), - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey = :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.lt)`), - compoundExpression([ - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.lt.${keyName})`), - idx === 0 ? - set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.lt.${keyName}`)) : - set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.lt.${keyName}`)), - true - )), - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey < :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.le)`), - compoundExpression([ - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.le.${keyName})`), - idx === 0 ? - set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.le.${keyName}`)) : - set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.le.${keyName}`)), - true - )), - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey <= :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.gt)`), - compoundExpression([ - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.gt.${keyName})`), - idx === 0 ? - set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.gt.${keyName}`)) : - set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.gt.${keyName}`)), - true - )), - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey > :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`) - ]) - ), - iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.ge)`), - compoundExpression([ - ...keyNames.map( - (keyName, idx) => iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.ge.${keyName})`), - idx === 0 ? - set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.ge.${keyName}`)) : - set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.ge.${keyName}`)), - true - )), - set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey >= :sortKey"`)), - qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), - // TODO: Handle N & B. - qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`) - ]) - ), - newline() - ]); + * Key conditions materialize as instances of ModelXKeyConditionInput passed via $ctx.args. + * If the arguments with the given sortKey name exists, create a DynamoDB expression that + * implements its logic. Possible operators: eq, le, lt, ge, gt, beginsWith, and between. + * @param argName The name of the argument containing the sort key condition object. + * @param attributeType The type of the DynamoDB attribute in the table. + * @param queryExprReference The name of the variable containing the query expression in the template. + */ +export function applyCompositeKeyConditionExpression( + keyNames: string[], + queryExprReference: string = 'query', + sortKeyArgumentName: string, + sortKeyAttributeName: string +) { + const accumulatorVar1 = 'sortKeyValue'; + const accumulatorVar2 = 'sortKeyValue2'; + const sep = ModelResourceIDs.ModelCompositeKeySeparator(); + return block('Applying Key Condition', [ + set(ref(accumulatorVar1), str('')), + set(ref(accumulatorVar2), str('')), + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.beginsWith)`), + compoundExpression([ + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.beginsWith.${keyName})`), + idx === 0 + ? set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.beginsWith.${keyName}`)) + : set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.beginsWith.${keyName}`)), + true + ) + ), + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND begins_with(#sortKey, :sortKey)"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), + // TODO: Handle N & B. + qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.between)`), + compoundExpression([ + iff( + raw(`$ctx.args.${sortKeyArgumentName}.between.size() != 2`), + raw(`$util.error("Argument ${sortKeyArgumentName}.between expects exactly 2 elements.")`) + ), + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.between[0].${keyName})`), + idx === 0 + ? set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.between[0].${keyName}`)) + : set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.between[0].${keyName}`)), + true + ) + ), + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.between[1].${keyName})`), + idx === 0 + ? set(ref(accumulatorVar2), str(`$ctx.args.${sortKeyArgumentName}.between[1].${keyName}`)) + : set(ref(accumulatorVar2), str(`$${accumulatorVar2}${sep}$ctx.args.${sortKeyArgumentName}.between[1].${keyName}`)), + true + ) + ), + set( + ref(`${queryExprReference}.expression`), + raw(`"$${queryExprReference}.expression AND #sortKey BETWEEN :sortKey0 AND :sortKey1"`) + ), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), + // TODO: Handle N & B. + qref(`$${queryExprReference}.expressionValues.put(":sortKey0", { "S": "$${accumulatorVar1}" })`), + qref(`$${queryExprReference}.expressionValues.put(":sortKey1", { "S": "$${accumulatorVar2}" })`), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.eq)`), + compoundExpression([ + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.eq.${keyName})`), + idx === 0 + ? set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.eq.${keyName}`)) + : set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.eq.${keyName}`)), + true + ) + ), + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey = :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), + // TODO: Handle N & B. + qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.lt)`), + compoundExpression([ + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.lt.${keyName})`), + idx === 0 + ? set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.lt.${keyName}`)) + : set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.lt.${keyName}`)), + true + ) + ), + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey < :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), + // TODO: Handle N & B. + qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.le)`), + compoundExpression([ + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.le.${keyName})`), + idx === 0 + ? set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.le.${keyName}`)) + : set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.le.${keyName}`)), + true + ) + ), + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey <= :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), + // TODO: Handle N & B. + qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.gt)`), + compoundExpression([ + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.gt.${keyName})`), + idx === 0 + ? set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.gt.${keyName}`)) + : set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.gt.${keyName}`)), + true + ) + ), + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey > :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), + // TODO: Handle N & B. + qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`), + ]) + ), + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && !$util.isNull($ctx.args.${sortKeyArgumentName}.ge)`), + compoundExpression([ + ...keyNames.map((keyName, idx) => + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}.ge.${keyName})`), + idx === 0 + ? set(ref(accumulatorVar1), str(`$ctx.args.${sortKeyArgumentName}.ge.${keyName}`)) + : set(ref(accumulatorVar1), str(`$${accumulatorVar1}${sep}$ctx.args.${sortKeyArgumentName}.ge.${keyName}`)), + true + ) + ), + set(ref(`${queryExprReference}.expression`), raw(`"$${queryExprReference}.expression AND #sortKey >= :sortKey"`)), + qref(`$${queryExprReference}.expressionNames.put("#sortKey", "${sortKeyAttributeName}")`), + // TODO: Handle N & B. + qref(`$${queryExprReference}.expressionValues.put(":sortKey", { "S": "$${accumulatorVar1}" })`), + ]) + ), + newline(), + ]); } - /** -* Key conditions materialize as instances of ModelXKeyConditionInput passed via $ctx.args. -* If the arguments with the given sortKey name exists, create a DynamoDB expression that -* implements its logic. Possible operators: eq, le, lt, ge, gt, beginsWith, and between. -* @param argName The name of the argument containing the sort key condition object. -* @param attributeType The type of the DynamoDB attribute in the table. -* @param queryExprReference The name of the variable containing the query expression in the template. -* @param compositeKeyName When handling a managed composite key from @key the name of the arg and underlying fields are different. -* @param compositeKeyValue When handling a managed composite key from @key the value of the composite key is made up of multiple parts known by the caller. -*/ -export function applyKeyExpressionForCompositeKey(keys: string[], attributeTypes: ('S' | 'N' | 'B')[] = ['S'], queryExprReference: string = 'query') { - if (keys.length > 2) { - // In the case of > 2, we condense the composite key, validate inputs at runtime, and wire up the HASH/RANGE expressions. - // In the case of === 2, we validate inputs at runtime and wire up the HASH/RANGE expressions. - const hashKeyName = keys[0]; - const hashKeyAttributeType = attributeTypes[0]; - const sortKeys = keys.slice(1); - const sortKeyTypes = attributeTypes.slice(1); - return compoundExpression([ - validateCompositeKeyArguments(keys), - setupHashKeyExpression(hashKeyName, hashKeyAttributeType, queryExprReference), - applyCompositeSortKey(sortKeys, sortKeyTypes, queryExprReference) - ]); - } else if (keys.length === 2) { - // In the case of === 2, we validate inputs at runtime and wire up the HASH/RANGE expressions. - const hashKeyName = keys[0]; - const hashKeyAttributeType = attributeTypes[0]; - const sortKeyName = keys[1]; - const sortKeyAttributeType = attributeTypes[1]; - return compoundExpression([ - validateKeyArguments(keys), - setupHashKeyExpression(hashKeyName, hashKeyAttributeType, queryExprReference), - applyKeyConditionExpression(sortKeyName, sortKeyAttributeType, queryExprReference) - ]); - } else if (keys.length === 1) { - const hashKeyName = keys[0]; - const hashKeyAttributeType = attributeTypes[0]; - return setupHashKeyExpression(hashKeyName, hashKeyAttributeType, queryExprReference); - } + * Key conditions materialize as instances of ModelXKeyConditionInput passed via $ctx.args. + * If the arguments with the given sortKey name exists, create a DynamoDB expression that + * implements its logic. Possible operators: eq, le, lt, ge, gt, beginsWith, and between. + * @param argName The name of the argument containing the sort key condition object. + * @param attributeType The type of the DynamoDB attribute in the table. + * @param queryExprReference The name of the variable containing the query expression in the template. + * @param compositeKeyName When handling a managed composite key from @key the name of the arg and underlying fields are different. + * @param compositeKeyValue When handling a managed composite key from @key the value of the composite key is made up of multiple parts known by the caller. + */ +export function applyKeyExpressionForCompositeKey( + keys: string[], + attributeTypes: ('S' | 'N' | 'B')[] = ['S'], + queryExprReference: string = 'query' +) { + if (keys.length > 2) { + // In the case of > 2, we condense the composite key, validate inputs at runtime, and wire up the HASH/RANGE expressions. + // In the case of === 2, we validate inputs at runtime and wire up the HASH/RANGE expressions. + const hashKeyName = keys[0]; + const hashKeyAttributeType = attributeTypes[0]; + const sortKeys = keys.slice(1); + const sortKeyTypes = attributeTypes.slice(1); + return compoundExpression([ + validateCompositeKeyArguments(keys), + setupHashKeyExpression(hashKeyName, hashKeyAttributeType, queryExprReference), + applyCompositeSortKey(sortKeys, sortKeyTypes, queryExprReference), + ]); + } else if (keys.length === 2) { + // In the case of === 2, we validate inputs at runtime and wire up the HASH/RANGE expressions. + const hashKeyName = keys[0]; + const hashKeyAttributeType = attributeTypes[0]; + const sortKeyName = keys[1]; + const sortKeyAttributeType = attributeTypes[1]; + return compoundExpression([ + validateKeyArguments(keys), + setupHashKeyExpression(hashKeyName, hashKeyAttributeType, queryExprReference), + applyKeyConditionExpression(sortKeyName, sortKeyAttributeType, queryExprReference), + ]); + } else if (keys.length === 1) { + const hashKeyName = keys[0]; + const hashKeyAttributeType = attributeTypes[0]; + return setupHashKeyExpression(hashKeyName, hashKeyAttributeType, queryExprReference); + } } function setupHashKeyExpression(hashKeyName: string, hashKeyAttributeType: string, queryExprReference: string) { - return iff( - raw(`!$util.isNull($ctx.args.${hashKeyName})`), - compoundExpression([ - set(ref(`${queryExprReference}.expression`), str(`#${hashKeyName} = :${hashKeyName}`)), - set(ref(`${queryExprReference}.expressionNames`), obj({ [`#${hashKeyName}`]: str(hashKeyName) })), - set(ref(`${queryExprReference}.expressionValues`), obj({ [`:${hashKeyName}`]: obj({ [hashKeyAttributeType]: str(`$ctx.args.${hashKeyName}`) }) })), - ]) - ) + return iff( + raw(`!$util.isNull($ctx.args.${hashKeyName})`), + compoundExpression([ + set(ref(`${queryExprReference}.expression`), str(`#${hashKeyName} = :${hashKeyName}`)), + set(ref(`${queryExprReference}.expressionNames`), obj({ [`#${hashKeyName}`]: str(hashKeyName) })), + set( + ref(`${queryExprReference}.expressionValues`), + obj({ [`:${hashKeyName}`]: obj({ [hashKeyAttributeType]: str(`$ctx.args.${hashKeyName}`) }) }) + ), + ]) + ); } /** * Applies a composite sort key to the query expression. */ -function applyCompositeSortKey(sortKeys: string[], sortKeyTypes: ('S'|'N'|'B')[], queryExprReference: string) { - if (sortKeys.length === 0) { - return newline(); - } - // E.g. status#date - const sortKeyAttributeName = ModelResourceIDs.ModelCompositeAttributeName(sortKeys); - const sortKeyArgumentName = ModelResourceIDs.ModelCompositeKeyArgumentName(sortKeys); - return compoundExpression([ - applyCompositeKeyConditionExpression(sortKeys, queryExprReference, sortKeyArgumentName, sortKeyAttributeName) - ]) +function applyCompositeSortKey(sortKeys: string[], sortKeyTypes: ('S' | 'N' | 'B')[], queryExprReference: string) { + if (sortKeys.length === 0) { + return newline(); + } + // E.g. status#date + const sortKeyAttributeName = ModelResourceIDs.ModelCompositeAttributeName(sortKeys); + const sortKeyArgumentName = ModelResourceIDs.ModelCompositeKeyArgumentName(sortKeys); + return compoundExpression([ + applyCompositeKeyConditionExpression(sortKeys, queryExprReference, sortKeyArgumentName, sortKeyAttributeName), + ]); } /** @@ -415,92 +500,110 @@ function applyCompositeSortKey(sortKeys: string[], sortKeyTypes: ('S'|'N'|'B')[] * query by ["k1", "k3"] as it is impossible to create a key condition without * the "k2" value. This snippet fails a query/list operation when invalid * argument sets are provided. - * @param keys + * @param keys */ function validateKeyArguments(keys: string[]) { - const exprs: Expression[] = []; - if (keys.length > 1) { - for (let index = keys.length - 1; index > 0; index--) { - const rightKey = keys[index]; - const previousKey = keys[index - 1]; - exprs.push( - iff( - raw(`!$util.isNull($ctx.args.${rightKey}) && $util.isNull($ctx.args.${previousKey})`), - raw(`$util.error("When providing argument '${rightKey}' you must also provide arguments ${keys.slice(0, index).join(', ')}", "InvalidArgumentsError")`) - ) - ) - } - return block('Validate key arguments.', exprs); - } else { - return newline(); + const exprs: Expression[] = []; + if (keys.length > 1) { + for (let index = keys.length - 1; index > 0; index--) { + const rightKey = keys[index]; + const previousKey = keys[index - 1]; + exprs.push( + iff( + raw(`!$util.isNull($ctx.args.${rightKey}) && $util.isNull($ctx.args.${previousKey})`), + raw( + `$util.error("When providing argument '${rightKey}' you must also provide arguments ${keys + .slice(0, index) + .join(', ')}", "InvalidArgumentsError")` + ) + ) + ); } + return block('Validate key arguments.', exprs); + } else { + return newline(); + } } function invalidArgumentError(err: string) { - return raw(`$util.error("${err}", "InvalidArgumentsError")`); + return raw(`$util.error("${err}", "InvalidArgumentsError")`); } function validateCompositeKeyArguments(keys: string[]) { - const sortKeys = keys.slice(1); - const hashKey = keys[0]; - const sortKeyArgumentName = ModelResourceIDs.ModelCompositeKeyArgumentName(sortKeys); - const exprs: Expression[] = [ + const sortKeys = keys.slice(1); + const hashKey = keys[0]; + const sortKeyArgumentName = ModelResourceIDs.ModelCompositeKeyArgumentName(sortKeys); + const exprs: Expression[] = [ + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && $util.isNullOrBlank($ctx.args.${hashKey})`), + invalidArgumentError(`When providing argument '${sortKeyArgumentName}' you must also provide '${hashKey}'.`) + ), + ]; + if (sortKeys.length > 1) { + const loopOverKeys = (fn: (rKey: string, pKey: string) => Expression) => { + const exprs = []; + for (let index = sortKeys.length - 1; index > 0; index--) { + const rightKey = sortKeys[index]; + const previousKey = sortKeys[index - 1]; + exprs.push(fn(rightKey, previousKey)); + } + return compoundExpression(exprs); + }; + const validateBetween = () => + compoundExpression([ iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName}) && $util.isNullOrBlank($ctx.args.${hashKey})`), - invalidArgumentError(`When providing argument '${sortKeyArgumentName}' you must also provide '${hashKey}'.`) - ) - ]; - if (sortKeys.length > 1) { - const loopOverKeys = (fn: (rKey: string, pKey: string) => Expression) => { - const exprs = []; - for (let index = sortKeys.length - 1; index > 0; index--) { - const rightKey = sortKeys[index]; - const previousKey = sortKeys[index - 1]; - exprs.push(fn(rightKey, previousKey)) - } - return compoundExpression(exprs); - } - const validateBetween = () => compoundExpression([ + raw(`$ctx.args.${sortKeyArgumentName}.between.size() != 2`), + invalidArgumentError(`Argument '${sortKeyArgumentName}.between' expects exactly two elements.`) + ), + loopOverKeys((rightKey: string, previousKey: string) => + compoundExpression([ iff( - raw(`$ctx.args.${sortKeyArgumentName}.between.size() != 2`), - invalidArgumentError(`Argument '${sortKeyArgumentName}.between' expects exactly two elements.`) + raw( + `!$util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[0].${rightKey}) && $util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[0].${previousKey})` + ), + invalidArgumentError( + `When providing argument '${sortKeyArgumentName}.between[0].${rightKey}' you must also provide '${sortKeyArgumentName}.between[0].${previousKey}'.` + ) ), - loopOverKeys((rightKey: string, previousKey: string) => compoundExpression([ - iff( - raw(`!$util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[0].${rightKey}) && $util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[0].${previousKey})`), - invalidArgumentError(`When providing argument '${sortKeyArgumentName}.between[0].${rightKey}' you must also provide '${sortKeyArgumentName}.between[0].${previousKey}'.`) - ), - iff( - raw(`!$util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[1].${rightKey}) && $util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[1].${previousKey})`), - invalidArgumentError(`When providing argument '${sortKeyArgumentName}.between[1].${rightKey}' you must also provide '${sortKeyArgumentName}.between[1].${previousKey}'.`) - ) - ])) - ]); - const validateOtherOperation = () => loopOverKeys((rightKey: string, previousKey: string) => iff( - raw(`!$util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.get("$operation").${rightKey}) && $util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.get("$operation").${previousKey})`), - invalidArgumentError(`When providing argument '${sortKeyArgumentName}.$operation.${rightKey}' you must also provide '${sortKeyArgumentName}.$operation.${previousKey}'.`) - )); - exprs.push( iff( - raw(`!$util.isNull($ctx.args.${sortKeyArgumentName})`), - compoundExpression([ - set(ref('sortKeyArgumentOperations'), raw(`$ctx.args.${sortKeyArgumentName}.keySet()`)), - iff( - raw(`$sortKeyArgumentOperations.size() > 1`), - invalidArgumentError(`Argument ${sortKeyArgumentName} must specify at most one key condition operation.`) - ), - forEach(ref('operation'), ref('sortKeyArgumentOperations'), [ - ifElse( - raw(`$operation == "between"`), - validateBetween(), - validateOtherOperation() - ) - ]) - ]) - ) + raw( + `!$util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[1].${rightKey}) && $util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.between[1].${previousKey})` + ), + invalidArgumentError( + `When providing argument '${sortKeyArgumentName}.between[1].${rightKey}' you must also provide '${sortKeyArgumentName}.between[1].${previousKey}'.` + ) + ), + ]) + ), + ]); + const validateOtherOperation = () => + loopOverKeys((rightKey: string, previousKey: string) => + iff( + raw( + `!$util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.get("$operation").${rightKey}) && $util.isNullOrBlank($ctx.args.${sortKeyArgumentName}.get("$operation").${previousKey})` + ), + invalidArgumentError( + `When providing argument '${sortKeyArgumentName}.$operation.${rightKey}' you must also provide '${sortKeyArgumentName}.$operation.${previousKey}'.` + ) ) - return block('Validate key arguments.', exprs); - } else { - return newline(); - } + ); + exprs.push( + iff( + raw(`!$util.isNull($ctx.args.${sortKeyArgumentName})`), + compoundExpression([ + set(ref('sortKeyArgumentOperations'), raw(`$ctx.args.${sortKeyArgumentName}.keySet()`)), + iff( + raw(`$sortKeyArgumentOperations.size() > 1`), + invalidArgumentError(`Argument ${sortKeyArgumentName} must specify at most one key condition operation.`) + ), + forEach(ref('operation'), ref('sortKeyArgumentOperations'), [ + ifElse(raw(`$operation == "between"`), validateBetween(), validateOtherOperation()), + ]), + ]) + ) + ); + return block('Validate key arguments.', exprs); + } else { + return newline(); + } } diff --git a/packages/graphql-transformer-common/src/index.ts b/packages/graphql-transformer-common/src/index.ts index 4114464790..86118681b6 100644 --- a/packages/graphql-transformer-common/src/index.ts +++ b/packages/graphql-transformer-common/src/index.ts @@ -1,11 +1,11 @@ -export * from './ResourceConstants' -export * from './definition' -export * from './util' -export * from './ResolverResourceIDs' -export * from './ModelResourceIDs' -export * from './SearchableResourceIDs' -export * from './nodeUtils' -export * from './HttpResourceIDs' -export * from './FunctionResourceIDs' +export * from './ResourceConstants'; +export * from './definition'; +export * from './util'; +export * from './ResolverResourceIDs'; +export * from './ModelResourceIDs'; +export * from './SearchableResourceIDs'; +export * from './nodeUtils'; +export * from './HttpResourceIDs'; +export * from './FunctionResourceIDs'; export * from './connectionUtils'; -export * from './dynamodbUtils'; \ No newline at end of file +export * from './dynamodbUtils'; diff --git a/packages/graphql-transformer-common/src/nodeUtils.ts b/packages/graphql-transformer-common/src/nodeUtils.ts index e62460e054..1ce0c5d53b 100644 --- a/packages/graphql-transformer-common/src/nodeUtils.ts +++ b/packages/graphql-transformer-common/src/nodeUtils.ts @@ -1,24 +1,24 @@ import { Kind, TypeNode } from 'graphql'; export function withNamedNodeNamed(t: TypeNode, n: string): TypeNode { - switch (t.kind) { - case Kind.NON_NULL_TYPE: - return { - ...t, - type: withNamedNodeNamed(t.type, n) - } as TypeNode - case Kind.LIST_TYPE: - return { - ...t, - type: withNamedNodeNamed(t.type, n) - } as TypeNode - case Kind.NAMED_TYPE: - return { - ...t, - name: { - kind: Kind.NAME, - value: n - } - } - } + switch (t.kind) { + case Kind.NON_NULL_TYPE: + return { + ...t, + type: withNamedNodeNamed(t.type, n), + } as TypeNode; + case Kind.LIST_TYPE: + return { + ...t, + type: withNamedNodeNamed(t.type, n), + } as TypeNode; + case Kind.NAMED_TYPE: + return { + ...t, + name: { + kind: Kind.NAME, + value: n, + }, + }; + } } diff --git a/packages/graphql-transformer-common/src/util.ts b/packages/graphql-transformer-common/src/util.ts index d12f7dbac0..16752c68de 100644 --- a/packages/graphql-transformer-common/src/util.ts +++ b/packages/graphql-transformer-common/src/util.ts @@ -1,32 +1,43 @@ export function plurality(val: string): string { - if (!val.trim()) { return ''; } - return val.concat('s') + if (!val.trim()) { + return ''; + } + return val.concat('s'); } export function graphqlName(val: string): string { - if (!val.trim()) { return ''; } - const cleaned = val.replace(/^[^_A-Za-z]+|[^_0-9A-Za-z]/g, '') - return cleaned + if (!val.trim()) { + return ''; + } + const cleaned = val.replace(/^[^_A-Za-z]+|[^_0-9A-Za-z]/g, ''); + return cleaned; } export function simplifyName(val: string): string { - if (!val.trim()) { return ''; } - return toPascalCase(val.replace(/-?_?\${[^}]*}/g, '').replace(/^[^_A-Za-z]+|[^_0-9A-Za-z]/g, '|').split('|')) + if (!val.trim()) { + return ''; + } + return toPascalCase( + val + .replace(/-?_?\${[^}]*}/g, '') + .replace(/^[^_A-Za-z]+|[^_0-9A-Za-z]/g, '|') + .split('|') + ); } export function toUpper(word: string): string { - return word.charAt(0).toUpperCase() + word.slice(1) + return word.charAt(0).toUpperCase() + word.slice(1); } export function toCamelCase(words: string[]): string { - const formatted = words.map((w, i) => i === 0 ? w.charAt(0).toLowerCase() + w.slice(1) : w.charAt(0).toUpperCase() + w.slice(1)) - return formatted.join('') + const formatted = words.map((w, i) => (i === 0 ? w.charAt(0).toLowerCase() + w.slice(1) : w.charAt(0).toUpperCase() + w.slice(1))); + return formatted.join(''); } export function toPascalCase(words: string[]): string { - const formatted = words.map((w, i) => w.charAt(0).toUpperCase() + w.slice(1)) - return formatted.join('') + const formatted = words.map((w, i) => w.charAt(0).toUpperCase() + w.slice(1)); + return formatted.join(''); } -export const NONE_VALUE = '___xamznone____' +export const NONE_VALUE = '___xamznone____'; export const NONE_INT_VALUE = -2147483648; diff --git a/packages/graphql-transformer-core/src/DeploymentResources.ts b/packages/graphql-transformer-core/src/DeploymentResources.ts index 91fff1ff8f..8d2a59959f 100644 --- a/packages/graphql-transformer-core/src/DeploymentResources.ts +++ b/packages/graphql-transformer-core/src/DeploymentResources.ts @@ -1,30 +1,32 @@ import { Template } from 'cloudform-types'; -import { NestedStacks } from './util/splitStack' +import { NestedStacks } from './util/splitStack'; export type StringMap = { - [path: string]: string -} -export type ResolverMap = StringMap -export type PipelineFunctionMap = StringMap + [path: string]: string; +}; +export type ResolverMap = StringMap; +export type PipelineFunctionMap = StringMap; export interface ResolversFunctionsAndSchema { - // Resolver templates keyed by their filename. - resolvers: ResolverMap, - // Contains mapping templates for pipeline functions. - pipelineFunctions: PipelineFunctionMap, - // Code for any functions that need to be deployed. - functions: { - [path: string]: string - }, - // The full GraphQL schema. - schema: string, + // Resolver templates keyed by their filename. + resolvers: ResolverMap; + // Contains mapping templates for pipeline functions. + pipelineFunctions: PipelineFunctionMap; + // Code for any functions that need to be deployed. + functions: { + [path: string]: string; + }; + // The full GraphQL schema. + schema: string; +} +export interface StackMapping { + [resourceId: string]: string; } -export interface StackMapping { [resourceId: string]: string } /** * The full set of resources needed for the deployment. */ export interface DeploymentResources extends ResolversFunctionsAndSchema, NestedStacks { - // The full stack mapping for the deployment. - stackMapping: StackMapping + // The full stack mapping for the deployment. + stackMapping: StackMapping; } -export default DeploymentResources \ No newline at end of file +export default DeploymentResources; diff --git a/packages/graphql-transformer-core/src/GraphQLTransform.ts b/packages/graphql-transformer-core/src/GraphQLTransform.ts index c455a28bb7..7b2502aa55 100644 --- a/packages/graphql-transformer-core/src/GraphQLTransform.ts +++ b/packages/graphql-transformer-core/src/GraphQLTransform.ts @@ -1,43 +1,55 @@ -import Template from 'cloudform-types/types/template' +import Template from 'cloudform-types/types/template'; import { - TypeSystemDefinitionNode, DirectiveDefinitionNode, - Kind, DirectiveNode, TypeDefinitionNode, ObjectTypeDefinitionNode, - InterfaceTypeDefinitionNode, ScalarTypeDefinitionNode, UnionTypeDefinitionNode, - EnumTypeDefinitionNode, InputObjectTypeDefinitionNode, FieldDefinitionNode, - InputValueDefinitionNode, EnumValueDefinitionNode, validate, TypeExtensionNode -} from 'graphql' -import { DeploymentResources } from './DeploymentResources' -import TransformerContext from './TransformerContext' -import blankTemplate from './util/blankTemplate' -import Transformer from './Transformer' -import ITransformer from './ITransformer' -import { InvalidTransformerError, UnknownDirectiveError, SchemaValidationError } from './errors' -import { validateModelSchema } from './validation' + TypeSystemDefinitionNode, + DirectiveDefinitionNode, + Kind, + DirectiveNode, + TypeDefinitionNode, + ObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + ScalarTypeDefinitionNode, + UnionTypeDefinitionNode, + EnumTypeDefinitionNode, + InputObjectTypeDefinitionNode, + FieldDefinitionNode, + InputValueDefinitionNode, + EnumValueDefinitionNode, + validate, + TypeExtensionNode, +} from 'graphql'; +import { DeploymentResources } from './DeploymentResources'; +import TransformerContext from './TransformerContext'; +import blankTemplate from './util/blankTemplate'; +import Transformer from './Transformer'; +import ITransformer from './ITransformer'; +import { InvalidTransformerError, UnknownDirectiveError, SchemaValidationError } from './errors'; +import { validateModelSchema } from './validation'; import { TransformFormatter } from './TransformFormatter'; function isFunction(obj: any) { - return obj && (typeof obj === 'function') + return obj && typeof obj === 'function'; } function makeSeenTransformationKey( - directive: DirectiveNode, - type: TypeDefinitionNode, - field?: FieldDefinitionNode | InputValueDefinitionNode | EnumValueDefinitionNode, - arg?: InputValueDefinitionNode, - index?: number + directive: DirectiveNode, + type: TypeDefinitionNode, + field?: FieldDefinitionNode | InputValueDefinitionNode | EnumValueDefinitionNode, + arg?: InputValueDefinitionNode, + index?: number ): string { - let key = ''; - if (directive && type && field && arg) { - key = `${type.name.value}.${field.name.value}.${arg.name.value}@${directive.name.value}` - } if (directive && type && field) { - key = `${type.name.value}.${field.name.value}@${directive.name.value}` - } else { - key = `${type.name.value}@${directive.name.value}` - } - if (index !== undefined) { - key += `[${index}]`; - } - return key; + let key = ''; + if (directive && type && field && arg) { + key = `${type.name.value}.${field.name.value}.${arg.name.value}@${directive.name.value}`; + } + if (directive && type && field) { + key = `${type.name.value}.${field.name.value}@${directive.name.value}`; + } else { + key = `${type.name.value}@${directive.name.value}`; + } + if (index !== undefined) { + key += `[${index}]`; + } + return key; } /** @@ -47,116 +59,116 @@ function makeSeenTransformationKey( * @param nodeKind The kind of the current node where the directive was found. */ function matchDirective(definition: DirectiveDefinitionNode, directive: DirectiveNode, node: TypeSystemDefinitionNode) { - if (!directive) { - return false; - } - if (definition.name.value !== directive.name.value) { - // The definition is for the wrong directive. Do not match. - return false; - } - let isValidLocation = false; - for (const location of definition.locations) { - switch (location.value) { - case `SCHEMA`: - isValidLocation = node.kind === Kind.SCHEMA_DEFINITION || isValidLocation - break - case `SCALAR`: - isValidLocation = node.kind === Kind.SCALAR_TYPE_DEFINITION || isValidLocation - break - case `OBJECT`: - isValidLocation = node.kind === Kind.OBJECT_TYPE_DEFINITION || isValidLocation - break - case `FIELD_DEFINITION`: - isValidLocation = node.kind as string === Kind.FIELD_DEFINITION || isValidLocation - break - case `ARGUMENT_DEFINITION`: - isValidLocation = node.kind as string === Kind.INPUT_VALUE_DEFINITION || isValidLocation - break - case `INTERFACE`: - isValidLocation = node.kind === Kind.INTERFACE_TYPE_DEFINITION || isValidLocation - break - case `UNION`: - isValidLocation = node.kind === Kind.UNION_TYPE_DEFINITION || isValidLocation - break - case `ENUM`: - isValidLocation = node.kind === Kind.ENUM_TYPE_DEFINITION || isValidLocation - break - case `ENUM_VALUE`: - isValidLocation = node.kind as string === Kind.ENUM_VALUE_DEFINITION || isValidLocation - break - case `INPUT_OBJECT`: - isValidLocation = node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION || isValidLocation - break - case `INPUT_FIELD_DEFINITION`: - isValidLocation = node.kind as string === Kind.INPUT_VALUE_DEFINITION || isValidLocation - break - } + if (!directive) { + return false; + } + if (definition.name.value !== directive.name.value) { + // The definition is for the wrong directive. Do not match. + return false; + } + let isValidLocation = false; + for (const location of definition.locations) { + switch (location.value) { + case `SCHEMA`: + isValidLocation = node.kind === Kind.SCHEMA_DEFINITION || isValidLocation; + break; + case `SCALAR`: + isValidLocation = node.kind === Kind.SCALAR_TYPE_DEFINITION || isValidLocation; + break; + case `OBJECT`: + isValidLocation = node.kind === Kind.OBJECT_TYPE_DEFINITION || isValidLocation; + break; + case `FIELD_DEFINITION`: + isValidLocation = (node.kind as string) === Kind.FIELD_DEFINITION || isValidLocation; + break; + case `ARGUMENT_DEFINITION`: + isValidLocation = (node.kind as string) === Kind.INPUT_VALUE_DEFINITION || isValidLocation; + break; + case `INTERFACE`: + isValidLocation = node.kind === Kind.INTERFACE_TYPE_DEFINITION || isValidLocation; + break; + case `UNION`: + isValidLocation = node.kind === Kind.UNION_TYPE_DEFINITION || isValidLocation; + break; + case `ENUM`: + isValidLocation = node.kind === Kind.ENUM_TYPE_DEFINITION || isValidLocation; + break; + case `ENUM_VALUE`: + isValidLocation = (node.kind as string) === Kind.ENUM_VALUE_DEFINITION || isValidLocation; + break; + case `INPUT_OBJECT`: + isValidLocation = node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION || isValidLocation; + break; + case `INPUT_FIELD_DEFINITION`: + isValidLocation = (node.kind as string) === Kind.INPUT_VALUE_DEFINITION || isValidLocation; + break; } - return isValidLocation; + } + return isValidLocation; } function matchFieldDirective(definition: DirectiveDefinitionNode, directive: DirectiveNode, node: FieldDefinitionNode) { - if (definition.name.value !== directive.name.value) { - // The definition is for the wrong directive. Do not match. - return false; - } - let isValidLocation = false; - for (const location of definition.locations) { - switch (location.value) { - case `FIELD_DEFINITION`: - isValidLocation = node.kind === Kind.FIELD_DEFINITION || isValidLocation - break - } + if (definition.name.value !== directive.name.value) { + // The definition is for the wrong directive. Do not match. + return false; + } + let isValidLocation = false; + for (const location of definition.locations) { + switch (location.value) { + case `FIELD_DEFINITION`: + isValidLocation = node.kind === Kind.FIELD_DEFINITION || isValidLocation; + break; } - return isValidLocation; + } + return isValidLocation; } function matchInputFieldDirective(definition: DirectiveDefinitionNode, directive: DirectiveNode, node: InputValueDefinitionNode) { - if (definition.name.value !== directive.name.value) { - // The definition is for the wrong directive. Do not match. - return false; - } - let isValidLocation = false; - for (const location of definition.locations) { - switch (location.value) { - case `INPUT_FIELD_DEFINITION`: - isValidLocation = node.kind === Kind.INPUT_VALUE_DEFINITION || isValidLocation - break - } + if (definition.name.value !== directive.name.value) { + // The definition is for the wrong directive. Do not match. + return false; + } + let isValidLocation = false; + for (const location of definition.locations) { + switch (location.value) { + case `INPUT_FIELD_DEFINITION`: + isValidLocation = node.kind === Kind.INPUT_VALUE_DEFINITION || isValidLocation; + break; } - return isValidLocation; + } + return isValidLocation; } function matchArgumentDirective(definition: DirectiveDefinitionNode, directive: DirectiveNode, node: InputValueDefinitionNode) { - if (definition.name.value !== directive.name.value) { - // The definition is for the wrong directive. Do not match. - return false; - } - let isValidLocation = false; - for (const location of definition.locations) { - switch (location.value) { - case `ARGUMENT_DEFINITION`: - isValidLocation = node.kind === Kind.INPUT_VALUE_DEFINITION || isValidLocation - break - } + if (definition.name.value !== directive.name.value) { + // The definition is for the wrong directive. Do not match. + return false; + } + let isValidLocation = false; + for (const location of definition.locations) { + switch (location.value) { + case `ARGUMENT_DEFINITION`: + isValidLocation = node.kind === Kind.INPUT_VALUE_DEFINITION || isValidLocation; + break; } - return isValidLocation; + } + return isValidLocation; } function matchEnumValueDirective(definition: DirectiveDefinitionNode, directive: DirectiveNode, node: EnumValueDefinitionNode) { - if (definition.name.value !== directive.name.value) { - // The definition is for the wrong directive. Do not match. - return false; - } - let isValidLocation = false; - for (const location of definition.locations) { - switch (location.value) { - case `ENUM_VALUE`: - isValidLocation = node.kind === Kind.ENUM_VALUE_DEFINITION || isValidLocation - break - } + if (definition.name.value !== directive.name.value) { + // The definition is for the wrong directive. Do not match. + return false; + } + let isValidLocation = false; + for (const location of definition.locations) { + switch (location.value) { + case `ENUM_VALUE`: + isValidLocation = node.kind === Kind.ENUM_VALUE_DEFINITION || isValidLocation; + break; } - return isValidLocation; + } + return isValidLocation; } type TypeDefinitionOrExtension = TypeDefinitionNode | TypeExtensionNode; @@ -168,417 +180,418 @@ type TypeDefinitionOrExtension = TypeDefinitionNode | TypeExtensionNode; * is emitted. */ export interface GraphQLTransformOptions { - transformers: ITransformer[], - // Override the formatter's stack mapping. This is useful when handling - // migrations as all the input/export/ref/getatt changes will be made - // automatically. - stackMapping?: StackMapping, + transformers: ITransformer[]; + // Override the formatter's stack mapping. This is useful when handling + // migrations as all the input/export/ref/getatt changes will be made + // automatically. + stackMapping?: StackMapping; } export type StackMapping = { [resourceId: string]: string }; export default class GraphQLTransform { + private transformers: ITransformer[]; + private stackMappingOverrides: StackMapping; - private transformers: ITransformer[] - private stackMappingOverrides: StackMapping; - - // A map from `${directive}.${typename}.${fieldName?}`: true - // that specifies we have run already run a directive at a given location. - // Only run a transformer function once per pair. This is refreshed each call to transform(). - private seenTransformations: { [k: string]: boolean } = {} + // A map from `${directive}.${typename}.${fieldName?}`: true + // that specifies we have run already run a directive at a given location. + // Only run a transformer function once per pair. This is refreshed each call to transform(). + private seenTransformations: { [k: string]: boolean } = {}; - constructor(options: GraphQLTransformOptions) { - if (!options.transformers || options.transformers.length === 0) { - throw new Error('Must provide at least one transformer.') - } - this.transformers = options.transformers; - this.stackMappingOverrides = options.stackMapping || {}; + constructor(options: GraphQLTransformOptions) { + if (!options.transformers || options.transformers.length === 0) { + throw new Error('Must provide at least one transformer.'); } + this.transformers = options.transformers; + this.stackMappingOverrides = options.stackMapping || {}; + } - /** - * Reduces the final context by running the set of transformers on - * the schema. Each transformer returns a new context that is passed - * on to the next transformer. At the end of the transformation a - * cloudformation template is returned. - * @param schema The model schema. - * @param references Any cloudformation references. - */ - public transform(schema: string): DeploymentResources { - this.seenTransformations = {} - const context = new TransformerContext(schema) - const validDirectiveNameMap = this.transformers.reduce( - (acc: any, t: Transformer) => ({ ...acc, [t.directive.name.value]: true }), - { aws_subscribe: true, aws_auth: true, aws_api_key: true, aws_iam: true, - aws_oidc: true, aws_cognito_user_pools: true, deprecated: true } - ) - let allModelDefinitions = [...context.inputDocument.definitions] - for (const transformer of this.transformers) { - allModelDefinitions = allModelDefinitions.concat( - ...transformer.typeDefinitions, - transformer.directive - ) - } - const errors = validateModelSchema({ kind: Kind.DOCUMENT, definitions: allModelDefinitions }) - if (errors && errors.length) { - throw new SchemaValidationError(errors.slice(0)) - } + /** + * Reduces the final context by running the set of transformers on + * the schema. Each transformer returns a new context that is passed + * on to the next transformer. At the end of the transformation a + * cloudformation template is returned. + * @param schema The model schema. + * @param references Any cloudformation references. + */ + public transform(schema: string): DeploymentResources { + this.seenTransformations = {}; + const context = new TransformerContext(schema); + const validDirectiveNameMap = this.transformers.reduce((acc: any, t: Transformer) => ({ ...acc, [t.directive.name.value]: true }), { + aws_subscribe: true, + aws_auth: true, + aws_api_key: true, + aws_iam: true, + aws_oidc: true, + aws_cognito_user_pools: true, + deprecated: true, + }); + let allModelDefinitions = [...context.inputDocument.definitions]; + for (const transformer of this.transformers) { + allModelDefinitions = allModelDefinitions.concat(...transformer.typeDefinitions, transformer.directive); + } + const errors = validateModelSchema({ kind: Kind.DOCUMENT, definitions: allModelDefinitions }); + if (errors && errors.length) { + throw new SchemaValidationError(errors.slice(0)); + } - for (const transformer of this.transformers) { - if (isFunction(transformer.before)) { - transformer.before(context) - } - // TODO: Validate that the transformer supports all the methods - // required for the directive definition. Also verify that - // directives are not used where they are not allowed. + for (const transformer of this.transformers) { + if (isFunction(transformer.before)) { + transformer.before(context); + } + // TODO: Validate that the transformer supports all the methods + // required for the directive definition. Also verify that + // directives are not used where they are not allowed. - // Apply each transformer and accumulate the context. - for (const def of context.inputDocument.definitions as TypeDefinitionOrExtension[]) { - switch (def.kind) { - case 'ObjectTypeDefinition': - this.transformObject(transformer, def, validDirectiveNameMap, context) - // Walk the fields and call field transformers. - break - case 'InterfaceTypeDefinition': - this.transformInterface(transformer, def, validDirectiveNameMap, context) - // Walk the fields and call field transformers. - break; - case 'ScalarTypeDefinition': - this.transformScalar(transformer, def, validDirectiveNameMap, context) - break; - case 'UnionTypeDefinition': - this.transformUnion(transformer, def, validDirectiveNameMap, context) - break; - case 'EnumTypeDefinition': - this.transformEnum(transformer, def, validDirectiveNameMap, context) - break; - case 'InputObjectTypeDefinition': - this.transformInputObject(transformer, def, validDirectiveNameMap, context) - break; - default: - continue - } - } - } - // .transform() is meant to behave like a composition so the - // after functions are called in the reverse order (as if they were popping off a stack) - let reverseThroughTransformers = this.transformers.length - 1; - while (reverseThroughTransformers >= 0) { - const transformer = this.transformers[reverseThroughTransformers] - // TODO: Validate the new context. - if (1 !== 1) { - throw new Error(`Invalid context after transformer ${transformer.name}`) - } - if (isFunction(transformer.after)) { - transformer.after(context) - } - reverseThroughTransformers -= 1 + // Apply each transformer and accumulate the context. + for (const def of context.inputDocument.definitions as TypeDefinitionOrExtension[]) { + switch (def.kind) { + case 'ObjectTypeDefinition': + this.transformObject(transformer, def, validDirectiveNameMap, context); + // Walk the fields and call field transformers. + break; + case 'InterfaceTypeDefinition': + this.transformInterface(transformer, def, validDirectiveNameMap, context); + // Walk the fields and call field transformers. + break; + case 'ScalarTypeDefinition': + this.transformScalar(transformer, def, validDirectiveNameMap, context); + break; + case 'UnionTypeDefinition': + this.transformUnion(transformer, def, validDirectiveNameMap, context); + break; + case 'EnumTypeDefinition': + this.transformEnum(transformer, def, validDirectiveNameMap, context); + break; + case 'InputObjectTypeDefinition': + this.transformInputObject(transformer, def, validDirectiveNameMap, context); + break; + default: + continue; } - // Format the context into many stacks. - this.updateContextForStackMappingOverrides(context); - const formatter = new TransformFormatter(); - return formatter.format(context) + } } + // .transform() is meant to behave like a composition so the + // after functions are called in the reverse order (as if they were popping off a stack) + let reverseThroughTransformers = this.transformers.length - 1; + while (reverseThroughTransformers >= 0) { + const transformer = this.transformers[reverseThroughTransformers]; + // TODO: Validate the new context. + if (1 !== 1) { + throw new Error(`Invalid context after transformer ${transformer.name}`); + } + if (isFunction(transformer.after)) { + transformer.after(context); + } + reverseThroughTransformers -= 1; + } + // Format the context into many stacks. + this.updateContextForStackMappingOverrides(context); + const formatter = new TransformFormatter(); + return formatter.format(context); + } - private updateContextForStackMappingOverrides(context: TransformerContext) { - for (const resourceId of Object.keys(this.stackMappingOverrides)) { - context.mapResourceToStack(this.stackMappingOverrides[resourceId], resourceId); - } + private updateContextForStackMappingOverrides(context: TransformerContext) { + for (const resourceId of Object.keys(this.stackMappingOverrides)) { + context.mapResourceToStack(this.stackMappingOverrides[resourceId], resourceId); } + } - private transformObject( - transformer: Transformer, - def: ObjectTypeDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.object)) { - const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.object(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'object()' method`) - } - } - index++; - } - for (const field of def.fields) { - this.transformField(transformer, def, field, validDirectiveNameMap, context) + private transformObject( + transformer: Transformer, + def: ObjectTypeDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.object)) { + const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.object(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'object()' method`); } + } + index++; + } + for (const field of def.fields) { + this.transformField(transformer, def, field, validDirectiveNameMap, context); } + } - private transformField( - transformer: Transformer, - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - def: FieldDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchFieldDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.field)) { - const transformKey = makeSeenTransformationKey(dir, parent, def, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.field(parent, def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'field()' method`) - } - } - index++; - } - for (const arg of def.arguments) { - this.transformArgument(transformer, parent, def, arg, validDirectiveNameMap, context) + private transformField( + transformer: Transformer, + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + def: FieldDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchFieldDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.field)) { + const transformKey = makeSeenTransformationKey(dir, parent, def, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.field(parent, def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'field()' method`); } + } + index++; + } + for (const arg of def.arguments) { + this.transformArgument(transformer, parent, def, arg, validDirectiveNameMap, context); } + } - private transformArgument( - transformer: Transformer, - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - field: FieldDefinitionNode, - arg: InputValueDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of arg.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchArgumentDirective(transformer.directive, dir, arg)) { - if (isFunction(transformer.argument)) { - const transformKey = makeSeenTransformationKey(dir, parent, field, arg, index) - if (!this.seenTransformations[transformKey]) { - transformer.argument(arg, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'argument()' method`) - } - } - index++; + private transformArgument( + transformer: Transformer, + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + field: FieldDefinitionNode, + arg: InputValueDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of arg.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchArgumentDirective(transformer.directive, dir, arg)) { + if (isFunction(transformer.argument)) { + const transformKey = makeSeenTransformationKey(dir, parent, field, arg, index); + if (!this.seenTransformations[transformKey]) { + transformer.argument(arg, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'argument()' method`); } + } + index++; } + } - private transformInterface( - transformer: Transformer, - def: InterfaceTypeDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.interface)) { - const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.interface(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'interface()' method`) - } - } - index++; - } - for (const field of def.fields) { - this.transformField(transformer, def, field, validDirectiveNameMap, context) + private transformInterface( + transformer: Transformer, + def: InterfaceTypeDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.interface)) { + const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.interface(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'interface()' method`); } + } + index++; + } + for (const field of def.fields) { + this.transformField(transformer, def, field, validDirectiveNameMap, context); } + } - private transformScalar( - transformer: Transformer, - def: ScalarTypeDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.scalar)) { - const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.scalar(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'scalar()' method`) - } - } - index++; + private transformScalar( + transformer: Transformer, + def: ScalarTypeDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.scalar)) { + const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.scalar(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'scalar()' method`); } + } + index++; } + } - private transformUnion( - transformer: Transformer, - def: UnionTypeDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.union)) { - const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.union(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'union()' method`) - } - } - index++; + private transformUnion( + transformer: Transformer, + def: UnionTypeDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.union)) { + const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.union(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'union()' method`); } + } + index++; } + } - private transformEnum( - transformer: Transformer, - def: EnumTypeDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.enum)) { - const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.enum(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'enum()' method`) - } - } - index++; - } - for (const value of def.values) { - this.transformEnumValue(transformer, def, value, validDirectiveNameMap, context) + private transformEnum( + transformer: Transformer, + def: EnumTypeDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.enum)) { + const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.enum(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'enum()' method`); } + } + index++; + } + for (const value of def.values) { + this.transformEnumValue(transformer, def, value, validDirectiveNameMap, context); } + } - private transformEnumValue( - transformer: Transformer, - enm: EnumTypeDefinitionNode, - def: EnumValueDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchEnumValueDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.enumValue)) { - const transformKey = makeSeenTransformationKey(dir, enm, def, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.enumValue(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'enumValue()' method`) - } - } - index++; + private transformEnumValue( + transformer: Transformer, + enm: EnumTypeDefinitionNode, + def: EnumValueDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchEnumValueDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.enumValue)) { + const transformKey = makeSeenTransformationKey(dir, enm, def, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.enumValue(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'enumValue()' method`); } + } + index++; } + } - private transformInputObject( - transformer: Transformer, - def: InputObjectTypeDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.input)) { - const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.input(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'input()' method`) - } - } - index++; - } - for (const field of def.fields) { - this.transformInputField(transformer, def, field, validDirectiveNameMap, context) + private transformInputObject( + transformer: Transformer, + def: InputObjectTypeDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.input)) { + const transformKey = makeSeenTransformationKey(dir, def, undefined, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.input(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'input()' method`); } + } + index++; + } + for (const field of def.fields) { + this.transformInputField(transformer, def, field, validDirectiveNameMap, context); } + } - private transformInputField( - transformer: Transformer, - input: InputObjectTypeDefinitionNode, - def: InputValueDefinitionNode, - validDirectiveNameMap: { [k: string]: boolean }, - context: TransformerContext - ) { - let index = 0; - for (const dir of def.directives) { - if (!validDirectiveNameMap[dir.name.value]) { - throw new UnknownDirectiveError( - `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` - ) - } - if (matchInputFieldDirective(transformer.directive, dir, def)) { - if (isFunction(transformer.inputValue)) { - const transformKey = makeSeenTransformationKey(dir, input, def, undefined, index) - if (!this.seenTransformations[transformKey]) { - transformer.inputValue(def, dir, context) - this.seenTransformations[transformKey] = true - } - } else { - throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'inputValue()' method`) - } - } - index++; + private transformInputField( + transformer: Transformer, + input: InputObjectTypeDefinitionNode, + def: InputValueDefinitionNode, + validDirectiveNameMap: { [k: string]: boolean }, + context: TransformerContext + ) { + let index = 0; + for (const dir of def.directives) { + if (!validDirectiveNameMap[dir.name.value]) { + throw new UnknownDirectiveError( + `Unknown directive '${dir.name.value}'. Either remove the directive from the schema or add a transformer to handle it.` + ); + } + if (matchInputFieldDirective(transformer.directive, dir, def)) { + if (isFunction(transformer.inputValue)) { + const transformKey = makeSeenTransformationKey(dir, input, def, undefined, index); + if (!this.seenTransformations[transformKey]) { + transformer.inputValue(def, dir, context); + this.seenTransformations[transformKey] = true; + } + } else { + throw new InvalidTransformerError(`The transformer '${transformer.name}' must implement the 'inputValue()' method`); } + } + index++; } + } } diff --git a/packages/graphql-transformer-core/src/ITransformer.ts b/packages/graphql-transformer-core/src/ITransformer.ts index f9ad609633..6c7aada3b4 100644 --- a/packages/graphql-transformer-core/src/ITransformer.ts +++ b/packages/graphql-transformer-core/src/ITransformer.ts @@ -1,102 +1,102 @@ import { - DirectiveNode, - ObjectTypeDefinitionNode, - InterfaceTypeDefinitionNode, - FieldDefinitionNode, - UnionTypeDefinitionNode, - EnumTypeDefinitionNode, - ScalarTypeDefinitionNode, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - EnumValueDefinitionNode, - DirectiveDefinitionNode, - TypeDefinitionNode -} from 'graphql' -import TransformerContext from './TransformerContext' + DirectiveNode, + ObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + FieldDefinitionNode, + UnionTypeDefinitionNode, + EnumTypeDefinitionNode, + ScalarTypeDefinitionNode, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + EnumValueDefinitionNode, + DirectiveDefinitionNode, + TypeDefinitionNode, +} from 'graphql'; +import TransformerContext from './TransformerContext'; export default interface ITransformer { - - name: string - - directive: DirectiveDefinitionNode - - typeDefinitions: TypeDefinitionNode[] - - /** - * An initializer that is called once at the beginning of a transformation. - * Initializers are called in the order they are declared. - */ - before?: (acc: TransformerContext) => void - - /** - * A finalizer that is called once after a transformation. - * Finalizers are called in reverse order as they are declared. - */ - after?: (acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on objects type definitions. This includes type - * extensions. - */ - object?: (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on objects type definitions. This includes type - * extensions. - */ - interface?: (definition: InterfaceTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on object for field definitions. - */ - field?: ( - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - definition: FieldDefinitionNode, - directive: DirectiveNode, - acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on object or input argument definitions. - */ - argument?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on union definitions. - */ - union?: (definition: UnionTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on enum definitions. - */ - enum?: (definition: EnumTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on enum value definitions. - */ - enumValue?: (definition: EnumValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on scalar definitions. - */ - scalar?: (definition: ScalarTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on input definitions. - */ - input?: (definition: InputObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on input value definitions. - */ - inputValue?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void + name: string; + + directive: DirectiveDefinitionNode; + + typeDefinitions: TypeDefinitionNode[]; + + /** + * An initializer that is called once at the beginning of a transformation. + * Initializers are called in the order they are declared. + */ + before?: (acc: TransformerContext) => void; + + /** + * A finalizer that is called once after a transformation. + * Finalizers are called in reverse order as they are declared. + */ + after?: (acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on objects type definitions. This includes type + * extensions. + */ + object?: (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on objects type definitions. This includes type + * extensions. + */ + interface?: (definition: InterfaceTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on object for field definitions. + */ + field?: ( + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + definition: FieldDefinitionNode, + directive: DirectiveNode, + acc: TransformerContext + ) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on object or input argument definitions. + */ + argument?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on union definitions. + */ + union?: (definition: UnionTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on enum definitions. + */ + enum?: (definition: EnumTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on enum value definitions. + */ + enumValue?: (definition: EnumValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on scalar definitions. + */ + scalar?: (definition: ScalarTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on input definitions. + */ + input?: (definition: InputObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on input value definitions. + */ + inputValue?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; } diff --git a/packages/graphql-transformer-core/src/TransformFormatter.ts b/packages/graphql-transformer-core/src/TransformFormatter.ts index b2e7b50a15..7d4a738419 100644 --- a/packages/graphql-transformer-core/src/TransformFormatter.ts +++ b/packages/graphql-transformer-core/src/TransformFormatter.ts @@ -1,202 +1,192 @@ -import TransformerContext from "./TransformerContext"; +import TransformerContext from './TransformerContext'; import { StringParameter } from 'cloudform-types'; -import Resource from "cloudform-types/types/resource"; -import { Fn } from "cloudform-types"; -import { - makeOperationType, - makeSchema -} from 'graphql-transformer-common'; -import { ObjectTypeDefinitionNode, print } from "graphql"; -import { stripDirectives } from "./stripDirectives"; -import { SchemaResourceUtil } from "./util/SchemaResourceUtil"; -import splitStack, { StackRules } from './util/splitStack' +import Resource from 'cloudform-types/types/resource'; +import { Fn } from 'cloudform-types'; +import { makeOperationType, makeSchema } from 'graphql-transformer-common'; +import { ObjectTypeDefinitionNode, print } from 'graphql'; +import { stripDirectives } from './stripDirectives'; +import { SchemaResourceUtil } from './util/SchemaResourceUtil'; +import splitStack, { StackRules } from './util/splitStack'; import { DeploymentResources, ResolversFunctionsAndSchema, ResolverMap } from './DeploymentResources'; -import { ResourceConstants } from "graphql-transformer-common"; +import { ResourceConstants } from 'graphql-transformer-common'; export class TransformFormatter { + private schemaResourceUtil = new SchemaResourceUtil(); - private schemaResourceUtil = new SchemaResourceUtil() - - /** - * Formats the ctx into a set of deployment resources. - * - * At this point, all resources that were created by scanning/reading - * GraphQL schema and cloudformation template files have been collected into - * a singular ctx.template object. Doing this allows the CLI to perform - * sophisticated mapping, de-duplication, stack references with correct - * import/export values, and other nice cleanup routines. Once this is - * complete, the singular object can be split into the necessary stacks - * (splitStack) for each GraphQL resource. - * - * @param ctx the transformer context. - * Returns all the deployment resources for the transformation. - */ - public format(ctx: TransformerContext): DeploymentResources { - ctx.mergeConditions(this.schemaResourceUtil.makeEnvironmentConditions()) - const resolversFunctionsAndSchema = this.collectResolversFunctionsAndSchema(ctx); - const defaultDependencies = [ResourceConstants.RESOURCES.GraphQLSchemaLogicalID]; - if (ctx.getResource(ResourceConstants.RESOURCES.NoneDataSource)) { - defaultDependencies.push(ResourceConstants.RESOURCES.NoneDataSource); - } - const nestedStacks = splitStack({ - stack: ctx.template, - stackRules: ctx.getStackMapping(), - defaultParameterValues: { - [ResourceConstants.PARAMETERS.AppSyncApiId]: Fn.GetAtt( - ResourceConstants.RESOURCES.GraphQLAPILogicalID, - "ApiId" - ) - }, - defaultParameterDefinitions: { - [ResourceConstants.PARAMETERS.AppSyncApiId]: new StringParameter({ - Description: `The id of the AppSync API associated with this project.`, - }) - }, - deployment: { - deploymentBucketParameterName: ResourceConstants.PARAMETERS.S3DeploymentBucket, - deploymentKeyParameterName: ResourceConstants.PARAMETERS.S3DeploymentRootKey - }, - importExportPrefix: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), - defaultDependencies - }) - return { - ...nestedStacks, - ...resolversFunctionsAndSchema - }; + /** + * Formats the ctx into a set of deployment resources. + * + * At this point, all resources that were created by scanning/reading + * GraphQL schema and cloudformation template files have been collected into + * a singular ctx.template object. Doing this allows the CLI to perform + * sophisticated mapping, de-duplication, stack references with correct + * import/export values, and other nice cleanup routines. Once this is + * complete, the singular object can be split into the necessary stacks + * (splitStack) for each GraphQL resource. + * + * @param ctx the transformer context. + * Returns all the deployment resources for the transformation. + */ + public format(ctx: TransformerContext): DeploymentResources { + ctx.mergeConditions(this.schemaResourceUtil.makeEnvironmentConditions()); + const resolversFunctionsAndSchema = this.collectResolversFunctionsAndSchema(ctx); + const defaultDependencies = [ResourceConstants.RESOURCES.GraphQLSchemaLogicalID]; + if (ctx.getResource(ResourceConstants.RESOURCES.NoneDataSource)) { + defaultDependencies.push(ResourceConstants.RESOURCES.NoneDataSource); } + const nestedStacks = splitStack({ + stack: ctx.template, + stackRules: ctx.getStackMapping(), + defaultParameterValues: { + [ResourceConstants.PARAMETERS.AppSyncApiId]: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + }, + defaultParameterDefinitions: { + [ResourceConstants.PARAMETERS.AppSyncApiId]: new StringParameter({ + Description: `The id of the AppSync API associated with this project.`, + }), + }, + deployment: { + deploymentBucketParameterName: ResourceConstants.PARAMETERS.S3DeploymentBucket, + deploymentKeyParameterName: ResourceConstants.PARAMETERS.S3DeploymentRootKey, + }, + importExportPrefix: Fn.Ref(ResourceConstants.PARAMETERS.AppSyncApiId), + defaultDependencies, + }); + return { + ...nestedStacks, + ...resolversFunctionsAndSchema, + }; + } - /** - * Schema helper to pull resources from the context and output the final schema resource. - */ - private buildSchema(ctx: TransformerContext): string { - const mutationNode: ObjectTypeDefinitionNode | undefined = ctx.getMutation() - const queryNode: ObjectTypeDefinitionNode | undefined = ctx.getQuery() - const subscriptionNode: ObjectTypeDefinitionNode | undefined = ctx.getSubscription() - let includeMutation = true - let includeQuery = true - let includeSubscription = true - if (!mutationNode || mutationNode.fields.length === 0) { - delete ctx.nodeMap.Mutation - includeMutation = false - } - if (!queryNode || queryNode.fields.length === 0) { - delete ctx.nodeMap.Query - includeQuery = false - } - if (!subscriptionNode || subscriptionNode.fields.length === 0) { - delete ctx.nodeMap.Subscription - includeSubscription = false - } - const ops = [] - if (includeQuery) { - ops.push(makeOperationType('query', queryNode.name.value)) - } - if (includeMutation) { - ops.push(makeOperationType('mutation', mutationNode.name.value)) - } - if (includeSubscription) { - ops.push(makeOperationType('subscription', subscriptionNode.name.value)) - } - const schema = makeSchema(ops) - ctx.putSchema(schema) - const astSansDirectives = stripDirectives({ - kind: 'Document', - definitions: Object.keys(ctx.nodeMap).map((k: string) => ctx.getType(k)) - }, ['aws_subscribe', 'aws_auth', 'aws_api_key', 'aws_iam', 'aws_oidc', 'aws_cognito_user_pools', 'deprecated']) - const SDL = print(astSansDirectives) - return SDL; + /** + * Schema helper to pull resources from the context and output the final schema resource. + */ + private buildSchema(ctx: TransformerContext): string { + const mutationNode: ObjectTypeDefinitionNode | undefined = ctx.getMutation(); + const queryNode: ObjectTypeDefinitionNode | undefined = ctx.getQuery(); + const subscriptionNode: ObjectTypeDefinitionNode | undefined = ctx.getSubscription(); + let includeMutation = true; + let includeQuery = true; + let includeSubscription = true; + if (!mutationNode || mutationNode.fields.length === 0) { + delete ctx.nodeMap.Mutation; + includeMutation = false; } - - /** - * Builds the schema and creates the schema record to pull from S3. - * Returns the schema SDL text as a string. - */ - private buildAndSetSchema(ctx: TransformerContext): string { - const SDL = this.buildSchema(ctx) - const schemaResource = this.schemaResourceUtil.makeAppSyncSchema() - ctx.setResource(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID, schemaResource) - return SDL + if (!queryNode || queryNode.fields.length === 0) { + delete ctx.nodeMap.Query; + includeQuery = false; + } + if (!subscriptionNode || subscriptionNode.fields.length === 0) { + delete ctx.nodeMap.Subscription; + includeSubscription = false; + } + const ops = []; + if (includeQuery) { + ops.push(makeOperationType('query', queryNode.name.value)); } + if (includeMutation) { + ops.push(makeOperationType('mutation', mutationNode.name.value)); + } + if (includeSubscription) { + ops.push(makeOperationType('subscription', subscriptionNode.name.value)); + } + const schema = makeSchema(ops); + ctx.putSchema(schema); + const astSansDirectives = stripDirectives( + { + kind: 'Document', + definitions: Object.keys(ctx.nodeMap).map((k: string) => ctx.getType(k)), + }, + ['aws_subscribe', 'aws_auth', 'aws_api_key', 'aws_iam', 'aws_oidc', 'aws_cognito_user_pools', 'deprecated'] + ); + const SDL = print(astSansDirectives); + return SDL; + } - private collectResolversFunctionsAndSchema(ctx: TransformerContext): ResolversFunctionsAndSchema { - const resolverParams = this.schemaResourceUtil.makeResolverS3RootParams() - ctx.mergeParameters(resolverParams.Parameters); - const templateResources: { [key: string]: Resource } = ctx.template.Resources - let resolverMap = {} - let pipelineFunctionMap = {} - let functionsMap = {} - for (const resourceName of Object.keys(templateResources)) { - const resource: Resource = templateResources[resourceName] - if (resource.Type === 'AWS::AppSync::Resolver') { - const resourceResolverMap = this.replaceResolverRecord(resourceName, ctx) - resolverMap = { ...resolverMap, ...resourceResolverMap } - } else if (resource.Type === 'AWS::AppSync::FunctionConfiguration') { - const functionConfigMap = this.replaceFunctionConfigurationRecord(resourceName, ctx) - pipelineFunctionMap = { ...pipelineFunctionMap, ...functionConfigMap } - } else if (resource.Type === 'AWS::Lambda::Function') { - // TODO: We only use the one function for now. Generalize this. - functionsMap = { - ...functionsMap, - [`${resourceName}.zip`]: ctx.metadata.get('ElasticsearchPathToStreamingLambda') - } - } - } - const schema = this.buildAndSetSchema(ctx); - return { - resolvers: resolverMap, - functions: functionsMap, - pipelineFunctions: pipelineFunctionMap, - schema - } + /** + * Builds the schema and creates the schema record to pull from S3. + * Returns the schema SDL text as a string. + */ + private buildAndSetSchema(ctx: TransformerContext): string { + const SDL = this.buildSchema(ctx); + const schemaResource = this.schemaResourceUtil.makeAppSyncSchema(); + ctx.setResource(ResourceConstants.RESOURCES.GraphQLSchemaLogicalID, schemaResource); + return SDL; + } + + private collectResolversFunctionsAndSchema(ctx: TransformerContext): ResolversFunctionsAndSchema { + const resolverParams = this.schemaResourceUtil.makeResolverS3RootParams(); + ctx.mergeParameters(resolverParams.Parameters); + const templateResources: { [key: string]: Resource } = ctx.template.Resources; + let resolverMap = {}; + let pipelineFunctionMap = {}; + let functionsMap = {}; + for (const resourceName of Object.keys(templateResources)) { + const resource: Resource = templateResources[resourceName]; + if (resource.Type === 'AWS::AppSync::Resolver') { + const resourceResolverMap = this.replaceResolverRecord(resourceName, ctx); + resolverMap = { ...resolverMap, ...resourceResolverMap }; + } else if (resource.Type === 'AWS::AppSync::FunctionConfiguration') { + const functionConfigMap = this.replaceFunctionConfigurationRecord(resourceName, ctx); + pipelineFunctionMap = { ...pipelineFunctionMap, ...functionConfigMap }; + } else if (resource.Type === 'AWS::Lambda::Function') { + // TODO: We only use the one function for now. Generalize this. + functionsMap = { + ...functionsMap, + [`${resourceName}.zip`]: ctx.metadata.get('ElasticsearchPathToStreamingLambda'), + }; + } } + const schema = this.buildAndSetSchema(ctx); + return { + resolvers: resolverMap, + functions: functionsMap, + pipelineFunctions: pipelineFunctionMap, + schema, + }; + } - private replaceResolverRecord(resourceName: string, ctx: TransformerContext): ResolverMap { - const resolverResource = ctx.template.Resources[resourceName] + private replaceResolverRecord(resourceName: string, ctx: TransformerContext): ResolverMap { + const resolverResource = ctx.template.Resources[resourceName]; - const requestMappingTemplate = resolverResource.Properties.RequestMappingTemplate - const responseMappingTemplate = resolverResource.Properties.ResponseMappingTemplate - // If the templates are not strings. aka they use CF intrinsic functions don't rewrite. - if ( - typeof requestMappingTemplate === 'string' && - typeof responseMappingTemplate === 'string' - ) { - const reqType = resolverResource.Properties.TypeName - const reqFieldName = resolverResource.Properties.FieldName - const reqFileName = `${reqType}.${reqFieldName}.req.vtl` + const requestMappingTemplate = resolverResource.Properties.RequestMappingTemplate; + const responseMappingTemplate = resolverResource.Properties.ResponseMappingTemplate; + // If the templates are not strings. aka they use CF intrinsic functions don't rewrite. + if (typeof requestMappingTemplate === 'string' && typeof responseMappingTemplate === 'string') { + const reqType = resolverResource.Properties.TypeName; + const reqFieldName = resolverResource.Properties.FieldName; + const reqFileName = `${reqType}.${reqFieldName}.req.vtl`; - const respType = resolverResource.Properties.TypeName - const respFieldName = resolverResource.Properties.FieldName - const respFileName = `${respType}.${respFieldName}.res.vtl` + const respType = resolverResource.Properties.TypeName; + const respFieldName = resolverResource.Properties.FieldName; + const respFileName = `${respType}.${respFieldName}.res.vtl`; - const updatedResolverResource = this.schemaResourceUtil.updateResolverResource(resolverResource) - ctx.setResource(resourceName, updatedResolverResource) - return { - [reqFileName]: requestMappingTemplate, - [respFileName]: responseMappingTemplate - } - } - return {} + const updatedResolverResource = this.schemaResourceUtil.updateResolverResource(resolverResource); + ctx.setResource(resourceName, updatedResolverResource); + return { + [reqFileName]: requestMappingTemplate, + [respFileName]: responseMappingTemplate, + }; } + return {}; + } - private replaceFunctionConfigurationRecord(resourceName: string, ctx: TransformerContext): ResolverMap { - const functionConfiguration = ctx.template.Resources[resourceName] + private replaceFunctionConfigurationRecord(resourceName: string, ctx: TransformerContext): ResolverMap { + const functionConfiguration = ctx.template.Resources[resourceName]; - const requestMappingTemplate = functionConfiguration.Properties.RequestMappingTemplate - const responseMappingTemplate = functionConfiguration.Properties.ResponseMappingTemplate - // If the templates are not strings. aka they use CF intrinsic functions don't rewrite. - if ( - typeof requestMappingTemplate === 'string' && - typeof responseMappingTemplate === 'string' - ) { - const reqFileName = `${functionConfiguration.Properties.Name}.req.vtl` - const respFileName = `${functionConfiguration.Properties.Name}.res.vtl` - const updatedResolverResource = this.schemaResourceUtil.updateFunctionConfigurationResource(functionConfiguration) - ctx.setResource(resourceName, updatedResolverResource) - return { - [reqFileName]: requestMappingTemplate, - [respFileName]: responseMappingTemplate - } - } - return {} + const requestMappingTemplate = functionConfiguration.Properties.RequestMappingTemplate; + const responseMappingTemplate = functionConfiguration.Properties.ResponseMappingTemplate; + // If the templates are not strings. aka they use CF intrinsic functions don't rewrite. + if (typeof requestMappingTemplate === 'string' && typeof responseMappingTemplate === 'string') { + const reqFileName = `${functionConfiguration.Properties.Name}.req.vtl`; + const respFileName = `${functionConfiguration.Properties.Name}.res.vtl`; + const updatedResolverResource = this.schemaResourceUtil.updateFunctionConfigurationResource(functionConfiguration); + ctx.setResource(resourceName, updatedResolverResource); + return { + [reqFileName]: requestMappingTemplate, + [respFileName]: responseMappingTemplate, + }; } + return {}; + } } diff --git a/packages/graphql-transformer-core/src/Transformer.ts b/packages/graphql-transformer-core/src/Transformer.ts index 2a58546f6a..04b506b240 100644 --- a/packages/graphql-transformer-core/src/Transformer.ts +++ b/packages/graphql-transformer-core/src/Transformer.ts @@ -1,24 +1,24 @@ -import TransformerContext from './TransformerContext' -import ITransformer from './ITransformer' +import TransformerContext from './TransformerContext'; +import ITransformer from './ITransformer'; import { - DirectiveDefinitionNode, - parse, - DirectiveNode, - ObjectTypeDefinitionNode, - InterfaceTypeDefinitionNode, - FieldDefinitionNode, - UnionTypeDefinitionNode, - Kind, - EnumTypeDefinitionNode, - ScalarTypeDefinitionNode, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - EnumValueDefinitionNode, - TypeDefinitionNode, - DefinitionNode, - DocumentNode -} from 'graphql' -import { InvalidTransformerError } from './errors' + DirectiveDefinitionNode, + parse, + DirectiveNode, + ObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + FieldDefinitionNode, + UnionTypeDefinitionNode, + Kind, + EnumTypeDefinitionNode, + ScalarTypeDefinitionNode, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + EnumValueDefinitionNode, + TypeDefinitionNode, + DefinitionNode, + DocumentNode, +} from 'graphql'; +import { InvalidTransformerError } from './errors'; /** * A GraphQLTransformer takes a context object, processes it, and @@ -27,111 +27,108 @@ import { InvalidTransformerError } from './errors' * for its stage of the transformation. */ export default class Transformer implements ITransformer { - - public name: string - - public directive: DirectiveDefinitionNode - - public typeDefinitions: TypeDefinitionNode[] - - /** - * Each transformer has a name. - * - * Each transformer defines a set of directives that it knows how to translate. - */ - constructor( - name: string, - document: DocumentNode | string - ) { - const doc = typeof document === 'string' ? parse(document) : document; - this.name = name - const directives = doc.definitions.filter(d => d.kind === Kind.DIRECTIVE_DEFINITION) as DirectiveDefinitionNode[] - const extraDefs = doc.definitions.filter(d => d.kind !== Kind.DIRECTIVE_DEFINITION) as TypeDefinitionNode[] - if (directives.length !== 1) { - throw new InvalidTransformerError('Transformers must specify exactly one directive definition.') - } - this.directive = directives[0] - - // Transformers can define extra shapes that can be used by the directive - // and validated. TODO: Validation. - this.typeDefinitions = extraDefs + public name: string; + + public directive: DirectiveDefinitionNode; + + public typeDefinitions: TypeDefinitionNode[]; + + /** + * Each transformer has a name. + * + * Each transformer defines a set of directives that it knows how to translate. + */ + constructor(name: string, document: DocumentNode | string) { + const doc = typeof document === 'string' ? parse(document) : document; + this.name = name; + const directives = doc.definitions.filter(d => d.kind === Kind.DIRECTIVE_DEFINITION) as DirectiveDefinitionNode[]; + const extraDefs = doc.definitions.filter(d => d.kind !== Kind.DIRECTIVE_DEFINITION) as TypeDefinitionNode[]; + if (directives.length !== 1) { + throw new InvalidTransformerError('Transformers must specify exactly one directive definition.'); } - - /** - * An initializer that is called once at the beginning of a transformation. - * Initializers are called in the order they are declared. - */ - before?: (acc: TransformerContext) => void - - /** - * A finalizer that is called once after a transformation. - * Finalizers are called in reverse order as they are declared. - */ - after?: (acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on objects type definitions. This includes type - * extensions. - */ - object?: (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on objects type definitions. This includes type - * extensions. - */ - interface?: (definition: InterfaceTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on object for field definitions. - */ - field?: ( - parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, - definition: FieldDefinitionNode, - directive: DirectiveNode, - acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on object or input argument definitions. - */ - argument?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on union definitions. - */ - union?: (definition: UnionTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on enum definitions. - */ - enum?: (definition: EnumTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on enum value definitions. - */ - enumValue?: (definition: EnumValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on scalar definitions. - */ - scalar?: (definition: ScalarTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on input definitions. - */ - input?: (definition: InputObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void - - /** - * A transformer implements a single function per location that its directive can be applied. - * This method handles transforming directives on input value definitions. - */ - inputValue?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void + this.directive = directives[0]; + + // Transformers can define extra shapes that can be used by the directive + // and validated. TODO: Validation. + this.typeDefinitions = extraDefs; + } + + /** + * An initializer that is called once at the beginning of a transformation. + * Initializers are called in the order they are declared. + */ + before?: (acc: TransformerContext) => void; + + /** + * A finalizer that is called once after a transformation. + * Finalizers are called in reverse order as they are declared. + */ + after?: (acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on objects type definitions. This includes type + * extensions. + */ + object?: (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on objects type definitions. This includes type + * extensions. + */ + interface?: (definition: InterfaceTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on object for field definitions. + */ + field?: ( + parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, + definition: FieldDefinitionNode, + directive: DirectiveNode, + acc: TransformerContext + ) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on object or input argument definitions. + */ + argument?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on union definitions. + */ + union?: (definition: UnionTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on enum definitions. + */ + enum?: (definition: EnumTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on enum value definitions. + */ + enumValue?: (definition: EnumValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on scalar definitions. + */ + scalar?: (definition: ScalarTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on input definitions. + */ + input?: (definition: InputObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; + + /** + * A transformer implements a single function per location that its directive can be applied. + * This method handles transforming directives on input value definitions. + */ + inputValue?: (definition: InputValueDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => void; } diff --git a/packages/graphql-transformer-core/src/TransformerContext.ts b/packages/graphql-transformer-core/src/TransformerContext.ts index 0750e44446..94f80a39ec 100644 --- a/packages/graphql-transformer-core/src/TransformerContext.ts +++ b/packages/graphql-transformer-core/src/TransformerContext.ts @@ -1,85 +1,88 @@ -import Template from 'cloudform-types/types/template' -import Resource from 'cloudform-types/types/resource' -import Parameter from 'cloudform-types/types/parameter' -import { Condition } from 'cloudform-types/types/dataTypes' -import Output from 'cloudform-types/types/output' +import Template from 'cloudform-types/types/template'; +import Resource from 'cloudform-types/types/resource'; +import Parameter from 'cloudform-types/types/parameter'; +import { Condition } from 'cloudform-types/types/dataTypes'; +import Output from 'cloudform-types/types/output'; import { - TypeSystemDefinitionNode, - ObjectTypeDefinitionNode, - FieldDefinitionNode, - InputObjectTypeDefinitionNode, - SchemaDefinitionNode, - ObjectTypeExtensionNode, - NamedTypeNode, - DocumentNode, - Kind, - parse, - EnumTypeDefinitionNode, - TypeDefinitionNode, - OperationTypeDefinitionNode, - InterfaceTypeDefinitionNode -} from 'graphql' -import blankTemplate from './util/blankTemplate' -import DefaultSchemaDefinition from './defaultSchema' + TypeSystemDefinitionNode, + ObjectTypeDefinitionNode, + FieldDefinitionNode, + InputObjectTypeDefinitionNode, + SchemaDefinitionNode, + ObjectTypeExtensionNode, + NamedTypeNode, + DocumentNode, + Kind, + parse, + EnumTypeDefinitionNode, + TypeDefinitionNode, + OperationTypeDefinitionNode, + InterfaceTypeDefinitionNode, +} from 'graphql'; +import blankTemplate from './util/blankTemplate'; +import DefaultSchemaDefinition from './defaultSchema'; import { - InterfaceTypeExtensionNode, UnionTypeExtensionNode, - UnionTypeDefinitionNode, EnumTypeExtensionNode, EnumValueDefinitionNode, - InputObjectTypeExtensionNode, InputValueDefinitionNode + InterfaceTypeExtensionNode, + UnionTypeExtensionNode, + UnionTypeDefinitionNode, + EnumTypeExtensionNode, + EnumValueDefinitionNode, + InputObjectTypeExtensionNode, + InputValueDefinitionNode, } from 'graphql/language/ast'; import { _Kind } from 'graphql/language/kinds'; export interface MappingParameters { + [key: string]: { [key: string]: { - [key: string]: { - [key: string]: string | number | string[]; - }; + [key: string]: string | number | string[]; }; + }; } export function blankObject(name: string): ObjectTypeDefinitionNode { - return { - kind: 'ObjectTypeDefinition', - name: { - kind: 'Name', - value: name - }, - fields: [], - directives: [], - interfaces: [] - } + return { + kind: 'ObjectTypeDefinition', + name: { + kind: 'Name', + value: name, + }, + fields: [], + directives: [], + interfaces: [], + }; } export function objectExtension(name: string, fields: FieldDefinitionNode[] = []): ObjectTypeExtensionNode { - return { - kind: Kind.OBJECT_TYPE_EXTENSION, - name: { - kind: 'Name', - value: name - }, - fields, - directives: [], - interfaces: [] - } + return { + kind: Kind.OBJECT_TYPE_EXTENSION, + name: { + kind: 'Name', + value: name, + }, + fields, + directives: [], + interfaces: [], + }; } export class TransformerContextMetadata { - - /** - * Used by transformers to pass information between one another. - */ - private metadata: { [key: string]: any } = {} - - public get(key: string): any { - return this.metadata[key]; - } - - public set(key: string, val: any): void { - return this.metadata[key] = val; - } - - public has(key: string) { - return Boolean(this.metadata[key] !== undefined) - } + /** + * Used by transformers to pass information between one another. + */ + private metadata: { [key: string]: any } = {}; + + public get(key: string): any { + return this.metadata[key]; + } + + public set(key: string, val: any): void { + return (this.metadata[key] = val); + } + + public has(key: string) { + return Boolean(this.metadata[key] !== undefined); + } } /** @@ -96,593 +99,592 @@ export type StackMapping = Map; * types, and parameters necessary to support an AppSync transform. */ export default class TransformerContext { - - public template: Template = blankTemplate() - - public nodeMap: { [name: string]: TypeSystemDefinitionNode } = {} - - public inputDocument: DocumentNode - - public metadata: TransformerContextMetadata = new TransformerContextMetadata() - - private stackMapping: StackMapping = new Map(); - - constructor(inputSDL: string) { - const doc: DocumentNode = parse(inputSDL) - for (const def of doc.definitions) { - if (def.kind === 'OperationDefinition' || def.kind === 'FragmentDefinition') { - throw new Error(`Found a ${def.kind}. Transformers accept only documents consisting of TypeSystemDefinitions.`) - } - } - this.inputDocument = doc - this.fillNodeMapWithInput(); - } - - /** - * Before running the transformers, first flush the input document - * into the node map. If a schema definition node then leave everything - * as is so customers can explicitly turn off mutations & subscriptions. - * If a SDN is not provided then we add the default schema and empty - * Query, Mutation, and Subscription - */ - private fillNodeMapWithInput(): void { - const extensionNodes = []; - for (const inputDef of this.inputDocument.definitions) { - switch (inputDef.kind) { - case Kind.OBJECT_TYPE_DEFINITION: - case Kind.SCALAR_TYPE_DEFINITION: - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.INPUT_OBJECT_TYPE_DEFINITION: - case Kind.ENUM_TYPE_DEFINITION: - case Kind.UNION_TYPE_DEFINITION: - const typeDef = inputDef as TypeDefinitionNode - if (!this.getType(typeDef.name.value)) { - this.addType(typeDef) - } - break; - case Kind.SCHEMA_DEFINITION: - if (!this.getSchema()) { - const typeDef = inputDef as SchemaDefinitionNode - this.putSchema(typeDef); - } - break; - case Kind.OBJECT_TYPE_EXTENSION: - case Kind.ENUM_TYPE_EXTENSION: - case Kind.UNION_TYPE_EXTENSION: - case Kind.INTERFACE_TYPE_EXTENSION: - case Kind.INPUT_OBJECT_TYPE_EXTENSION: - extensionNodes.push(inputDef); - break; - case Kind.SCALAR_TYPE_EXTENSION: - default: - /* pass any others */ - } - } - // We add the extension nodes last so that the order of input documents does not matter. - // At this point, all input documents have been processed so the base types will be present. - for (const ext of extensionNodes) { - switch (ext.kind) { - case Kind.OBJECT_TYPE_EXTENSION: - this.addObjectExtension(ext); - break; - case Kind.INTERFACE_TYPE_EXTENSION: - this.addInterfaceExtension(ext); - break; - case Kind.UNION_TYPE_EXTENSION: - this.addUnionExtension(ext); - break; - case Kind.ENUM_TYPE_EXTENSION: - this.addEnumExtension(ext); - break; - case Kind.INPUT_OBJECT_TYPE_EXTENSION: - this.addInputExtension(ext); - break; - case Kind.SCALAR_TYPE_EXTENSION: - default: - continue; - } - } - // If no schema definition is provided then fill with the default one. - if (!this.getSchema()) { - this.putSchema(DefaultSchemaDefinition); - } - } - - /** - * Scans through the context nodeMap and returns all type definition nodes - * that are of the given kind. - * @param kind Kind value of type definition nodes expected. - */ - public getTypeDefinitionsOfKind(kind: string) { - const typeDefs: TypeDefinitionNode[] = []; - for (const key of Object.keys(this.nodeMap)) { - const definition = this.nodeMap[key]; - if (definition.kind === kind) { - typeDefs.push(definition as TypeDefinitionNode); - } - } - return typeDefs - } - - public mergeResources(resources: { [key: string]: Resource }) { - for (const resourceId of Object.keys(resources)) { - if (this.template.Resources[resourceId]) { - throw new Error(`Conflicting CloudFormation resource logical id: ${resourceId}`) - } - } - this.template.Resources = { ...this.template.Resources, ...resources } - } - - public mergeParameters(params: { [key: string]: Parameter }) { - for (const parameterName of Object.keys(params)) { - if (this.template.Parameters[parameterName]) { - throw new Error(`Conflicting CloudFormation parameter name: ${parameterName}`) - } - } - this.template.Parameters = { ...this.template.Parameters, ...params } - } - - public mergeConditions(conditions: { [key: string]: Condition }) { - if (!this.template.Conditions) { - this.template.Conditions = {} - } - for (const conditionName of Object.keys(conditions)) { - if (this.template.Conditions[conditionName]) { - throw new Error(`Conflicting CloudFormation condition name: ${conditionName}`) - } - } - this.template.Conditions = { ...this.template.Conditions, ...conditions } - } - - public getResource(resource: string): Resource { - return this.template.Resources[resource]; - } - - public setResource(key: string, resource: Resource): void { - this.template.Resources[key] = resource - } - - public setOutput(key: string, output: Output): void { - this.template.Outputs[key] = output; - } - - public getOutput(key: string): Output { - return this.template.Outputs[key] - } - - public mergeOutputs(outputs: { [key: string]: Output }) { - for (const outputName of Object.keys(outputs)) { - if (this.template.Parameters[outputName]) { - throw new Error(`Conflicting CloudFormation parameter name: ${outputName}`) - } - } - this.template.Outputs = { ...this.template.Outputs, ...outputs } - } - - public mergeMappings(mapping: MappingParameters ) { - for (const mappingName of Object.keys(mapping)) { - if (this.template.Mappings[mappingName]) { - throw new Error(`Conflicting CloudFormation mapping name: ${mappingName}`) - } - } - this.template.Mappings = {...this.template.Mappings, ...mapping } - } - - /** - * Add an object type definition node to the context. If the type already - * exists an error will be thrown. - * @param obj The object type definition node to add. - */ - public putSchema(obj: SchemaDefinitionNode) { - this.nodeMap.__schema = obj - } - - /** - * Returns the schema definition record. If the user provides a schema - * definition as part of the input document, that node is returned. - * Otherwise a blank schema definition with default operation types - * is returned. - */ - public getSchema(): SchemaDefinitionNode { - return this.nodeMap.__schema as SchemaDefinitionNode; - } - - public getQueryTypeName(): string | undefined { - const schemaNode = this.getSchema(); - const queryTypeName = schemaNode.operationTypes.find((op: OperationTypeDefinitionNode) => op.operation === 'query'); - if (queryTypeName && queryTypeName.type && queryTypeName.type.name) { - return queryTypeName.type.name.value; - } - } - - public getQuery(): ObjectTypeDefinitionNode | undefined { - const queryTypeName = this.getQueryTypeName(); - if (queryTypeName) { - return this.nodeMap[queryTypeName] as ObjectTypeDefinitionNode | undefined; - } - } - - public getMutationTypeName(): string | undefined { - const schemaNode = this.getSchema(); - const mutationTypeName = schemaNode.operationTypes.find((op: OperationTypeDefinitionNode) => op.operation === 'mutation'); - if (mutationTypeName && mutationTypeName.type && mutationTypeName.type.name) { - return mutationTypeName.type.name.value; - } - } - - public getMutation(): ObjectTypeDefinitionNode | undefined { - const mutationTypeName = this.getMutationTypeName(); - if (mutationTypeName) { - return this.nodeMap[mutationTypeName] as ObjectTypeDefinitionNode | undefined; - } - } - - public getSubscriptionTypeName(): string | undefined { - const schemaNode = this.getSchema(); - const subscriptionTypeName = schemaNode.operationTypes.find((op: OperationTypeDefinitionNode) => op.operation === 'subscription'); - if (subscriptionTypeName && subscriptionTypeName.type && subscriptionTypeName.type.name) { - return subscriptionTypeName.type.name.value; - } - } - - public getSubscription(): ObjectTypeDefinitionNode | undefined { - const subscriptionTypeName = this.getSubscriptionTypeName(); - if (subscriptionTypeName) { - return this.nodeMap[subscriptionTypeName] as ObjectTypeDefinitionNode | undefined; - } - } - - /** - * Add a generic type. - * @param obj The type to add - */ - public addType(obj: TypeDefinitionNode) { - if (this.nodeMap[obj.name.value]) { - throw new Error(`Conflicting type '${obj.name.value}' found.`) - } - this.nodeMap[obj.name.value] = obj - } - - public putType(obj: TypeDefinitionNode) { - this.nodeMap[obj.name.value] = obj - } - - public getType(name: string): TypeSystemDefinitionNode | undefined { - return this.nodeMap[name] - } - - /** - * Add an object type definition node to the context. If the type already - * exists an error will be thrown. - * @param obj The object type definition node to add. - */ - public addObject(obj: ObjectTypeDefinitionNode) { - if (this.nodeMap[obj.name.value]) { - throw new Error(`Conflicting type '${obj.name.value}' found.`) - } - this.nodeMap[obj.name.value] = obj - } - - public getObject(name: string): ObjectTypeDefinitionNode | undefined { - if (this.nodeMap[name]) { - const node = this.nodeMap[name] - if (node.kind === Kind.OBJECT_TYPE_DEFINITION) { - return node as ObjectTypeDefinitionNode; - } - } - } - - /** - * Extends the context query object with additional fields. - * If the customer uses a name other than 'Query' this will proxy to the - * correct type. - * @param fields The fields to add the query type. - */ - public addQueryFields(fields: FieldDefinitionNode[]) { - const queryTypeName = this.getQueryTypeName(); - if (queryTypeName) { - if (!this.getType(queryTypeName)) { - this.addType(blankObject(queryTypeName)) - } - let queryType = objectExtension(queryTypeName, fields) - this.addObjectExtension(queryType); - } - } - - /** - * Extends the context mutation object with additional fields. - * If the customer uses a name other than 'Mutation' this will proxy to the - * correct type. - * @param fields The fields to add the mutation type. - */ - public addMutationFields(fields: FieldDefinitionNode[]) { - const mutationTypeName = this.getMutationTypeName(); - if (mutationTypeName) { - if (!this.getType(mutationTypeName)) { - this.addType(blankObject(mutationTypeName)) - } - let mutationType = objectExtension(mutationTypeName, fields) - this.addObjectExtension(mutationType); - } - } - - /** - * Extends the context subscription object with additional fields. - * If the customer uses a name other than 'Subscription' this will proxy to the - * correct type. - * @param fields The fields to add the subscription type. - */ - public addSubscriptionFields(fields: FieldDefinitionNode[]) { - const subscriptionTypeName = this.getSubscriptionTypeName(); - if (subscriptionTypeName) { - if (!this.getType(subscriptionTypeName)) { - this.addType(blankObject(subscriptionTypeName)) - } - let subscriptionType = objectExtension(subscriptionTypeName, fields) - this.addObjectExtension(subscriptionType); - } - } - - /** - * Add an object type extension definition node to the context. If a type with this - * name does not already exist, an exception is thrown. - * @param obj The object type definition node to add. - */ - public addObjectExtension(obj: ObjectTypeExtensionNode) { - if (!this.nodeMap[obj.name.value]) { - throw new Error(`Cannot extend non-existant type '${obj.name.value}'.`) - } - // AppSync does not yet understand type extensions so fold the types in. - const oldNode = this.getObject(obj.name.value) - const newDirs = [] - const oldDirs = oldNode.directives || [] - - // Filter out duplicate directives, do not add them - if (obj.directives) { - for (const newDir of obj.directives) { - if (Boolean(oldDirs.find((d) => d.name.value === newDir.name.value)) === false) { - newDirs.push(newDir); - } - } - } - - const mergedDirs = [...oldDirs, ...newDirs] - - // An extension cannot redeclare fields. - const oldFields = oldNode.fields || [] - const oldFieldMap = oldFields.reduce( - (acc: any, field: FieldDefinitionNode) => ({ - ...acc, - [field.name.value]: field - }), - {} - ) - const newFields = obj.fields || [] - const mergedFields = [...oldFields] - for (const newField of newFields) { - if (oldFieldMap[newField.name.value]) { - throw new Error(`Object type extension '${obj.name.value}' cannot redeclare field ${newField.name.value}`) - } - mergedFields.push(newField) - } - - // An extension cannot redeclare interfaces - const oldInterfaces = oldNode.interfaces || [] - const oldInterfaceMap = oldInterfaces.reduce( - (acc: any, field: NamedTypeNode) => ({ - ...acc, - [field.name.value]: field - }), - {} - ) - const newInterfaces = obj.interfaces || [] - const mergedInterfaces = [...oldInterfaces] - for (const newInterface of newInterfaces) { - if (oldInterfaceMap[newInterface.name.value]) { - throw new Error(`Object type extension '${obj.name.value}' cannot redeclare interface ${newInterface.name.value}`) - } - mergedInterfaces.push(newInterface) - } - this.nodeMap[oldNode.name.value] = { - ...oldNode, - interfaces: mergedInterfaces, - directives: mergedDirs, - fields: mergedFields - } - } - - /** - * Add an input object type extension definition node to the context. If a type with this - * name does not already exist, an exception is thrown. - * @param obj The input object type definition node to add. - */ - public addInputExtension(obj: InputObjectTypeExtensionNode) { - if (!this.nodeMap[obj.name.value]) { - throw new Error(`Cannot extend non-existant input '${obj.name.value}'.`) - } - // AppSync does not yet understand type extensions so fold the types in. - const oldNode = this.getType(obj.name.value) as InputObjectTypeDefinitionNode - const newDirs = obj.directives || [] - const oldDirs = oldNode.directives || [] - const mergedDirs = [...oldDirs, ...newDirs] - - // An extension cannot redeclare fields. - const oldFields = oldNode.fields || [] - const oldFieldMap = oldFields.reduce( - (acc: any, field: InputValueDefinitionNode) => ({ - ...acc, - [field.name.value]: field - }), - {} - ) - const newFields = obj.fields || [] - const mergedFields = [...oldFields] - for (const newField of newFields) { - if (oldFieldMap[newField.name.value]) { - throw new Error(`Input object type extension '${obj.name.value}' cannot redeclare field ${newField.name.value}`) - } - mergedFields.push(newField) - } - - this.nodeMap[oldNode.name.value] = { - ...oldNode, - directives: mergedDirs, - fields: mergedFields - } - } - - /** - * Add an interface extension definition node to the context. If a type with this - * name does not already exist, an exception is thrown. - * @param obj The interface type definition node to add. - */ - public addInterfaceExtension(obj: InterfaceTypeExtensionNode) { - if (!this.nodeMap[obj.name.value]) { - throw new Error(`Cannot extend non-existant interface '${obj.name.value}'.`) - } - // AppSync does not yet understand type extensions so fold the types in. - const oldNode = this.getType(obj.name.value) as InterfaceTypeDefinitionNode; - const newDirs = obj.directives || [] - const oldDirs = oldNode.directives || [] - const mergedDirs = [...oldDirs, ...newDirs] - - // An extension cannot redeclare fields. - const oldFields = oldNode.fields || [] - const oldFieldMap = oldFields.reduce( - (acc: any, field: FieldDefinitionNode) => ({ - ...acc, - [field.name.value]: field - }), - {} - ) - const newFields = obj.fields || [] - const mergedFields = [...oldFields] - for (const newField of newFields) { - if (oldFieldMap[newField.name.value]) { - throw new Error(`Interface type extension '${obj.name.value}' cannot redeclare field ${newField.name.value}`) - } - mergedFields.push(newField) - } - - this.nodeMap[oldNode.name.value] = { - ...oldNode, - directives: mergedDirs, - fields: mergedFields - } - } - - /** - * Add an union extension definition node to the context. If a type with this - * name does not already exist, an exception is thrown. - * @param obj The union type definition node to add. - */ - public addUnionExtension(obj: UnionTypeExtensionNode) { - if (!this.nodeMap[obj.name.value]) { - throw new Error(`Cannot extend non-existant union '${obj.name.value}'.`) - } - // AppSync does not yet understand type extensions so fold the types in. - const oldNode = this.getType(obj.name.value) as UnionTypeDefinitionNode; - const newDirs = obj.directives || [] - const oldDirs = oldNode.directives || [] - const mergedDirs = [...oldDirs, ...newDirs] - - // An extension cannot redeclare possible values - const oldTypes = oldNode.types || [] - const oldTypeMap = oldTypes.reduce( - (acc: any, type: NamedTypeNode) => ({ - ...acc, - [type.name.value]: true - }), - {} - ) - const newTypes = obj.types || [] - const mergedFields = [...oldTypes] - for (const newType of newTypes) { - if (oldTypeMap[newType.name.value]) { - throw new Error(`Union type extension '${obj.name.value}' cannot redeclare type ${newType.name.value}`) - } - mergedFields.push(newType) - } - - this.nodeMap[oldNode.name.value] = { - ...oldNode, - directives: mergedDirs, - types: mergedFields - } - } - - /** - * Add an enum extension definition node to the context. If a type with this - * name does not already exist, an exception is thrown. - * @param obj The enum type definition node to add. - */ - public addEnumExtension(obj: EnumTypeExtensionNode) { - if (!this.nodeMap[obj.name.value]) { - throw new Error(`Cannot extend non-existant enum '${obj.name.value}'.`) - } - // AppSync does not yet understand type extensions so fold the types in. - const oldNode = this.getType(obj.name.value) as EnumTypeDefinitionNode; - const newDirs = obj.directives || [] - const oldDirs = oldNode.directives || [] - const mergedDirs = [...oldDirs, ...newDirs] - - // An extension cannot redeclare possible values - const oldValues = oldNode.values || [] - const oldValuesMap = oldValues.reduce( - (acc: any, type: EnumValueDefinitionNode) => ({ - ...acc, - [type.name.value]: true - }), - {} - ) - const newValues = obj.values || [] - const mergedValues = [...oldValues] - for (const newValue of newValues) { - if (oldValuesMap[newValue.name.value]) { - throw new Error(`Enum type extension '${obj.name.value}' cannot redeclare value ${newValue.name.value}`) - } - mergedValues.push(newValue) - } - - this.nodeMap[oldNode.name.value] = { - ...oldNode, - directives: mergedDirs, - values: mergedValues - } - } - - /** - * Add an input type definition node to the context. - * @param inp The input type definition node to add. - */ - public addInput(inp: InputObjectTypeDefinitionNode) { - if (this.nodeMap[inp.name.value]) { - throw new Error(`Conflicting input type '${inp.name.value}' found.`) - } - this.nodeMap[inp.name.value] = inp - } - - /** - * Add an enum type definition node to the context. - * @param en The enum type definition node to add. - */ - public addEnum(en: EnumTypeDefinitionNode) { - if (this.nodeMap[en.name.value]) { - throw new Error(`Conflicting enum type '${en.name.value}' found.`) - } - this.nodeMap[en.name.value] = en - } - - /** - * Add an item to the stack mapping. - * @param stackName The destination stack name. - * @param resource The resource id that should be put into the stack. - */ - public mapResourceToStack(stackName: string, resource: string) { - this.stackMapping.set(resource, stackName); - } - - public getStackMapping(): StackMapping { - return this.stackMapping - } + public template: Template = blankTemplate(); + + public nodeMap: { [name: string]: TypeSystemDefinitionNode } = {}; + + public inputDocument: DocumentNode; + + public metadata: TransformerContextMetadata = new TransformerContextMetadata(); + + private stackMapping: StackMapping = new Map(); + + constructor(inputSDL: string) { + const doc: DocumentNode = parse(inputSDL); + for (const def of doc.definitions) { + if (def.kind === 'OperationDefinition' || def.kind === 'FragmentDefinition') { + throw new Error(`Found a ${def.kind}. Transformers accept only documents consisting of TypeSystemDefinitions.`); + } + } + this.inputDocument = doc; + this.fillNodeMapWithInput(); + } + + /** + * Before running the transformers, first flush the input document + * into the node map. If a schema definition node then leave everything + * as is so customers can explicitly turn off mutations & subscriptions. + * If a SDN is not provided then we add the default schema and empty + * Query, Mutation, and Subscription + */ + private fillNodeMapWithInput(): void { + const extensionNodes = []; + for (const inputDef of this.inputDocument.definitions) { + switch (inputDef.kind) { + case Kind.OBJECT_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_DEFINITION: + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + case Kind.ENUM_TYPE_DEFINITION: + case Kind.UNION_TYPE_DEFINITION: + const typeDef = inputDef as TypeDefinitionNode; + if (!this.getType(typeDef.name.value)) { + this.addType(typeDef); + } + break; + case Kind.SCHEMA_DEFINITION: + if (!this.getSchema()) { + const typeDef = inputDef as SchemaDefinitionNode; + this.putSchema(typeDef); + } + break; + case Kind.OBJECT_TYPE_EXTENSION: + case Kind.ENUM_TYPE_EXTENSION: + case Kind.UNION_TYPE_EXTENSION: + case Kind.INTERFACE_TYPE_EXTENSION: + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + extensionNodes.push(inputDef); + break; + case Kind.SCALAR_TYPE_EXTENSION: + default: + /* pass any others */ + } + } + // We add the extension nodes last so that the order of input documents does not matter. + // At this point, all input documents have been processed so the base types will be present. + for (const ext of extensionNodes) { + switch (ext.kind) { + case Kind.OBJECT_TYPE_EXTENSION: + this.addObjectExtension(ext); + break; + case Kind.INTERFACE_TYPE_EXTENSION: + this.addInterfaceExtension(ext); + break; + case Kind.UNION_TYPE_EXTENSION: + this.addUnionExtension(ext); + break; + case Kind.ENUM_TYPE_EXTENSION: + this.addEnumExtension(ext); + break; + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + this.addInputExtension(ext); + break; + case Kind.SCALAR_TYPE_EXTENSION: + default: + continue; + } + } + // If no schema definition is provided then fill with the default one. + if (!this.getSchema()) { + this.putSchema(DefaultSchemaDefinition); + } + } + + /** + * Scans through the context nodeMap and returns all type definition nodes + * that are of the given kind. + * @param kind Kind value of type definition nodes expected. + */ + public getTypeDefinitionsOfKind(kind: string) { + const typeDefs: TypeDefinitionNode[] = []; + for (const key of Object.keys(this.nodeMap)) { + const definition = this.nodeMap[key]; + if (definition.kind === kind) { + typeDefs.push(definition as TypeDefinitionNode); + } + } + return typeDefs; + } + + public mergeResources(resources: { [key: string]: Resource }) { + for (const resourceId of Object.keys(resources)) { + if (this.template.Resources[resourceId]) { + throw new Error(`Conflicting CloudFormation resource logical id: ${resourceId}`); + } + } + this.template.Resources = { ...this.template.Resources, ...resources }; + } + + public mergeParameters(params: { [key: string]: Parameter }) { + for (const parameterName of Object.keys(params)) { + if (this.template.Parameters[parameterName]) { + throw new Error(`Conflicting CloudFormation parameter name: ${parameterName}`); + } + } + this.template.Parameters = { ...this.template.Parameters, ...params }; + } + + public mergeConditions(conditions: { [key: string]: Condition }) { + if (!this.template.Conditions) { + this.template.Conditions = {}; + } + for (const conditionName of Object.keys(conditions)) { + if (this.template.Conditions[conditionName]) { + throw new Error(`Conflicting CloudFormation condition name: ${conditionName}`); + } + } + this.template.Conditions = { ...this.template.Conditions, ...conditions }; + } + + public getResource(resource: string): Resource { + return this.template.Resources[resource]; + } + + public setResource(key: string, resource: Resource): void { + this.template.Resources[key] = resource; + } + + public setOutput(key: string, output: Output): void { + this.template.Outputs[key] = output; + } + + public getOutput(key: string): Output { + return this.template.Outputs[key]; + } + + public mergeOutputs(outputs: { [key: string]: Output }) { + for (const outputName of Object.keys(outputs)) { + if (this.template.Parameters[outputName]) { + throw new Error(`Conflicting CloudFormation parameter name: ${outputName}`); + } + } + this.template.Outputs = { ...this.template.Outputs, ...outputs }; + } + + public mergeMappings(mapping: MappingParameters) { + for (const mappingName of Object.keys(mapping)) { + if (this.template.Mappings[mappingName]) { + throw new Error(`Conflicting CloudFormation mapping name: ${mappingName}`); + } + } + this.template.Mappings = { ...this.template.Mappings, ...mapping }; + } + + /** + * Add an object type definition node to the context. If the type already + * exists an error will be thrown. + * @param obj The object type definition node to add. + */ + public putSchema(obj: SchemaDefinitionNode) { + this.nodeMap.__schema = obj; + } + + /** + * Returns the schema definition record. If the user provides a schema + * definition as part of the input document, that node is returned. + * Otherwise a blank schema definition with default operation types + * is returned. + */ + public getSchema(): SchemaDefinitionNode { + return this.nodeMap.__schema as SchemaDefinitionNode; + } + + public getQueryTypeName(): string | undefined { + const schemaNode = this.getSchema(); + const queryTypeName = schemaNode.operationTypes.find((op: OperationTypeDefinitionNode) => op.operation === 'query'); + if (queryTypeName && queryTypeName.type && queryTypeName.type.name) { + return queryTypeName.type.name.value; + } + } + + public getQuery(): ObjectTypeDefinitionNode | undefined { + const queryTypeName = this.getQueryTypeName(); + if (queryTypeName) { + return this.nodeMap[queryTypeName] as ObjectTypeDefinitionNode | undefined; + } + } + + public getMutationTypeName(): string | undefined { + const schemaNode = this.getSchema(); + const mutationTypeName = schemaNode.operationTypes.find((op: OperationTypeDefinitionNode) => op.operation === 'mutation'); + if (mutationTypeName && mutationTypeName.type && mutationTypeName.type.name) { + return mutationTypeName.type.name.value; + } + } + + public getMutation(): ObjectTypeDefinitionNode | undefined { + const mutationTypeName = this.getMutationTypeName(); + if (mutationTypeName) { + return this.nodeMap[mutationTypeName] as ObjectTypeDefinitionNode | undefined; + } + } + + public getSubscriptionTypeName(): string | undefined { + const schemaNode = this.getSchema(); + const subscriptionTypeName = schemaNode.operationTypes.find((op: OperationTypeDefinitionNode) => op.operation === 'subscription'); + if (subscriptionTypeName && subscriptionTypeName.type && subscriptionTypeName.type.name) { + return subscriptionTypeName.type.name.value; + } + } + + public getSubscription(): ObjectTypeDefinitionNode | undefined { + const subscriptionTypeName = this.getSubscriptionTypeName(); + if (subscriptionTypeName) { + return this.nodeMap[subscriptionTypeName] as ObjectTypeDefinitionNode | undefined; + } + } + + /** + * Add a generic type. + * @param obj The type to add + */ + public addType(obj: TypeDefinitionNode) { + if (this.nodeMap[obj.name.value]) { + throw new Error(`Conflicting type '${obj.name.value}' found.`); + } + this.nodeMap[obj.name.value] = obj; + } + + public putType(obj: TypeDefinitionNode) { + this.nodeMap[obj.name.value] = obj; + } + + public getType(name: string): TypeSystemDefinitionNode | undefined { + return this.nodeMap[name]; + } + + /** + * Add an object type definition node to the context. If the type already + * exists an error will be thrown. + * @param obj The object type definition node to add. + */ + public addObject(obj: ObjectTypeDefinitionNode) { + if (this.nodeMap[obj.name.value]) { + throw new Error(`Conflicting type '${obj.name.value}' found.`); + } + this.nodeMap[obj.name.value] = obj; + } + + public getObject(name: string): ObjectTypeDefinitionNode | undefined { + if (this.nodeMap[name]) { + const node = this.nodeMap[name]; + if (node.kind === Kind.OBJECT_TYPE_DEFINITION) { + return node as ObjectTypeDefinitionNode; + } + } + } + + /** + * Extends the context query object with additional fields. + * If the customer uses a name other than 'Query' this will proxy to the + * correct type. + * @param fields The fields to add the query type. + */ + public addQueryFields(fields: FieldDefinitionNode[]) { + const queryTypeName = this.getQueryTypeName(); + if (queryTypeName) { + if (!this.getType(queryTypeName)) { + this.addType(blankObject(queryTypeName)); + } + let queryType = objectExtension(queryTypeName, fields); + this.addObjectExtension(queryType); + } + } + + /** + * Extends the context mutation object with additional fields. + * If the customer uses a name other than 'Mutation' this will proxy to the + * correct type. + * @param fields The fields to add the mutation type. + */ + public addMutationFields(fields: FieldDefinitionNode[]) { + const mutationTypeName = this.getMutationTypeName(); + if (mutationTypeName) { + if (!this.getType(mutationTypeName)) { + this.addType(blankObject(mutationTypeName)); + } + let mutationType = objectExtension(mutationTypeName, fields); + this.addObjectExtension(mutationType); + } + } + + /** + * Extends the context subscription object with additional fields. + * If the customer uses a name other than 'Subscription' this will proxy to the + * correct type. + * @param fields The fields to add the subscription type. + */ + public addSubscriptionFields(fields: FieldDefinitionNode[]) { + const subscriptionTypeName = this.getSubscriptionTypeName(); + if (subscriptionTypeName) { + if (!this.getType(subscriptionTypeName)) { + this.addType(blankObject(subscriptionTypeName)); + } + let subscriptionType = objectExtension(subscriptionTypeName, fields); + this.addObjectExtension(subscriptionType); + } + } + + /** + * Add an object type extension definition node to the context. If a type with this + * name does not already exist, an exception is thrown. + * @param obj The object type definition node to add. + */ + public addObjectExtension(obj: ObjectTypeExtensionNode) { + if (!this.nodeMap[obj.name.value]) { + throw new Error(`Cannot extend non-existant type '${obj.name.value}'.`); + } + // AppSync does not yet understand type extensions so fold the types in. + const oldNode = this.getObject(obj.name.value); + const newDirs = []; + const oldDirs = oldNode.directives || []; + + // Filter out duplicate directives, do not add them + if (obj.directives) { + for (const newDir of obj.directives) { + if (Boolean(oldDirs.find(d => d.name.value === newDir.name.value)) === false) { + newDirs.push(newDir); + } + } + } + + const mergedDirs = [...oldDirs, ...newDirs]; + + // An extension cannot redeclare fields. + const oldFields = oldNode.fields || []; + const oldFieldMap = oldFields.reduce( + (acc: any, field: FieldDefinitionNode) => ({ + ...acc, + [field.name.value]: field, + }), + {} + ); + const newFields = obj.fields || []; + const mergedFields = [...oldFields]; + for (const newField of newFields) { + if (oldFieldMap[newField.name.value]) { + throw new Error(`Object type extension '${obj.name.value}' cannot redeclare field ${newField.name.value}`); + } + mergedFields.push(newField); + } + + // An extension cannot redeclare interfaces + const oldInterfaces = oldNode.interfaces || []; + const oldInterfaceMap = oldInterfaces.reduce( + (acc: any, field: NamedTypeNode) => ({ + ...acc, + [field.name.value]: field, + }), + {} + ); + const newInterfaces = obj.interfaces || []; + const mergedInterfaces = [...oldInterfaces]; + for (const newInterface of newInterfaces) { + if (oldInterfaceMap[newInterface.name.value]) { + throw new Error(`Object type extension '${obj.name.value}' cannot redeclare interface ${newInterface.name.value}`); + } + mergedInterfaces.push(newInterface); + } + this.nodeMap[oldNode.name.value] = { + ...oldNode, + interfaces: mergedInterfaces, + directives: mergedDirs, + fields: mergedFields, + }; + } + + /** + * Add an input object type extension definition node to the context. If a type with this + * name does not already exist, an exception is thrown. + * @param obj The input object type definition node to add. + */ + public addInputExtension(obj: InputObjectTypeExtensionNode) { + if (!this.nodeMap[obj.name.value]) { + throw new Error(`Cannot extend non-existant input '${obj.name.value}'.`); + } + // AppSync does not yet understand type extensions so fold the types in. + const oldNode = this.getType(obj.name.value) as InputObjectTypeDefinitionNode; + const newDirs = obj.directives || []; + const oldDirs = oldNode.directives || []; + const mergedDirs = [...oldDirs, ...newDirs]; + + // An extension cannot redeclare fields. + const oldFields = oldNode.fields || []; + const oldFieldMap = oldFields.reduce( + (acc: any, field: InputValueDefinitionNode) => ({ + ...acc, + [field.name.value]: field, + }), + {} + ); + const newFields = obj.fields || []; + const mergedFields = [...oldFields]; + for (const newField of newFields) { + if (oldFieldMap[newField.name.value]) { + throw new Error(`Input object type extension '${obj.name.value}' cannot redeclare field ${newField.name.value}`); + } + mergedFields.push(newField); + } + + this.nodeMap[oldNode.name.value] = { + ...oldNode, + directives: mergedDirs, + fields: mergedFields, + }; + } + + /** + * Add an interface extension definition node to the context. If a type with this + * name does not already exist, an exception is thrown. + * @param obj The interface type definition node to add. + */ + public addInterfaceExtension(obj: InterfaceTypeExtensionNode) { + if (!this.nodeMap[obj.name.value]) { + throw new Error(`Cannot extend non-existant interface '${obj.name.value}'.`); + } + // AppSync does not yet understand type extensions so fold the types in. + const oldNode = this.getType(obj.name.value) as InterfaceTypeDefinitionNode; + const newDirs = obj.directives || []; + const oldDirs = oldNode.directives || []; + const mergedDirs = [...oldDirs, ...newDirs]; + + // An extension cannot redeclare fields. + const oldFields = oldNode.fields || []; + const oldFieldMap = oldFields.reduce( + (acc: any, field: FieldDefinitionNode) => ({ + ...acc, + [field.name.value]: field, + }), + {} + ); + const newFields = obj.fields || []; + const mergedFields = [...oldFields]; + for (const newField of newFields) { + if (oldFieldMap[newField.name.value]) { + throw new Error(`Interface type extension '${obj.name.value}' cannot redeclare field ${newField.name.value}`); + } + mergedFields.push(newField); + } + + this.nodeMap[oldNode.name.value] = { + ...oldNode, + directives: mergedDirs, + fields: mergedFields, + }; + } + + /** + * Add an union extension definition node to the context. If a type with this + * name does not already exist, an exception is thrown. + * @param obj The union type definition node to add. + */ + public addUnionExtension(obj: UnionTypeExtensionNode) { + if (!this.nodeMap[obj.name.value]) { + throw new Error(`Cannot extend non-existant union '${obj.name.value}'.`); + } + // AppSync does not yet understand type extensions so fold the types in. + const oldNode = this.getType(obj.name.value) as UnionTypeDefinitionNode; + const newDirs = obj.directives || []; + const oldDirs = oldNode.directives || []; + const mergedDirs = [...oldDirs, ...newDirs]; + + // An extension cannot redeclare possible values + const oldTypes = oldNode.types || []; + const oldTypeMap = oldTypes.reduce( + (acc: any, type: NamedTypeNode) => ({ + ...acc, + [type.name.value]: true, + }), + {} + ); + const newTypes = obj.types || []; + const mergedFields = [...oldTypes]; + for (const newType of newTypes) { + if (oldTypeMap[newType.name.value]) { + throw new Error(`Union type extension '${obj.name.value}' cannot redeclare type ${newType.name.value}`); + } + mergedFields.push(newType); + } + + this.nodeMap[oldNode.name.value] = { + ...oldNode, + directives: mergedDirs, + types: mergedFields, + }; + } + + /** + * Add an enum extension definition node to the context. If a type with this + * name does not already exist, an exception is thrown. + * @param obj The enum type definition node to add. + */ + public addEnumExtension(obj: EnumTypeExtensionNode) { + if (!this.nodeMap[obj.name.value]) { + throw new Error(`Cannot extend non-existant enum '${obj.name.value}'.`); + } + // AppSync does not yet understand type extensions so fold the types in. + const oldNode = this.getType(obj.name.value) as EnumTypeDefinitionNode; + const newDirs = obj.directives || []; + const oldDirs = oldNode.directives || []; + const mergedDirs = [...oldDirs, ...newDirs]; + + // An extension cannot redeclare possible values + const oldValues = oldNode.values || []; + const oldValuesMap = oldValues.reduce( + (acc: any, type: EnumValueDefinitionNode) => ({ + ...acc, + [type.name.value]: true, + }), + {} + ); + const newValues = obj.values || []; + const mergedValues = [...oldValues]; + for (const newValue of newValues) { + if (oldValuesMap[newValue.name.value]) { + throw new Error(`Enum type extension '${obj.name.value}' cannot redeclare value ${newValue.name.value}`); + } + mergedValues.push(newValue); + } + + this.nodeMap[oldNode.name.value] = { + ...oldNode, + directives: mergedDirs, + values: mergedValues, + }; + } + + /** + * Add an input type definition node to the context. + * @param inp The input type definition node to add. + */ + public addInput(inp: InputObjectTypeDefinitionNode) { + if (this.nodeMap[inp.name.value]) { + throw new Error(`Conflicting input type '${inp.name.value}' found.`); + } + this.nodeMap[inp.name.value] = inp; + } + + /** + * Add an enum type definition node to the context. + * @param en The enum type definition node to add. + */ + public addEnum(en: EnumTypeDefinitionNode) { + if (this.nodeMap[en.name.value]) { + throw new Error(`Conflicting enum type '${en.name.value}' found.`); + } + this.nodeMap[en.name.value] = en; + } + + /** + * Add an item to the stack mapping. + * @param stackName The destination stack name. + * @param resource The resource id that should be put into the stack. + */ + public mapResourceToStack(stackName: string, resource: string) { + this.stackMapping.set(resource, stackName); + } + + public getStackMapping(): StackMapping { + return this.stackMapping; + } } diff --git a/packages/graphql-transformer-core/src/__tests__/GraphQLTransform.test.ts b/packages/graphql-transformer-core/src/__tests__/GraphQLTransform.test.ts index b2255297ac..f2d661b2b9 100644 --- a/packages/graphql-transformer-core/src/__tests__/GraphQLTransform.test.ts +++ b/packages/graphql-transformer-core/src/__tests__/GraphQLTransform.test.ts @@ -1,107 +1,107 @@ -import { - ObjectTypeDefinitionNode, DirectiveNode, parse -} from 'graphql' -import GraphQLTransform from '../GraphQLTransform' -import TransformerContext from '../TransformerContext' -import Transformer from '../Transformer' -import { getDirectiveArguments, gql } from '../util' +import { ObjectTypeDefinitionNode, DirectiveNode, parse } from 'graphql'; +import GraphQLTransform from '../GraphQLTransform'; +import TransformerContext from '../TransformerContext'; +import Transformer from '../Transformer'; +import { getDirectiveArguments, gql } from '../util'; class ValidObjectTransformer extends Transformer { - constructor() { - super( - 'ValidObjectTransformer', gql`directive @ObjectDirective on OBJECT`) - } + constructor() { + super( + 'ValidObjectTransformer', + gql` + directive @ObjectDirective on OBJECT + ` + ); + } - public object = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => { - return - } + public object = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => { + return; + }; } class InvalidObjectTransformer extends Transformer { - constructor() { - super('InvalidObjectTransformer', gql`directive @ObjectDirective on OBJECT`) - } + constructor() { + super( + 'InvalidObjectTransformer', + gql` + directive @ObjectDirective on OBJECT + ` + ); + } } test('Test graphql transformer validation happy case', () => { - const validSchema = `type Post @ObjectDirective { id: ID! }` - const transformer = new GraphQLTransform({ - transformers: [ - new ValidObjectTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() + const validSchema = `type Post @ObjectDirective { id: ID! }`; + const transformer = new GraphQLTransform({ + transformers: [new ValidObjectTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); }); test('Test graphql transformer validation. Transformer does not implement required method.', () => { - const validSchema = `type Post @ObjectDirective { id: ID! }` - const transformer = new GraphQLTransform({ - transformers: [ - new InvalidObjectTransformer() - ] - }) - try { - transformer.transform(validSchema); - } catch (e) { - expect(e.name).toEqual('InvalidTransformerError') - } + const validSchema = `type Post @ObjectDirective { id: ID! }`; + const transformer = new GraphQLTransform({ + transformers: [new InvalidObjectTransformer()], + }); + try { + transformer.transform(validSchema); + } catch (e) { + expect(e.name).toEqual('InvalidTransformerError'); + } }); test('Test graphql transformer validation. Unknown directive.', () => { - const invalidSchema = `type Post @UnknownDirective { id: ID! }` - const transformer = new GraphQLTransform({ - transformers: [ - new InvalidObjectTransformer() - ] - }) - try { - transformer.transform(invalidSchema); - } catch (e) { - expect(e.name).toEqual('SchemaValidationError') - } + const invalidSchema = `type Post @UnknownDirective { id: ID! }`; + const transformer = new GraphQLTransform({ + transformers: [new InvalidObjectTransformer()], + }); + try { + transformer.transform(invalidSchema); + } catch (e) { + expect(e.name).toEqual('SchemaValidationError'); + } }); class PingTransformer extends Transformer { - constructor() { - super( - 'ValidObjectTransformer', - gql` - directive @ping(config: PingConfig) on OBJECT - input PingConfig { - url: String! - } - `) - } + constructor() { + super( + 'ValidObjectTransformer', + gql` + directive @ping(config: PingConfig) on OBJECT + input PingConfig { + url: String! + } + ` + ); + } - public object = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => { - return - } + public object = (definition: ObjectTypeDefinitionNode, directive: DirectiveNode, acc: TransformerContext) => { + return; + }; } test('Test graphql transformer validation on bad shapes. @ping directive.', () => { - const invalidSchema = `type Post @ping(config: { bad: "shape" }) { id: ID! }` - const transformer = new GraphQLTransform({ - transformers: [ - new PingTransformer() - ] - }) - try { - console.log(`Transforming: \n${invalidSchema}`) - const out = transformer.transform(invalidSchema); - expect(true).toEqual(false) - } catch (e) { - console.log(e.message) - expect(e.name).toEqual('SchemaValidationError') - } + const invalidSchema = `type Post @ping(config: { bad: "shape" }) { id: ID! }`; + const transformer = new GraphQLTransform({ + transformers: [new PingTransformer()], + }); + try { + console.log(`Transforming: \n${invalidSchema}`); + const out = transformer.transform(invalidSchema); + expect(true).toEqual(false); + } catch (e) { + console.log(e.message); + expect(e.name).toEqual('SchemaValidationError'); + } }); test('Test graphql transformer returns correct number of arguments from directive', () => { - const validSchema = `type Post @model(queries: { list: "listPost" }, mutations: {create: "createCustom"}) { name: String! }` - const transformer = new ValidObjectTransformer() - const doc = parse(validSchema) - const def = doc.definitions[0] as ObjectTypeDefinitionNode - const map: any = getDirectiveArguments(def.directives[0]) - expect(map).not.toBeNull() - expect(Object.keys(map)).toEqual(expect.arrayContaining(['mutations', 'queries'])) -}) \ No newline at end of file + const validSchema = `type Post @model(queries: { list: "listPost" }, mutations: {create: "createCustom"}) { name: String! }`; + const transformer = new ValidObjectTransformer(); + const doc = parse(validSchema); + const def = doc.definitions[0] as ObjectTypeDefinitionNode; + const map: any = getDirectiveArguments(def.directives[0]); + expect(map).not.toBeNull(); + expect(Object.keys(map)).toEqual(expect.arrayContaining(['mutations', 'queries'])); +}); diff --git a/packages/graphql-transformer-core/src/__tests__/TransformFormatter.test.ts b/packages/graphql-transformer-core/src/__tests__/TransformFormatter.test.ts index dc67585236..b7b8c7658a 100644 --- a/packages/graphql-transformer-core/src/__tests__/TransformFormatter.test.ts +++ b/packages/graphql-transformer-core/src/__tests__/TransformFormatter.test.ts @@ -1,91 +1,83 @@ -import { TransformFormatter } from '../TransformFormatter' +import { TransformFormatter } from '../TransformFormatter'; import { Template, Fn, AppSync, DynamoDB } from 'cloudform-types'; import { TransformerContext } from '..'; const template: Template = { - Parameters: { - IsProd: { - Type: "String", - Default: "prod" - } + Parameters: { + IsProd: { + Type: 'String', + Default: 'prod', }, - Conditions: { - IsProd: Fn.Equals(Fn.Ref("env"), "prod") + }, + Conditions: { + IsProd: Fn.Equals(Fn.Ref('env'), 'prod'), + }, + Resources: { + API: new AppSync.GraphQLApi({ + Name: 'My AppSync API', + AuthenticationType: 'API_KEY', + }), + PostTableDataSource: new AppSync.DataSource({ + ApiId: Fn.Ref('API'), + Name: 'PostDataSource', + Type: 'AMAZON_DYNAMODB', + }), + PostTable: new DynamoDB.Table({ + KeySchema: [ + { + AttributeName: 'id', + KeyType: 'HASH', + }, + ], + ProvisionedThroughput: { + ReadCapacityUnits: 5, + WriteCapacityUnits: 5, + }, + }), + CreatePostResolver: new AppSync.Resolver({ + ApiId: Fn.Ref('API'), + DataSourceName: Fn.GetAtt('PostTableDataSource', 'name'), + FieldName: 'createPost', + TypeName: 'Mutation', + }), + UpdatePostResolver: new AppSync.Resolver({ + ApiId: Fn.Ref('API'), + DataSourceName: Fn.Join(':', [Fn.Ref('PostTable'), Fn.Join(':', [Fn.GetAtt('PostTableDataSource', 'name')])]), + // Contrived examples for test coverage. + FieldName: Fn.Split(':', Fn.Ref('PostTable')), + TypeName: Fn.Sub('${t}', { + t: Fn.Ref('PostTable'), + }), + RequestMappingTemplate: Fn.Select(0, [Fn.Ref('PostTable')]), + }), + }, + Outputs: { + PostTableOutput: { + Description: 'PostTable Arn.', + Value: Fn.GetAtt('PostTable', 'Arn'), }, - Resources: { - API: new AppSync.GraphQLApi({ - Name: "My AppSync API", - AuthenticationType: "API_KEY" - }), - PostTableDataSource: new AppSync.DataSource({ - ApiId: Fn.Ref("API"), - Name: "PostDataSource", - Type: "AMAZON_DYNAMODB" - }), - PostTable: new DynamoDB.Table({ - KeySchema: [{ - AttributeName: "id", - KeyType: "HASH" - }], - ProvisionedThroughput: { - ReadCapacityUnits: 5, - WriteCapacityUnits: 5 - } - }), - CreatePostResolver: new AppSync.Resolver({ - ApiId: Fn.Ref("API"), - DataSourceName: Fn.GetAtt("PostTableDataSource", "name"), - FieldName: "createPost", - TypeName: "Mutation" - }), - UpdatePostResolver: new AppSync.Resolver({ - ApiId: Fn.Ref("API"), - DataSourceName: Fn.Join(":", [ - Fn.Ref("PostTable"), - Fn.Join(":", [ - Fn.GetAtt("PostTableDataSource", "name") - ]) - ]), - // Contrived examples for test coverage. - FieldName: Fn.Split(":", Fn.Ref("PostTable")), - TypeName: Fn.Sub("${t}", { - t: Fn.Ref("PostTable") - }), - RequestMappingTemplate: Fn.Select(0, [ - Fn.Ref("PostTable") - ]) - }) + }, + Mappings: { + LayerResourceMapping: { + 'us-east-1': { + layerRegion: 'arn:aws:lambda:us-east-1:668099181075:layer:AWSLambda-Python-AWS-SDK:1', + }, }, - Outputs: { - PostTableOutput: { - Description: "PostTable Arn.", - Value: Fn.GetAtt("PostTable", 'Arn') - } - }, - Mappings: { - "LayerResourceMapping":{ - "us-east-1": { - "layerRegion": "arn:aws:lambda:us-east-1:668099181075:layer:AWSLambda-Python-AWS-SDK:1" - }, - } - } -} - - - + }, +}; test('Test getTemplateReferences', () => { - const formatter = new TransformFormatter(); - const context = new TransformerContext('type Post @model { id: ID! title: String }') - context.mapResourceToStack('PostModel', 'CreatePostResolver'); - context.mapResourceToStack('PostModel', 'UpdatePostResolver'); - context.mapResourceToStack('PostModel', 'PostTableDataSource'); - context.mapResourceToStack('PostModel', 'PostTable'); - context.mapResourceToStack('PostModel', 'PostTableOutput'); - context.template = template; - const deploymentResources = formatter.format(context) - expect(Object.keys(deploymentResources.stacks.PostModel.Resources)).toHaveLength(4) - expect(Object.keys(deploymentResources.rootStack.Resources)).toHaveLength(3) - expect(Object.keys(deploymentResources.stacks.PostModel.Outputs)).toHaveLength(1); - expect(Object.keys(deploymentResources.rootStack.Outputs)).toHaveLength(0); -}); \ No newline at end of file + const formatter = new TransformFormatter(); + const context = new TransformerContext('type Post @model { id: ID! title: String }'); + context.mapResourceToStack('PostModel', 'CreatePostResolver'); + context.mapResourceToStack('PostModel', 'UpdatePostResolver'); + context.mapResourceToStack('PostModel', 'PostTableDataSource'); + context.mapResourceToStack('PostModel', 'PostTable'); + context.mapResourceToStack('PostModel', 'PostTableOutput'); + context.template = template; + const deploymentResources = formatter.format(context); + expect(Object.keys(deploymentResources.stacks.PostModel.Resources)).toHaveLength(4); + expect(Object.keys(deploymentResources.rootStack.Resources)).toHaveLength(3); + expect(Object.keys(deploymentResources.stacks.PostModel.Outputs)).toHaveLength(1); + expect(Object.keys(deploymentResources.rootStack.Outputs)).toHaveLength(0); +}); diff --git a/packages/graphql-transformer-core/src/__tests__/getTemplateReferences.test.ts b/packages/graphql-transformer-core/src/__tests__/getTemplateReferences.test.ts index 187206791f..8a1aa072dc 100644 --- a/packages/graphql-transformer-core/src/__tests__/getTemplateReferences.test.ts +++ b/packages/graphql-transformer-core/src/__tests__/getTemplateReferences.test.ts @@ -1,80 +1,80 @@ -import { getTemplateReferences } from '../util/getTemplateReferences' +import { getTemplateReferences } from '../util/getTemplateReferences'; import { Template, Fn, AppSync, DynamoDB } from 'cloudform-types'; const template: Template = { - Parameters: { - IsProd: { - Type: "String", - Default: "prod" - } + Parameters: { + IsProd: { + Type: 'String', + Default: 'prod', }, - Conditions: { - IsProd: Fn.Equals(Fn.Ref("env"), "prod") - }, - Resources: { - API: new AppSync.GraphQLApi({ - Name: "My AppSync API", - AuthenticationType: "API_KEY" - }), - PostTableDataSource: new AppSync.DataSource({ - ApiId: Fn.Ref("API"), - Name: "PostDataSource", - Type: "AMAZON_DYNAMODB" - }), - PostTable: new DynamoDB.Table({ - KeySchema: [{ - AttributeName: "id", - KeyType: "HASH" - }], - ProvisionedThroughput: { - ReadCapacityUnits: 5, - WriteCapacityUnits: 5 - } - }), - CreatePostResolver: new AppSync.Resolver({ - ApiId: Fn.Ref("API"), - DataSourceName: Fn.GetAtt("PostTableDataSource", "name"), - FieldName: "createPost", - TypeName: "Mutation" - }), - UpdatePostResolver: new AppSync.Resolver({ - ApiId: Fn.Ref("API"), - DataSourceName: Fn.Join(":", [ - Fn.Ref("PostTable"), - Fn.Join(":", [ - Fn.GetAtt("PostTableDataSource", "name") - ]) - ]), - // Contrived examples for test coverage. - FieldName: Fn.Split(":", Fn.Ref("PostTable")), - TypeName: Fn.Sub("${t}", { - t: Fn.Ref("PostTable") - }), - RequestMappingTemplate: Fn.Select(0, [ - Fn.Ref("PostTable") - ]) - }) - } -} - + }, + Conditions: { + IsProd: Fn.Equals(Fn.Ref('env'), 'prod'), + }, + Resources: { + API: new AppSync.GraphQLApi({ + Name: 'My AppSync API', + AuthenticationType: 'API_KEY', + }), + PostTableDataSource: new AppSync.DataSource({ + ApiId: Fn.Ref('API'), + Name: 'PostDataSource', + Type: 'AMAZON_DYNAMODB', + }), + PostTable: new DynamoDB.Table({ + KeySchema: [ + { + AttributeName: 'id', + KeyType: 'HASH', + }, + ], + ProvisionedThroughput: { + ReadCapacityUnits: 5, + WriteCapacityUnits: 5, + }, + }), + CreatePostResolver: new AppSync.Resolver({ + ApiId: Fn.Ref('API'), + DataSourceName: Fn.GetAtt('PostTableDataSource', 'name'), + FieldName: 'createPost', + TypeName: 'Mutation', + }), + UpdatePostResolver: new AppSync.Resolver({ + ApiId: Fn.Ref('API'), + DataSourceName: Fn.Join(':', [Fn.Ref('PostTable'), Fn.Join(':', [Fn.GetAtt('PostTableDataSource', 'name')])]), + // Contrived examples for test coverage. + FieldName: Fn.Split(':', Fn.Ref('PostTable')), + TypeName: Fn.Sub('${t}', { + t: Fn.Ref('PostTable'), + }), + RequestMappingTemplate: Fn.Select(0, [Fn.Ref('PostTable')]), + }), + }, +}; test('Test getTemplateReferences', () => { - const referenceMap = getTemplateReferences(template) //JSON.parse(JSON.stringify(template, null, 4))); - expect(referenceMap).toBeTruthy(); - expect(referenceMap.env).toEqual([['Conditions', 'IsProd', 'Fn::Equals', '0']]) - expect(referenceMap.API.sort()).toEqual([ - ['Resources', 'PostTableDataSource', 'Properties', 'ApiId'], - ['Resources', 'CreatePostResolver', 'Properties', 'ApiId'], - ['Resources', 'UpdatePostResolver', 'Properties', 'ApiId'] - ].sort()) - expect(referenceMap.PostTableDataSource.sort()).toEqual([ - ['Resources', 'CreatePostResolver', 'Properties', 'DataSourceName'], - ['Resources', 'UpdatePostResolver', 'Properties', 'DataSourceName', 'Fn::Join', '1', '1', 'Fn::Join', '1', '0'], - ].sort()) - expect(referenceMap.PostTable.sort()).toEqual([ - ['Resources', 'UpdatePostResolver', 'Properties', 'DataSourceName', 'Fn::Join', '1', '0'], - ['Resources', 'UpdatePostResolver', 'Properties', 'FieldName', 'Fn::Split', '1'], - ['Resources', 'UpdatePostResolver', 'Properties', 'TypeName', 'Fn::Sub', '1', 't'], - ['Resources', 'UpdatePostResolver', 'Properties', 'RequestMappingTemplate', 'Fn::Select', '1', '0'] - ].sort()) -}); \ No newline at end of file + const referenceMap = getTemplateReferences(template); //JSON.parse(JSON.stringify(template, null, 4))); + expect(referenceMap).toBeTruthy(); + expect(referenceMap.env).toEqual([['Conditions', 'IsProd', 'Fn::Equals', '0']]); + expect(referenceMap.API.sort()).toEqual( + [ + ['Resources', 'PostTableDataSource', 'Properties', 'ApiId'], + ['Resources', 'CreatePostResolver', 'Properties', 'ApiId'], + ['Resources', 'UpdatePostResolver', 'Properties', 'ApiId'], + ].sort() + ); + expect(referenceMap.PostTableDataSource.sort()).toEqual( + [ + ['Resources', 'CreatePostResolver', 'Properties', 'DataSourceName'], + ['Resources', 'UpdatePostResolver', 'Properties', 'DataSourceName', 'Fn::Join', '1', '1', 'Fn::Join', '1', '0'], + ].sort() + ); + expect(referenceMap.PostTable.sort()).toEqual( + [ + ['Resources', 'UpdatePostResolver', 'Properties', 'DataSourceName', 'Fn::Join', '1', '0'], + ['Resources', 'UpdatePostResolver', 'Properties', 'FieldName', 'Fn::Split', '1'], + ['Resources', 'UpdatePostResolver', 'Properties', 'TypeName', 'Fn::Sub', '1', 't'], + ['Resources', 'UpdatePostResolver', 'Properties', 'RequestMappingTemplate', 'Fn::Select', '1', '0'], + ].sort() + ); +}); diff --git a/packages/graphql-transformer-core/src/collectDirectives.ts b/packages/graphql-transformer-core/src/collectDirectives.ts index da10662ecd..3c0dafde18 100644 --- a/packages/graphql-transformer-core/src/collectDirectives.ts +++ b/packages/graphql-transformer-core/src/collectDirectives.ts @@ -1,150 +1,151 @@ import { - ObjectTypeDefinitionNode, DirectiveNode, InterfaceTypeDefinitionNode, - UnionTypeDefinitionNode, ScalarTypeDefinitionNode, InputObjectTypeDefinitionNode, - FieldDefinitionNode, InputValueDefinitionNode, EnumValueDefinitionNode, EnumTypeDefinitionNode, - parse, - Kind -} from "graphql"; + ObjectTypeDefinitionNode, + DirectiveNode, + InterfaceTypeDefinitionNode, + UnionTypeDefinitionNode, + ScalarTypeDefinitionNode, + InputObjectTypeDefinitionNode, + FieldDefinitionNode, + InputValueDefinitionNode, + EnumValueDefinitionNode, + EnumTypeDefinitionNode, + parse, + Kind, +} from 'graphql'; export function collectDirectiveNames(sdl: string): string[] { - const dirs = collectDirectives(sdl) - return dirs.map(d => d.name.value) + const dirs = collectDirectives(sdl); + return dirs.map(d => d.name.value); } export function collectDirectives(sdl: string): DirectiveNode[] { - const doc = parse(sdl) - let directives = [] - for (const def of doc.definitions) { - switch (def.kind) { - case Kind.OBJECT_TYPE_DEFINITION: - // Does def node have a @model and no @auth. - directives = directives.concat(collectObjectDirectives(def)) - break; - case Kind.INTERFACE_TYPE_DEFINITION: - directives = directives.concat(collectInterfaceDirectives(def)) - break; - case Kind.UNION_TYPE_DEFINITION: - directives = directives.concat(collectUnionDirectives(def)) - break; - case Kind.INPUT_OBJECT_TYPE_DEFINITION: - directives = directives.concat(collectInputObjectDirectives(def)) - break; - case Kind.ENUM_TYPE_DEFINITION: - directives = directives.concat(collectEnumDirectives(def)) - break; - case Kind.SCALAR_TYPE_DEFINITION: - directives = directives.concat(collectScalarDirectives(def)) - break; - } + const doc = parse(sdl); + let directives = []; + for (const def of doc.definitions) { + switch (def.kind) { + case Kind.OBJECT_TYPE_DEFINITION: + // Does def node have a @model and no @auth. + directives = directives.concat(collectObjectDirectives(def)); + break; + case Kind.INTERFACE_TYPE_DEFINITION: + directives = directives.concat(collectInterfaceDirectives(def)); + break; + case Kind.UNION_TYPE_DEFINITION: + directives = directives.concat(collectUnionDirectives(def)); + break; + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + directives = directives.concat(collectInputObjectDirectives(def)); + break; + case Kind.ENUM_TYPE_DEFINITION: + directives = directives.concat(collectEnumDirectives(def)); + break; + case Kind.SCALAR_TYPE_DEFINITION: + directives = directives.concat(collectScalarDirectives(def)); + break; } - return directives + } + return directives; } export function collectDirectivesByTypeNames(sdl: string): Object { - let types = collectDirectivesByType(sdl); - const directives: Set = new Set(); - Object.keys(types).forEach( (dir) => { - let set: Set = new Set(); - types[dir].forEach( (d: DirectiveNode) => { - set.add(d.name.value); - directives.add(d.name.value); - }) - types[dir] = Array.from(set); - }) - return { types, directives: Array.from(directives) }; + let types = collectDirectivesByType(sdl); + const directives: Set = new Set(); + Object.keys(types).forEach(dir => { + let set: Set = new Set(); + types[dir].forEach((d: DirectiveNode) => { + set.add(d.name.value); + directives.add(d.name.value); + }); + types[dir] = Array.from(set); + }); + return { types, directives: Array.from(directives) }; } export function collectDirectivesByType(sdl: string): Object { - const doc = parse(sdl) - // defined types with directives list - let types = {}; - for (const def of doc.definitions) { - switch (def.kind) { - case Kind.OBJECT_TYPE_DEFINITION: - types[def.name.value] = [ ...types[def.name.value] || [], - ...collectObjectDirectives(def)] - break; - case Kind.INTERFACE_TYPE_DEFINITION: - types[def.name.value] = [...types[def.name.value] || [], - ...collectInterfaceDirectives(def)] - break; - case Kind.UNION_TYPE_DEFINITION: - types[def.name.value] = [...types[def.name.value] || [], - ...collectUnionDirectives(def)] - break; - case Kind.INPUT_OBJECT_TYPE_DEFINITION: - types[def.name.value] = [...types[def.name.value] || [], - ...collectInputObjectDirectives(def)] - break; - case Kind.ENUM_TYPE_DEFINITION: - types[def.name.value] = [...types[def.name.value] || [], - ...collectEnumDirectives(def)] - break; - case Kind.SCALAR_TYPE_DEFINITION: - types[def.name.value] = [...types[def.name.value] || [], - ...collectScalarDirectives(def)] - break; - } + const doc = parse(sdl); + // defined types with directives list + let types = {}; + for (const def of doc.definitions) { + switch (def.kind) { + case Kind.OBJECT_TYPE_DEFINITION: + types[def.name.value] = [...(types[def.name.value] || []), ...collectObjectDirectives(def)]; + break; + case Kind.INTERFACE_TYPE_DEFINITION: + types[def.name.value] = [...(types[def.name.value] || []), ...collectInterfaceDirectives(def)]; + break; + case Kind.UNION_TYPE_DEFINITION: + types[def.name.value] = [...(types[def.name.value] || []), ...collectUnionDirectives(def)]; + break; + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + types[def.name.value] = [...(types[def.name.value] || []), ...collectInputObjectDirectives(def)]; + break; + case Kind.ENUM_TYPE_DEFINITION: + types[def.name.value] = [...(types[def.name.value] || []), ...collectEnumDirectives(def)]; + break; + case Kind.SCALAR_TYPE_DEFINITION: + types[def.name.value] = [...(types[def.name.value] || []), ...collectScalarDirectives(def)]; + break; } - return types; + } + return types; } export function collectObjectDirectives(node: ObjectTypeDefinitionNode): DirectiveNode[] { - let dirs = [] - for (const field of node.fields) { - const fieldDirs = collectFieldDirectives(field) - dirs = dirs.concat(fieldDirs) - } - return dirs.concat(node.directives) + let dirs = []; + for (const field of node.fields) { + const fieldDirs = collectFieldDirectives(field); + dirs = dirs.concat(fieldDirs); + } + return dirs.concat(node.directives); } export function collectInterfaceDirectives(node: InterfaceTypeDefinitionNode): DirectiveNode[] { - let dirs = [] - for (const field of node.fields) { - const fieldDirs = collectFieldDirectives(field) - dirs = dirs.concat(fieldDirs) - } - return dirs.concat(node.directives) + let dirs = []; + for (const field of node.fields) { + const fieldDirs = collectFieldDirectives(field); + dirs = dirs.concat(fieldDirs); + } + return dirs.concat(node.directives); } export function collectFieldDirectives(node: FieldDefinitionNode): DirectiveNode[] { - let dirs = [] - for (const arg of node.arguments) { - const argDirs = collectArgumentDirectives(arg) - dirs = dirs.concat(argDirs) - } - return dirs.concat(node.directives) + let dirs = []; + for (const arg of node.arguments) { + const argDirs = collectArgumentDirectives(arg); + dirs = dirs.concat(argDirs); + } + return dirs.concat(node.directives); } export function collectArgumentDirectives(node: InputValueDefinitionNode): DirectiveNode[] { - return [...(node.directives || [])] + return [...(node.directives || [])]; } export function collectUnionDirectives(node: UnionTypeDefinitionNode): DirectiveNode[] { - return [...(node.directives || [])] + return [...(node.directives || [])]; } export function collectScalarDirectives(node: ScalarTypeDefinitionNode): DirectiveNode[] { - return [...(node.directives || [])] + return [...(node.directives || [])]; } export function collectInputObjectDirectives(node: InputObjectTypeDefinitionNode): DirectiveNode[] { - let dirs = [] - for (const field of node.fields) { - const fieldDirs = collectArgumentDirectives(field) - dirs = dirs.concat(fieldDirs) - } - return dirs.concat(node.directives) + let dirs = []; + for (const field of node.fields) { + const fieldDirs = collectArgumentDirectives(field); + dirs = dirs.concat(fieldDirs); + } + return dirs.concat(node.directives); } export function collectEnumDirectives(node: EnumTypeDefinitionNode): DirectiveNode[] { - let dirs = [] - for (const val of node.values) { - const valDirs = collectEnumValueDirectives(val) - dirs = dirs.concat(valDirs) - } - return dirs.concat(node.directives) + let dirs = []; + for (const val of node.values) { + const valDirs = collectEnumValueDirectives(val); + dirs = dirs.concat(valDirs); + } + return dirs.concat(node.directives); } export function collectEnumValueDirectives(node: EnumValueDefinitionNode): DirectiveNode[] { - return [...(node.directives || [])] -} \ No newline at end of file + return [...(node.directives || [])]; +} diff --git a/packages/graphql-transformer-core/src/defaultSchema.ts b/packages/graphql-transformer-core/src/defaultSchema.ts index e2c37ff36e..899b77b6fd 100644 --- a/packages/graphql-transformer-core/src/defaultSchema.ts +++ b/packages/graphql-transformer-core/src/defaultSchema.ts @@ -1,41 +1,41 @@ import { SchemaDefinitionNode, Kind } from 'graphql'; const DEFAULT_SCHEMA_DEFINITION: SchemaDefinitionNode = { - kind: Kind.SCHEMA_DEFINITION, - directives: [], - operationTypes: [ - { - kind: Kind.OPERATION_TYPE_DEFINITION, - operation: 'query', - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: 'Query' - } - } + kind: Kind.SCHEMA_DEFINITION, + directives: [], + operationTypes: [ + { + kind: Kind.OPERATION_TYPE_DEFINITION, + operation: 'query', + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Query', }, - { - kind: Kind.OPERATION_TYPE_DEFINITION, - operation: 'mutation', - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: 'Mutation' - } - } + }, + }, + { + kind: Kind.OPERATION_TYPE_DEFINITION, + operation: 'mutation', + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Mutation', }, - { - kind: Kind.OPERATION_TYPE_DEFINITION, - operation: 'subscription', - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: 'Subscription' - } - } - } - ] -} + }, + }, + { + kind: Kind.OPERATION_TYPE_DEFINITION, + operation: 'subscription', + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Subscription', + }, + }, + }, + ], +}; export default DEFAULT_SCHEMA_DEFINITION; diff --git a/packages/graphql-transformer-core/src/errors.ts b/packages/graphql-transformer-core/src/errors.ts index f71c4da99d..1b58d496e9 100644 --- a/packages/graphql-transformer-core/src/errors.ts +++ b/packages/graphql-transformer-core/src/errors.ts @@ -1,27 +1,25 @@ -import { GraphQLError } from 'graphql' +import { GraphQLError } from 'graphql'; export class InvalidTransformerError extends Error { - - constructor(message: string) { - super(message); - Object.setPrototypeOf(this, InvalidTransformerError.prototype); - this.name = "InvalidTransformerError"; - if ((Error as any).captureStackTrace) { - (Error as any).captureStackTrace(this, InvalidTransformerError) - } + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, InvalidTransformerError.prototype); + this.name = 'InvalidTransformerError'; + if ((Error as any).captureStackTrace) { + (Error as any).captureStackTrace(this, InvalidTransformerError); } + } } export class SchemaValidationError extends Error { - - constructor(errors: GraphQLError[]) { - super(`Schema Errors:\n\n${errors.join('\n')}`); - Object.setPrototypeOf(this, SchemaValidationError.prototype); - this.name = "SchemaValidationError"; - if ((Error as any).captureStackTrace) { - (Error as any).captureStackTrace(this, SchemaValidationError) - } + constructor(errors: GraphQLError[]) { + super(`Schema Errors:\n\n${errors.join('\n')}`); + Object.setPrototypeOf(this, SchemaValidationError.prototype); + this.name = 'SchemaValidationError'; + if ((Error as any).captureStackTrace) { + (Error as any).captureStackTrace(this, SchemaValidationError); } + } } /** @@ -33,53 +31,52 @@ export class SchemaValidationError extends Error { * of an Int or BigInt type. */ export class TransformerContractError extends Error { - - constructor(message: string) { - super(message); - Object.setPrototypeOf(this, TransformerContractError.prototype); - this.name = "TransformerContractError"; - if ((Error as any).captureStackTrace) { - (Error as any).captureStackTrace(this, TransformerContractError) - } + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, TransformerContractError.prototype); + this.name = 'TransformerContractError'; + if ((Error as any).captureStackTrace) { + (Error as any).captureStackTrace(this, TransformerContractError); } + } } /** * Thrown by the sanity checker when a user is trying to make a migration that is known to not work. */ export class InvalidMigrationError extends Error { - fix: string; - cause: string; - constructor(message: string, cause: string, fix: string) { - super(message); - Object.setPrototypeOf(this, InvalidMigrationError.prototype); - this.name = "InvalidMigrationError"; - this.fix = fix; - this.cause = cause; - } + fix: string; + cause: string; + constructor(message: string, cause: string, fix: string) { + super(message); + Object.setPrototypeOf(this, InvalidMigrationError.prototype); + this.name = 'InvalidMigrationError'; + this.fix = fix; + this.cause = cause; + } } InvalidMigrationError.prototype.toString = function() { - return `${this.message}\nCause: ${this.cause}\nHow to fix: ${this.fix}`; -} + return `${this.message}\nCause: ${this.cause}\nHow to fix: ${this.fix}`; +}; export class InvalidDirectiveError extends Error { - constructor(message: string) { - super(message); - Object.setPrototypeOf(this, InvalidDirectiveError.prototype); - this.name = "InvalidDirectiveError"; - if ((Error as any).captureStackTrace) { - (Error as any).captureStackTrace(this, InvalidDirectiveError) - } + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, InvalidDirectiveError.prototype); + this.name = 'InvalidDirectiveError'; + if ((Error as any).captureStackTrace) { + (Error as any).captureStackTrace(this, InvalidDirectiveError); } + } } export class UnknownDirectiveError extends Error { - constructor(message: string) { - super(message); - Object.setPrototypeOf(this, UnknownDirectiveError.prototype); - this.name = "UnknownDirectiveError"; - if ((Error as any).captureStackTrace) { - (Error as any).captureStackTrace(this, UnknownDirectiveError) - } + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, UnknownDirectiveError.prototype); + this.name = 'UnknownDirectiveError'; + if ((Error as any).captureStackTrace) { + (Error as any).captureStackTrace(this, UnknownDirectiveError); } + } } diff --git a/packages/graphql-transformer-core/src/index.ts b/packages/graphql-transformer-core/src/index.ts index 04b6a4548f..dcfba27021 100644 --- a/packages/graphql-transformer-core/src/index.ts +++ b/packages/graphql-transformer-core/src/index.ts @@ -1,41 +1,41 @@ -import './polyfills/Object.assign' -import TransformerContext from './TransformerContext' -import Transformer from './Transformer' -import ITransformer from './ITransformer' -import GraphQLTransform from './GraphQLTransform' +import './polyfills/Object.assign'; +import TransformerContext from './TransformerContext'; +import Transformer from './Transformer'; +import ITransformer from './ITransformer'; +import GraphQLTransform from './GraphQLTransform'; import { collectDirectiveNames, collectDirectivesByTypeNames } from './collectDirectives'; -import { stripDirectives } from './stripDirectives' +import { stripDirectives } from './stripDirectives'; import { - buildProject as buildAPIProject, - uploadDeployment as uploadAPIProject, - migrateAPIProject, - revertAPIMigration, -} from './util/amplifyUtils' + buildProject as buildAPIProject, + uploadDeployment as uploadAPIProject, + migrateAPIProject, + revertAPIMigration, +} from './util/amplifyUtils'; import { - readSchema as readProjectSchema, - loadProject as readProjectConfiguration, - loadConfig as readTransformerConfiguration, - writeConfig as writeTransformerConfiguration, -} from './util/transformConfig' + readSchema as readProjectSchema, + loadProject as readProjectConfiguration, + loadConfig as readTransformerConfiguration, + writeConfig as writeTransformerConfiguration, +} from './util/transformConfig'; -export * from './errors' -export * from './util' +export * from './errors'; +export * from './util'; -export default GraphQLTransform +export default GraphQLTransform; export { - TransformerContext, - Transformer, - ITransformer, - collectDirectiveNames, - collectDirectivesByTypeNames, - stripDirectives, - buildAPIProject, - migrateAPIProject, - uploadAPIProject, - readProjectSchema, - readProjectConfiguration, - readTransformerConfiguration, - writeTransformerConfiguration, - revertAPIMigration, -} + TransformerContext, + Transformer, + ITransformer, + collectDirectiveNames, + collectDirectivesByTypeNames, + stripDirectives, + buildAPIProject, + migrateAPIProject, + uploadAPIProject, + readProjectSchema, + readProjectConfiguration, + readTransformerConfiguration, + writeTransformerConfiguration, + revertAPIMigration, +}; diff --git a/packages/graphql-transformer-core/src/polyfills/Object.assign.ts b/packages/graphql-transformer-core/src/polyfills/Object.assign.ts index 00b8a3ceb2..635f68f313 100644 --- a/packages/graphql-transformer-core/src/polyfills/Object.assign.ts +++ b/packages/graphql-transformer-core/src/polyfills/Object.assign.ts @@ -1,27 +1,27 @@ interface ObjectConstructor { - assign(target: any, ...sources: any[]): any; + assign(target: any, ...sources: any[]): any; } if (typeof Object.assign !== 'function') { - (function () { - Object.assign = function (target: any) { - 'use strict'; - if (target === undefined || target === null) { - throw new TypeError('Cannot convert undefined or null to object'); - } + (function() { + Object.assign = function(target: any) { + 'use strict'; + if (target === undefined || target === null) { + throw new TypeError('Cannot convert undefined or null to object'); + } - var output = Object(target); - for (var index = 1; index < arguments.length; index++) { - var source = arguments[index]; - if (source !== undefined && source !== null) { - for (var nextKey in source) { - if (source.hasOwnProperty(nextKey)) { - output[nextKey] = source[nextKey]; - } - } - } + var output = Object(target); + for (var index = 1; index < arguments.length; index++) { + var source = arguments[index]; + if (source !== undefined && source !== null) { + for (var nextKey in source) { + if (source.hasOwnProperty(nextKey)) { + output[nextKey] = source[nextKey]; } - return output; - }; - })(); -} \ No newline at end of file + } + } + } + return output; + }; + })(); +} diff --git a/packages/graphql-transformer-core/src/stripDirectives.ts b/packages/graphql-transformer-core/src/stripDirectives.ts index 19d8c67dd0..e51b80ee53 100644 --- a/packages/graphql-transformer-core/src/stripDirectives.ts +++ b/packages/graphql-transformer-core/src/stripDirectives.ts @@ -1,114 +1,123 @@ import { - ObjectTypeDefinitionNode, DirectiveNode, InterfaceTypeDefinitionNode, - UnionTypeDefinitionNode, ScalarTypeDefinitionNode, InputObjectTypeDefinitionNode, - FieldDefinitionNode, InputValueDefinitionNode, EnumValueDefinitionNode, EnumTypeDefinitionNode, - parse, - Kind, - DocumentNode -} from "graphql"; + ObjectTypeDefinitionNode, + DirectiveNode, + InterfaceTypeDefinitionNode, + UnionTypeDefinitionNode, + ScalarTypeDefinitionNode, + InputObjectTypeDefinitionNode, + FieldDefinitionNode, + InputValueDefinitionNode, + EnumValueDefinitionNode, + EnumTypeDefinitionNode, + parse, + Kind, + DocumentNode, +} from 'graphql'; export function stripDirectives(doc: DocumentNode, except: string[] = []): DocumentNode { - const definitions = [] - for (const def of doc.definitions) { - switch (def.kind) { - case Kind.OBJECT_TYPE_DEFINITION: - definitions.push(stripObjectDirectives(def)) - break; - case Kind.INTERFACE_TYPE_DEFINITION: - definitions.push(stripInterfaceDirectives(def)) - break; - case Kind.UNION_TYPE_DEFINITION: - definitions.push(stripUnionDirectives(def)) - break; - case Kind.INPUT_OBJECT_TYPE_DEFINITION: - definitions.push(stripInputObjectDirectives(def)) - break; - case Kind.ENUM_TYPE_DEFINITION: - definitions.push(stripEnumDirectives(def)) - break; - case Kind.SCALAR_TYPE_DEFINITION: - definitions.push(stripScalarDirectives(def)) - break; - } + const definitions = []; + for (const def of doc.definitions) { + switch (def.kind) { + case Kind.OBJECT_TYPE_DEFINITION: + definitions.push(stripObjectDirectives(def)); + break; + case Kind.INTERFACE_TYPE_DEFINITION: + definitions.push(stripInterfaceDirectives(def)); + break; + case Kind.UNION_TYPE_DEFINITION: + definitions.push(stripUnionDirectives(def)); + break; + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + definitions.push(stripInputObjectDirectives(def)); + break; + case Kind.ENUM_TYPE_DEFINITION: + definitions.push(stripEnumDirectives(def)); + break; + case Kind.SCALAR_TYPE_DEFINITION: + definitions.push(stripScalarDirectives(def)); + break; } + } - function excepted(dir: DirectiveNode) { return Boolean(except.find(f => dir.name.value === f))} + function excepted(dir: DirectiveNode) { + return Boolean(except.find(f => dir.name.value === f)); + } - function stripObjectDirectives(node: ObjectTypeDefinitionNode): ObjectTypeDefinitionNode { - const fields = node.fields ? node.fields.map(stripFieldDirectives) : node.fields - return { - ...node, - fields, - directives: node.directives.filter(excepted) - } - } - - function stripInterfaceDirectives(node: InterfaceTypeDefinitionNode): InterfaceTypeDefinitionNode { - const fields = node.fields ? node.fields.map(stripFieldDirectives) : node.fields - return { - ...node, - fields, - directives: node.directives.filter(excepted) - } - } + function stripObjectDirectives(node: ObjectTypeDefinitionNode): ObjectTypeDefinitionNode { + const fields = node.fields ? node.fields.map(stripFieldDirectives) : node.fields; + return { + ...node, + fields, + directives: node.directives.filter(excepted), + }; + } - function stripFieldDirectives(node: FieldDefinitionNode): FieldDefinitionNode { - const args = node.arguments ? node.arguments.map(stripArgumentDirectives) : node.arguments - return { - ...node, - arguments: args, - directives: node.directives.filter(excepted) - } - } + function stripInterfaceDirectives(node: InterfaceTypeDefinitionNode): InterfaceTypeDefinitionNode { + const fields = node.fields ? node.fields.map(stripFieldDirectives) : node.fields; + return { + ...node, + fields, + directives: node.directives.filter(excepted), + }; + } - function stripArgumentDirectives(node: InputValueDefinitionNode): InputValueDefinitionNode { - return { - ...node, - directives: node.directives.filter(excepted) - } - } + function stripFieldDirectives(node: FieldDefinitionNode): FieldDefinitionNode { + const args = node.arguments ? node.arguments.map(stripArgumentDirectives) : node.arguments; + return { + ...node, + arguments: args, + directives: node.directives.filter(excepted), + }; + } - function stripUnionDirectives(node: UnionTypeDefinitionNode): UnionTypeDefinitionNode { - return { - ...node, - directives: node.directives.filter(excepted) - } - } + function stripArgumentDirectives(node: InputValueDefinitionNode): InputValueDefinitionNode { + return { + ...node, + directives: node.directives.filter(excepted), + }; + } - function stripScalarDirectives(node: ScalarTypeDefinitionNode): ScalarTypeDefinitionNode { - return { - ...node, - directives: node.directives.filter(excepted) - } - } + function stripUnionDirectives(node: UnionTypeDefinitionNode): UnionTypeDefinitionNode { + return { + ...node, + directives: node.directives.filter(excepted), + }; + } - function stripInputObjectDirectives(node: InputObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - const fields = node.fields ? node.fields.map(stripArgumentDirectives) : node.fields - return { - ...node, - fields, - directives: node.directives.filter(excepted) - } - } + function stripScalarDirectives(node: ScalarTypeDefinitionNode): ScalarTypeDefinitionNode { + return { + ...node, + directives: node.directives.filter(excepted), + }; + } - function stripEnumDirectives(node: EnumTypeDefinitionNode): EnumTypeDefinitionNode { - const values = node.values ? node.values.map(stripEnumValueDirectives) : node.values - return { - ...node, - values, - directives: node.directives.filter(excepted) - } - } + function stripInputObjectDirectives(node: InputObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { + const fields = node.fields ? node.fields.map(stripArgumentDirectives) : node.fields; + return { + ...node, + fields, + directives: node.directives.filter(excepted), + }; + } - function stripEnumValueDirectives(node: EnumValueDefinitionNode): EnumValueDefinitionNode { - return { - ...node, - directives: node.directives.filter(excepted) - } - } + function stripEnumDirectives(node: EnumTypeDefinitionNode): EnumTypeDefinitionNode { + const values = node.values ? node.values.map(stripEnumValueDirectives) : node.values; + return { + ...node, + values, + directives: node.directives.filter(excepted), + }; + } + function stripEnumValueDirectives(node: EnumValueDefinitionNode): EnumValueDefinitionNode { return { - kind: Kind.DOCUMENT, - definitions - } + ...node, + directives: node.directives.filter(excepted), + }; + } + + return { + kind: Kind.DOCUMENT, + definitions, + }; } diff --git a/packages/graphql-transformer-core/src/util/SchemaResourceUtil.ts b/packages/graphql-transformer-core/src/util/SchemaResourceUtil.ts index db39fb3fee..da22fbe3d0 100644 --- a/packages/graphql-transformer-core/src/util/SchemaResourceUtil.ts +++ b/packages/graphql-transformer-core/src/util/SchemaResourceUtil.ts @@ -1,117 +1,96 @@ -import AppSync from 'cloudform-types/types/appSync' -import Template from 'cloudform-types/types/template' -import { Fn, StringParameter } from 'cloudform-types' -import { ResourceConstants } from 'graphql-transformer-common' -import Resource from "cloudform-types/types/resource"; +import AppSync from 'cloudform-types/types/appSync'; +import Template from 'cloudform-types/types/template'; +import { Fn, StringParameter } from 'cloudform-types'; +import { ResourceConstants } from 'graphql-transformer-common'; +import Resource from 'cloudform-types/types/resource'; import Parameter from 'cloudform-types/types/parameter'; -const RESOLVERS_DIRECTORY_NAME = "resolvers" -const STACKS_DIRECTORY_NAME = "stacks" +const RESOLVERS_DIRECTORY_NAME = 'resolvers'; +const STACKS_DIRECTORY_NAME = 'stacks'; export class SchemaResourceUtil { + public makeResolverS3RootParams(): Template { + return { + Parameters: { + [ResourceConstants.PARAMETERS.Env]: new StringParameter({ + Description: `The environment name. e.g. Dev, Test, or Production`, + Default: ResourceConstants.NONE, + }), + [ResourceConstants.PARAMETERS.S3DeploymentBucket]: new StringParameter({ + Description: 'The S3 bucket containing all deployment assets for the project.', + }), + [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: new StringParameter({ + Description: 'An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory.', + }), + }, + }; + } - public makeResolverS3RootParams(): Template { - return { - Parameters: { - [ResourceConstants.PARAMETERS.Env]: new StringParameter({ - Description: `The environment name. e.g. Dev, Test, or Production`, - Default: ResourceConstants.NONE - }), - [ResourceConstants.PARAMETERS.S3DeploymentBucket]: new StringParameter({ - Description: 'The S3 bucket containing all deployment assets for the project.' - }), - [ResourceConstants.PARAMETERS.S3DeploymentRootKey]: new StringParameter({ - Description: 'An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory.' - }) - } - } - } + public makeEnvironmentConditions() { + return { + [ResourceConstants.CONDITIONS.HasEnvironmentParameter]: Fn.Not( + Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.Env), ResourceConstants.NONE) + ), + }; + } - public makeEnvironmentConditions() { - return { - [ResourceConstants.CONDITIONS.HasEnvironmentParameter]: - Fn.Not(Fn.Equals(Fn.Ref(ResourceConstants.PARAMETERS.Env), ResourceConstants.NONE)) - } - } + public updateResolverResource(resource: Resource) { + resource.Properties.RequestMappingTemplateS3Location = Fn.Sub( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}', + { + S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + ResolverFileName: Fn.Join('.', [resource.Properties.TypeName, resource.Properties.FieldName, 'req', 'vtl']), + } + ); + resource.Properties.ResponseMappingTemplateS3Location = Fn.Sub( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}', + { + S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + ResolverFileName: Fn.Join('.', [resource.Properties.TypeName, resource.Properties.FieldName, 'res', 'vtl']), + } + ); + delete resource.Properties.RequestMappingTemplate; + delete resource.Properties.ResponseMappingTemplate; + return resource; + } - public updateResolverResource(resource: Resource) { - resource.Properties.RequestMappingTemplateS3Location = Fn.Sub( - "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}", - { - S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - ResolverFileName: Fn.Join('.', [ - resource.Properties.TypeName, - resource.Properties.FieldName, - 'req', - 'vtl', - ]) - } - ); - resource.Properties.ResponseMappingTemplateS3Location = Fn.Sub( - "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}", - { - S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - ResolverFileName: Fn.Join('.', [ - resource.Properties.TypeName, - resource.Properties.FieldName, - 'res', - 'vtl' - ]) - } - ); - delete resource.Properties.RequestMappingTemplate; - delete resource.Properties.ResponseMappingTemplate; - return resource; - } - - public updateFunctionConfigurationResource(resource: Resource) { - resource.Properties.RequestMappingTemplateS3Location = Fn.Sub( - "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}", - { - S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - ResolverFileName: Fn.Join('.', [ - resource.Properties.Name, - 'req', - 'vtl', - ]) - } - ); - resource.Properties.ResponseMappingTemplateS3Location = Fn.Sub( - "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}", - { - S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - ResolverFileName: Fn.Join('.', [ - resource.Properties.Name, - 'res', - 'vtl' - ]) - } - ); - delete resource.Properties.RequestMappingTemplate; - delete resource.Properties.ResponseMappingTemplate; - return resource; - } + public updateFunctionConfigurationResource(resource: Resource) { + resource.Properties.RequestMappingTemplateS3Location = Fn.Sub( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}', + { + S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + ResolverFileName: Fn.Join('.', [resource.Properties.Name, 'req', 'vtl']), + } + ); + resource.Properties.ResponseMappingTemplateS3Location = Fn.Sub( + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/pipelineFunctions/${ResolverFileName}', + { + S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + ResolverFileName: Fn.Join('.', [resource.Properties.Name, 'res', 'vtl']), + } + ); + delete resource.Properties.RequestMappingTemplate; + delete resource.Properties.ResponseMappingTemplate; + return resource; + } - public makeAppSyncSchema(schema?: string) { - if (schema) { - return new AppSync.GraphQLSchema({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - Definition: schema - }) - } - return new AppSync.GraphQLSchema({ - ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - DefinitionS3Location: Fn.Sub( - "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/schema.graphql", - { - S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey) - } - ) - }) + public makeAppSyncSchema(schema?: string) { + if (schema) { + return new AppSync.GraphQLSchema({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + Definition: schema, + }); } + return new AppSync.GraphQLSchema({ + ApiId: Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + DefinitionS3Location: Fn.Sub('s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/schema.graphql', { + S3DeploymentBucket: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + S3DeploymentRootKey: Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + }), + }); + } } diff --git a/packages/graphql-transformer-core/src/util/amplifyUtils.ts b/packages/graphql-transformer-core/src/util/amplifyUtils.ts index 5505b9f981..4e5a3e5dd3 100644 --- a/packages/graphql-transformer-core/src/util/amplifyUtils.ts +++ b/packages/graphql-transformer-core/src/util/amplifyUtils.ts @@ -1,6 +1,6 @@ const fs = require('fs-extra'); import * as path from 'path'; -import { CloudFormation, Fn, Template } from "cloudform-types"; +import { CloudFormation, Fn, Template } from 'cloudform-types'; import GraphQLTransform from '..'; import DeploymentResources from '../DeploymentResources'; import { StackMapping } from '../GraphQLTransform'; @@ -13,48 +13,47 @@ const CLOUDFORMATION_FILE_NAME = 'cloudformation-template.json'; const PARAMETERS_FILE_NAME = 'parameters.json'; export interface ProjectOptions { - projectDirectory?: string - transformersFactory: Function, - transformersFactoryArgs: object[], - currentCloudBackendDirectory: string - rootStackFileName?: string - dryRun?: boolean, - disableResolverOverrides?: boolean, - buildParameters?: Object, + projectDirectory?: string; + transformersFactory: Function; + transformersFactoryArgs: object[]; + currentCloudBackendDirectory: string; + rootStackFileName?: string; + dryRun?: boolean; + disableResolverOverrides?: boolean; + buildParameters?: Object; } export async function buildProject(opts: ProjectOptions) { - await ensureMissingStackMappings(opts); + await ensureMissingStackMappings(opts); - const builtProject = await _buildProject(opts); + const builtProject = await _buildProject(opts); - if (opts.projectDirectory && !opts.dryRun) { - await writeDeploymentToDisk(builtProject, path.join(opts.projectDirectory, 'build'), - opts.rootStackFileName, opts.buildParameters); - if (opts.currentCloudBackendDirectory) { - const lastBuildPath = path.join(opts.currentCloudBackendDirectory, 'build'); - const thisBuildPath = path.join(opts.projectDirectory, 'build'); - await Sanity.check(lastBuildPath, thisBuildPath, opts.rootStackFileName); - } + if (opts.projectDirectory && !opts.dryRun) { + await writeDeploymentToDisk(builtProject, path.join(opts.projectDirectory, 'build'), opts.rootStackFileName, opts.buildParameters); + if (opts.currentCloudBackendDirectory) { + const lastBuildPath = path.join(opts.currentCloudBackendDirectory, 'build'); + const thisBuildPath = path.join(opts.projectDirectory, 'build'); + await Sanity.check(lastBuildPath, thisBuildPath, opts.rootStackFileName); } - return builtProject; + } + return builtProject; } async function _buildProject(opts: ProjectOptions) { - const userProjectConfig = await loadProject(opts.projectDirectory, opts) - const stackMapping = getStackMappingFromProjectConfig(userProjectConfig.config); - // Create the transformer instances, we've to make sure we're not reusing them within the same CLI command - // because the StackMapping feature already builds the project once. - const transformers = opts.transformersFactory(...opts.transformersFactoryArgs); - const transform = new GraphQLTransform({ - transformers, - stackMapping - }); - let transformOutput = transform.transform(userProjectConfig.schema.toString()); - if (userProjectConfig.config && userProjectConfig.config.Migration) { - transformOutput = adjustBuildForMigration(transformOutput, userProjectConfig.config.Migration); - } - const merged = mergeUserConfigWithTransformOutput(userProjectConfig, transformOutput) - return merged; + const userProjectConfig = await loadProject(opts.projectDirectory, opts); + const stackMapping = getStackMappingFromProjectConfig(userProjectConfig.config); + // Create the transformer instances, we've to make sure we're not reusing them within the same CLI command + // because the StackMapping feature already builds the project once. + const transformers = opts.transformersFactory(...opts.transformersFactoryArgs); + const transform = new GraphQLTransform({ + transformers, + stackMapping, + }); + let transformOutput = transform.transform(userProjectConfig.schema.toString()); + if (userProjectConfig.config && userProjectConfig.config.Migration) { + transformOutput = adjustBuildForMigration(transformOutput, userProjectConfig.config.Migration); + } + const merged = mergeUserConfigWithTransformOutput(userProjectConfig, transformOutput); + return merged; } /** @@ -63,15 +62,15 @@ async function _buildProject(opts: ProjectOptions) { * to remain in the top level stack. */ function getStackMappingFromProjectConfig(config?: TransformConfig): StackMapping { - const stackMapping = getOrDefault(config, 'StackMapping', {}); - const migrationConfig = config.Migration; - if (migrationConfig && migrationConfig.V1) { - const resourceIdsToHoist = migrationConfig.V1.Resources || []; - for (const idToHoist of resourceIdsToHoist) { - stackMapping[idToHoist] = 'root'; - } + const stackMapping = getOrDefault(config, 'StackMapping', {}); + const migrationConfig = config.Migration; + if (migrationConfig && migrationConfig.V1) { + const resourceIdsToHoist = migrationConfig.V1.Resources || []; + for (const idToHoist of resourceIdsToHoist) { + stackMapping[idToHoist] = 'root'; } - return stackMapping; + } + return stackMapping; } /** @@ -82,32 +81,32 @@ function getStackMappingFromProjectConfig(config?: TransformConfig): StackMappin * @param idsToHoist The logical ids to hoist into the root of the template. */ function adjustBuildForMigration(resources: DeploymentResources, migrationConfig?: TransformMigrationConfig): DeploymentResources { - if (migrationConfig && migrationConfig.V1) { - const resourceIdsToHoist = migrationConfig.V1.Resources || []; - if (resourceIdsToHoist.length === 0) { - return resources; - } - const resourceIdMap = resourceIdsToHoist.reduce((acc: any, k: string) => ({ ...acc, [k]: true}), {}); - for (const stackKey of Object.keys(resources.stacks)) { - const template = resources.stacks[stackKey]; - for (const resourceKey of Object.keys(template.Resources)) { - if (resourceIdMap[resourceKey]) { - // Handle any special detials for migrated details. - const resource = template.Resources[resourceKey]; - template.Resources[resourceKey] = formatMigratedResource(resource); - } - } - } - const rootStack = resources.rootStack; - for (const resourceKey of Object.keys(rootStack.Resources)) { - if (resourceIdMap[resourceKey]) { - // Handle any special detials for migrated details. - const resource = rootStack.Resources[resourceKey]; - rootStack.Resources[resourceKey] = formatMigratedResource(resource); - } + if (migrationConfig && migrationConfig.V1) { + const resourceIdsToHoist = migrationConfig.V1.Resources || []; + if (resourceIdsToHoist.length === 0) { + return resources; + } + const resourceIdMap = resourceIdsToHoist.reduce((acc: any, k: string) => ({ ...acc, [k]: true }), {}); + for (const stackKey of Object.keys(resources.stacks)) { + const template = resources.stacks[stackKey]; + for (const resourceKey of Object.keys(template.Resources)) { + if (resourceIdMap[resourceKey]) { + // Handle any special detials for migrated details. + const resource = template.Resources[resourceKey]; + template.Resources[resourceKey] = formatMigratedResource(resource); } + } + } + const rootStack = resources.rootStack; + for (const resourceKey of Object.keys(rootStack.Resources)) { + if (resourceIdMap[resourceKey]) { + // Handle any special detials for migrated details. + const resource = rootStack.Resources[resourceKey]; + rootStack.Resources[resourceKey] = formatMigratedResource(resource); + } } - return resources; + } + return resources; } /** @@ -118,278 +117,275 @@ function adjustBuildForMigration(resources: DeploymentResources, migrationConfig * working without changes. */ async function ensureMissingStackMappings(config: ProjectOptions) { - const { currentCloudBackendDirectory } = config; - let transformOutput = undefined; - - if (currentCloudBackendDirectory) { - const missingStackMappings = {}; - transformOutput = await _buildProject(config); - const copyOfCloudBackend = await readFromPath(currentCloudBackendDirectory); - const stackMapping = transformOutput.stackMapping; - if (copyOfCloudBackend && copyOfCloudBackend.build && copyOfCloudBackend.build.stacks) { - // leave the custom stack alone. Don't split them into separate stacks - const customStacks = Object.keys(copyOfCloudBackend.stacks || {}); - const stackNames = Object.keys(copyOfCloudBackend.build.stacks).filter( - stack => !customStacks.includes(stack) - ); - - // We walk through each of the stacks that were deployed in the most recent deployment. - // If we find a resource that was deployed into a different stack than it should have - // we make a note of it and include it in the missing stack mapping. - for (const stackFileName of stackNames) { - const stackName = stackFileName.slice(0, stackFileName.length - path.extname(stackFileName).length); - const lastDeployedStack = JSON.parse(copyOfCloudBackend.build.stacks[stackFileName]); - if (lastDeployedStack) { - const resourceIdsInStack = Object.keys(lastDeployedStack.Resources); - for (const resourceId of resourceIdsInStack) { - if (stackMapping[resourceId] && stackName !== stackMapping[resourceId]) { - missingStackMappings[resourceId] = stackName; - } - } - const outputIdsInStack = Object.keys(lastDeployedStack.Outputs); - for (const outputId of outputIdsInStack) { - if (stackMapping[outputId] && stackName !== stackMapping[outputId]) { - missingStackMappings[outputId] = stackName; - } - } - } + const { currentCloudBackendDirectory } = config; + let transformOutput = undefined; + + if (currentCloudBackendDirectory) { + const missingStackMappings = {}; + transformOutput = await _buildProject(config); + const copyOfCloudBackend = await readFromPath(currentCloudBackendDirectory); + const stackMapping = transformOutput.stackMapping; + if (copyOfCloudBackend && copyOfCloudBackend.build && copyOfCloudBackend.build.stacks) { + // leave the custom stack alone. Don't split them into separate stacks + const customStacks = Object.keys(copyOfCloudBackend.stacks || {}); + const stackNames = Object.keys(copyOfCloudBackend.build.stacks).filter(stack => !customStacks.includes(stack)); + + // We walk through each of the stacks that were deployed in the most recent deployment. + // If we find a resource that was deployed into a different stack than it should have + // we make a note of it and include it in the missing stack mapping. + for (const stackFileName of stackNames) { + const stackName = stackFileName.slice(0, stackFileName.length - path.extname(stackFileName).length); + const lastDeployedStack = JSON.parse(copyOfCloudBackend.build.stacks[stackFileName]); + if (lastDeployedStack) { + const resourceIdsInStack = Object.keys(lastDeployedStack.Resources); + for (const resourceId of resourceIdsInStack) { + if (stackMapping[resourceId] && stackName !== stackMapping[resourceId]) { + missingStackMappings[resourceId] = stackName; } - - // We then do the same thing with the root stack. - const lastDeployedStack = JSON.parse(copyOfCloudBackend.build[config.rootStackFileName]); - const resourceIdsInStack = Object.keys(lastDeployedStack.Resources); - for (const resourceId of resourceIdsInStack) { - if (stackMapping[resourceId] && 'root' !== stackMapping[resourceId]) { - missingStackMappings[resourceId] = 'root'; - } - } - const outputIdsInStack = Object.keys(lastDeployedStack.Outputs); - for (const outputId of outputIdsInStack) { - if (stackMapping[outputId] && 'root' !== stackMapping[outputId]) { - missingStackMappings[outputId] = 'root'; - } - }; - // If there are missing stack mappings, we write them to disk. - if (Object.keys(missingStackMappings).length) { - let conf = await loadConfig(config.projectDirectory); - conf = { ...conf, StackMapping: { ...getOrDefault(conf, 'StackMapping', {}), ...missingStackMappings } }; - await writeConfig(config.projectDirectory, conf); + } + const outputIdsInStack = Object.keys(lastDeployedStack.Outputs); + for (const outputId of outputIdsInStack) { + if (stackMapping[outputId] && stackName !== stackMapping[outputId]) { + missingStackMappings[outputId] = stackName; } + } + } + } + + // We then do the same thing with the root stack. + const lastDeployedStack = JSON.parse(copyOfCloudBackend.build[config.rootStackFileName]); + const resourceIdsInStack = Object.keys(lastDeployedStack.Resources); + for (const resourceId of resourceIdsInStack) { + if (stackMapping[resourceId] && 'root' !== stackMapping[resourceId]) { + missingStackMappings[resourceId] = 'root'; } + } + const outputIdsInStack = Object.keys(lastDeployedStack.Outputs); + for (const outputId of outputIdsInStack) { + if (stackMapping[outputId] && 'root' !== stackMapping[outputId]) { + missingStackMappings[outputId] = 'root'; + } + } + // If there are missing stack mappings, we write them to disk. + if (Object.keys(missingStackMappings).length) { + let conf = await loadConfig(config.projectDirectory); + conf = { ...conf, StackMapping: { ...getOrDefault(conf, 'StackMapping', {}), ...missingStackMappings } }; + await writeConfig(config.projectDirectory, conf); + } } + } - return transformOutput; + return transformOutput; } /** * Merge user config on top of transform output when needed. */ -function mergeUserConfigWithTransformOutput( - userConfig: Partial, - transformOutput: DeploymentResources -) { - // Override user defined resolvers. - const userResolvers = userConfig.resolvers || {}; - const transformResolvers = transformOutput.resolvers; - for (const userResolver of Object.keys(userResolvers)) { - transformResolvers[userResolver] = userConfig.resolvers[userResolver] +function mergeUserConfigWithTransformOutput(userConfig: Partial, transformOutput: DeploymentResources) { + // Override user defined resolvers. + const userResolvers = userConfig.resolvers || {}; + const transformResolvers = transformOutput.resolvers; + for (const userResolver of Object.keys(userResolvers)) { + transformResolvers[userResolver] = userConfig.resolvers[userResolver]; + } + + // Override user defined stacks. + const userStacks = userConfig.stacks || {}; + const transformStacks = transformOutput.stacks; + const rootStack = transformOutput.rootStack; + + // Get all the transform stacks. Custom stacks will depend on all of them + // so they can always access data sources created by the transform. + const resourceTypesToDependOn = { + 'AWS::CloudFormation::Stack': true, + 'AWS::AppSync::GraphQLApi': true, + 'AWS::AppSync::GraphQLSchema': true, + }; + const allResourceIds = Object.keys(rootStack.Resources).filter((k: string) => { + const resource = rootStack.Resources[k]; + return resourceTypesToDependOn[resource.Type]; + }); + // Looping through the parameters defined by the transform (aka. rootStack) + const parametersKeys = Object.keys(rootStack.Parameters); + const customStackParams = parametersKeys.reduce( + (acc: any, k: string) => ({ + ...acc, + [k]: Fn.Ref(k), + }), + {} + ); + // customStackParams is a map that will be passed as the "parameters" value + // to any nested stacks. + customStackParams[ResourceConstants.PARAMETERS.AppSyncApiId] = Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'); + + // Load the root stack's parameters as we will update them with the Child Stack's parameters + // if they are not already present in the root stack. + let updatedParameters = rootStack.Parameters; + + for (const userStack of Object.keys(userStacks)) { + if (transformOutput.stacks[userStack]) { + throw new Error(`You cannot provide a stack named ${userStack} as it \ + will be overwritten by a stack generated by the GraphQL Transform.`); } - - // Override user defined stacks. - const userStacks = userConfig.stacks || {}; - const transformStacks = transformOutput.stacks; - const rootStack = transformOutput.rootStack; - - // Get all the transform stacks. Custom stacks will depend on all of them - // so they can always access data sources created by the transform. - const resourceTypesToDependOn = { - "AWS::CloudFormation::Stack": true, - "AWS::AppSync::GraphQLApi": true, - "AWS::AppSync::GraphQLSchema": true, - }; - const allResourceIds = Object.keys(rootStack.Resources).filter( - (k: string) => { - const resource = rootStack.Resources[k]; - return resourceTypesToDependOn[resource.Type]; - } - ); - // Looping through the parameters defined by the transform (aka. rootStack) - const parametersKeys = Object.keys(rootStack.Parameters); - const customStackParams = parametersKeys.reduce((acc: any, k: string) => ({ - ...acc, - [k]: Fn.Ref(k) - }), {}) - // customStackParams is a map that will be passed as the "parameters" value - // to any nested stacks. - customStackParams[ResourceConstants.PARAMETERS.AppSyncApiId] = Fn.GetAtt( - ResourceConstants.RESOURCES.GraphQLAPILogicalID, - 'ApiId' - ); - - // Load the root stack's parameters as we will update them with the Child Stack's parameters - // if they are not already present in the root stack. - let updatedParameters = rootStack.Parameters; - - for (const userStack of Object.keys(userStacks)) { - if (transformOutput.stacks[userStack]) { - throw new Error(`You cannot provide a stack named ${userStack} as it \ - will be overwritten by a stack generated by the GraphQL Transform.`) - } - const userDefinedStack = userConfig.stacks[userStack]; - + const userDefinedStack = userConfig.stacks[userStack]; + + /** + * First loop through the parameters in the user defined stack and see + * if there are any Parameters that are present in the child but not the + * root stack - if so, add it to the root stack. + */ + for (const key of Object.keys(userDefinedStack.Parameters)) { + if (customStackParams[key] == null) { + customStackParams[key] = Fn.Ref(key); /** - * First loop through the parameters in the user defined stack and see - * if there are any Parameters that are present in the child but not the - * root stack - if so, add it to the root stack. + * First check to the ensure that the key does not already exist in the Root stack + * This helps to prevent the customer from overwriting parameters that are used by the library */ - for (const key of Object.keys(userDefinedStack.Parameters)) { - if (customStackParams[key] == null) { - customStackParams[key] = Fn.Ref(key); - /** - * First check to the ensure that the key does not already exist in the Root stack - * This helps to prevent the customer from overwriting parameters that are used by the library - */ - if (updatedParameters[key]) { - throw new Error(`Cannot redefine CloudFormation parameter ${key} in stack ${userStack}.`); - } else { - // Add the entire parameter entry from the user defined stack's parameter - updatedParameters[key] = userDefinedStack.Parameters[key]; - } - } + if (updatedParameters[key]) { + throw new Error(`Cannot redefine CloudFormation parameter ${key} in stack ${userStack}.`); + } else { + // Add the entire parameter entry from the user defined stack's parameter + updatedParameters[key] = userDefinedStack.Parameters[key]; } - // Providing a parameter value when the parameters is not explicitly defined - // in the template causes CloudFormation to throw an error. This will only - // provide the value to the nested stack if the user has specified it. - const parametersForStack = Object.keys(userDefinedStack.Parameters).reduce((acc, k) => ({ - ...acc, - [k]: customStackParams[k], - }), {}); - - transformStacks[userStack] = userDefinedStack; - // Split on non alphabetic characters to make a valid resource id. - const stackResourceId = userStack.split(/[^A-Za-z]/).join(''); - const customNestedStack = new CloudFormation.Stack({ - Parameters: parametersForStack, - TemplateURL: Fn.Join( - '/', - [ - "https://s3.amazonaws.com", - Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), - Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), - 'stacks', - userStack - ] - ) - }).dependsOn(allResourceIds); - rootStack.Resources[stackResourceId] = customNestedStack; + } } + // Providing a parameter value when the parameters is not explicitly defined + // in the template causes CloudFormation to throw an error. This will only + // provide the value to the nested stack if the user has specified it. + const parametersForStack = Object.keys(userDefinedStack.Parameters).reduce( + (acc, k) => ({ + ...acc, + [k]: customStackParams[k], + }), + {} + ); - // Update the Root Stack Params since we have added the Child Stack Params if they are missing. - rootStack.Parameters = updatedParameters; - return { - ...transformOutput, - resolvers: transformResolvers, - stacks: transformStacks - } + transformStacks[userStack] = userDefinedStack; + // Split on non alphabetic characters to make a valid resource id. + const stackResourceId = userStack.split(/[^A-Za-z]/).join(''); + const customNestedStack = new CloudFormation.Stack({ + Parameters: parametersForStack, + TemplateURL: Fn.Join('/', [ + 'https://s3.amazonaws.com', + Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentBucket), + Fn.Ref(ResourceConstants.PARAMETERS.S3DeploymentRootKey), + 'stacks', + userStack, + ]), + }).dependsOn(allResourceIds); + rootStack.Resources[stackResourceId] = customNestedStack; + } + + // Update the Root Stack Params since we have added the Child Stack Params if they are missing. + rootStack.Parameters = updatedParameters; + return { + ...transformOutput, + resolvers: transformResolvers, + stacks: transformStacks, + }; } export interface UploadOptions { - directory: string, - upload(blob: { Key: string, Body: Buffer | string}): Promise + directory: string; + upload(blob: { Key: string; Body: Buffer | string }): Promise; } /** * Reads deployment assets from disk and uploads to the cloud via an uploader. * @param opts Deployment options. */ export async function uploadDeployment(opts: UploadOptions) { - try { - if (!opts.directory) { - throw new Error(`You must provide a 'directory'`) - } else if (!fs.existsSync(opts.directory)) { - throw new Error(`Invalid 'directory': directory does not exist at ${opts.directory}`) - } - if (!opts.upload || typeof opts.upload !== 'function') { - throw new Error(`You must provide an 'upload' function`) - } - await walkDirPosix(opts.directory, opts.upload) - } catch (e) { - throw e + try { + if (!opts.directory) { + throw new Error(`You must provide a 'directory'`); + } else if (!fs.existsSync(opts.directory)) { + throw new Error(`Invalid 'directory': directory does not exist at ${opts.directory}`); } + if (!opts.upload || typeof opts.upload !== 'function') { + throw new Error(`You must provide an 'upload' function`); + } + await walkDirPosix(opts.directory, opts.upload); + } catch (e) { + throw e; + } } /** * Writes a deployment to disk at a path. */ -async function writeDeploymentToDisk(deployment: DeploymentResources, directory: string, - rootStackFileName: string = 'rootStack.json', buildParameters: Object) { - - // Delete the last deployments resources. - await emptyDirectory(directory) - - // Write the schema to disk - const schema = deployment.schema; - const fullSchemaPath = path.normalize(directory + `/schema.graphql`) - fs.writeFileSync(fullSchemaPath, schema) - - // Setup the directories if they do not exist. - initStacksAndResolversDirectories(directory); - - // Write resolvers to disk - const resolverFileNames = Object.keys(deployment.resolvers); - const resolverRootPath = resolverDirectoryPath(directory) - for (const resolverFileName of resolverFileNames) { - const fullResolverPath = path.normalize(resolverRootPath + '/' + resolverFileName); - fs.writeFileSync(fullResolverPath, deployment.resolvers[resolverFileName]); - } - - // Write pipeline resolvers to disk - const pipelineFunctions = Object.keys(deployment.pipelineFunctions); - const pipelineFunctionRootPath = pipelineFunctionDirectoryPath(directory) - for (const functionFileName of pipelineFunctions) { - const fullTemplatePath = path.normalize(pipelineFunctionRootPath + '/' + functionFileName); - fs.writeFileSync(fullTemplatePath, deployment.pipelineFunctions[functionFileName]); - } - - // Write the stacks to disk - const stackNames = Object.keys(deployment.stacks); - const stackRootPath = stacksDirectoryPath(directory) - for (const stackFileName of stackNames) { - const fileNameParts = stackFileName.split('.'); - if (fileNameParts.length === 1) { - fileNameParts.push('json') - } - const fullFileName = fileNameParts.join('.'); - throwIfNotJSONExt(fullFileName); - const fullStackPath = path.normalize(stackRootPath + '/' + fullFileName); - let stackString: any = deployment.stacks[stackFileName]; - stackString = typeof stackString === 'string' ? deployment.stacks[stackFileName] : JSON.stringify(deployment.stacks[stackFileName], null, 4); - fs.writeFileSync(fullStackPath, stackString); - } - - // Write any functions to disk - const functionNames = Object.keys(deployment.functions); - const functionRootPath = path.normalize(directory + `/functions`) - if (!fs.existsSync(functionRootPath)) { - fs.mkdirSync(functionRootPath); - } - for (const functionName of functionNames) { - const fullFunctionPath = path.normalize(functionRootPath + '/' + functionName); - const zipContents = fs.readFileSync(deployment.functions[functionName]) - fs.writeFileSync(fullFunctionPath, zipContents); +async function writeDeploymentToDisk( + deployment: DeploymentResources, + directory: string, + rootStackFileName: string = 'rootStack.json', + buildParameters: Object +) { + // Delete the last deployments resources. + await emptyDirectory(directory); + + // Write the schema to disk + const schema = deployment.schema; + const fullSchemaPath = path.normalize(directory + `/schema.graphql`); + fs.writeFileSync(fullSchemaPath, schema); + + // Setup the directories if they do not exist. + initStacksAndResolversDirectories(directory); + + // Write resolvers to disk + const resolverFileNames = Object.keys(deployment.resolvers); + const resolverRootPath = resolverDirectoryPath(directory); + for (const resolverFileName of resolverFileNames) { + const fullResolverPath = path.normalize(resolverRootPath + '/' + resolverFileName); + fs.writeFileSync(fullResolverPath, deployment.resolvers[resolverFileName]); + } + + // Write pipeline resolvers to disk + const pipelineFunctions = Object.keys(deployment.pipelineFunctions); + const pipelineFunctionRootPath = pipelineFunctionDirectoryPath(directory); + for (const functionFileName of pipelineFunctions) { + const fullTemplatePath = path.normalize(pipelineFunctionRootPath + '/' + functionFileName); + fs.writeFileSync(fullTemplatePath, deployment.pipelineFunctions[functionFileName]); + } + + // Write the stacks to disk + const stackNames = Object.keys(deployment.stacks); + const stackRootPath = stacksDirectoryPath(directory); + for (const stackFileName of stackNames) { + const fileNameParts = stackFileName.split('.'); + if (fileNameParts.length === 1) { + fileNameParts.push('json'); } - const rootStack = deployment.rootStack; - const rootStackPath = path.normalize(directory + `/${rootStackFileName}`); - fs.writeFileSync(rootStackPath, JSON.stringify(rootStack, null, 4)); - - // Write params to disk - const jsonString = JSON.stringify(buildParameters, null, 4); - const parametersOutputFilePath = path.join(directory, PARAMETERS_FILE_NAME); - fs.writeFileSync(parametersOutputFilePath, jsonString); + const fullFileName = fileNameParts.join('.'); + throwIfNotJSONExt(fullFileName); + const fullStackPath = path.normalize(stackRootPath + '/' + fullFileName); + let stackString: any = deployment.stacks[stackFileName]; + stackString = + typeof stackString === 'string' ? deployment.stacks[stackFileName] : JSON.stringify(deployment.stacks[stackFileName], null, 4); + fs.writeFileSync(fullStackPath, stackString); + } + + // Write any functions to disk + const functionNames = Object.keys(deployment.functions); + const functionRootPath = path.normalize(directory + `/functions`); + if (!fs.existsSync(functionRootPath)) { + fs.mkdirSync(functionRootPath); + } + for (const functionName of functionNames) { + const fullFunctionPath = path.normalize(functionRootPath + '/' + functionName); + const zipContents = fs.readFileSync(deployment.functions[functionName]); + fs.writeFileSync(fullFunctionPath, zipContents); + } + const rootStack = deployment.rootStack; + const rootStackPath = path.normalize(directory + `/${rootStackFileName}`); + fs.writeFileSync(rootStackPath, JSON.stringify(rootStack, null, 4)); + + // Write params to disk + const jsonString = JSON.stringify(buildParameters, null, 4); + const parametersOutputFilePath = path.join(directory, PARAMETERS_FILE_NAME); + fs.writeFileSync(parametersOutputFilePath, jsonString); } interface MigrationOptions { - projectDirectory: string, - cloudBackendDirectory?: string, + projectDirectory: string; + cloudBackendDirectory?: string; } /** * Using the current cloudbackend as the source of truth of the current env, @@ -398,259 +394,259 @@ interface MigrationOptions { * @param opts */ export async function migrateAPIProject(opts: MigrationOptions) { - const projectDirectory = opts.projectDirectory; - const cloudBackendDirectory = opts.cloudBackendDirectory || projectDirectory; - - // Read the existing project structures from both the current cloud directory - // and the current project environment. - const copyOfCloudBackend = await readFromPath(cloudBackendDirectory); - if (copyOfCloudBackend.build && !copyOfCloudBackend.build[CLOUDFORMATION_FILE_NAME]) { - copyOfCloudBackend.build[CLOUDFORMATION_FILE_NAME] = copyOfCloudBackend[CLOUDFORMATION_FILE_NAME]; - } - const projectConfig = await readFromPath(projectDirectory); - - // Perform the intermediate migration. - const cloudBackendConfig = await readV1ProjectConfiguration(cloudBackendDirectory); - const transformConfig = makeTransformConfigFromOldProject(cloudBackendConfig); - await updateToIntermediateProject(projectDirectory, cloudBackendConfig, transformConfig); - - // Return the old project structures in case of revert. - return { - project: projectConfig, - cloudBackend: copyOfCloudBackend - } + const projectDirectory = opts.projectDirectory; + const cloudBackendDirectory = opts.cloudBackendDirectory || projectDirectory; + + // Read the existing project structures from both the current cloud directory + // and the current project environment. + const copyOfCloudBackend = await readFromPath(cloudBackendDirectory); + if (copyOfCloudBackend.build && !copyOfCloudBackend.build[CLOUDFORMATION_FILE_NAME]) { + copyOfCloudBackend.build[CLOUDFORMATION_FILE_NAME] = copyOfCloudBackend[CLOUDFORMATION_FILE_NAME]; + } + const projectConfig = await readFromPath(projectDirectory); + + // Perform the intermediate migration. + const cloudBackendConfig = await readV1ProjectConfiguration(cloudBackendDirectory); + const transformConfig = makeTransformConfigFromOldProject(cloudBackendConfig); + await updateToIntermediateProject(projectDirectory, cloudBackendConfig, transformConfig); + + // Return the old project structures in case of revert. + return { + project: projectConfig, + cloudBackend: copyOfCloudBackend, + }; } export async function revertAPIMigration(directory: string, oldProject: AmplifyApiV1Project) { - await fs.remove(directory); - await writeToPath(directory, oldProject); + await fs.remove(directory); + await writeToPath(directory, oldProject); } interface AmplifyApiV1Project { - schema: string; - parameters: any; - template: Template; + schema: string; + parameters: any; + template: Template; } /** * Read the configuration for the old version of amplify CLI. */ export async function readV1ProjectConfiguration(projectDirectory: string): Promise { - // Schema - const schema = await readSchema(projectDirectory); - - // Get the template - const cloudFormationTemplatePath = path.join(projectDirectory, CLOUDFORMATION_FILE_NAME); - const cloudFormationTemplateExists = await fs.exists(cloudFormationTemplatePath); - if (!cloudFormationTemplateExists) { - throw new Error(`Could not find cloudformation template at ${cloudFormationTemplatePath}`); - } - const cloudFormationTemplateStr = await fs.readFile(cloudFormationTemplatePath); - const cloudFormationTemplate = JSON.parse(cloudFormationTemplateStr.toString()); - - // Get the params - const parametersFilePath = path.join(projectDirectory, 'parameters.json'); - const parametersFileExists = await fs.exists(parametersFilePath); - if (!parametersFileExists) { - throw new Error(`Could not find parameters.json at ${parametersFilePath}`); - } - const parametersFileStr = await fs.readFile(parametersFilePath); - const parametersFile = JSON.parse(parametersFileStr.toString()); - - return { - template: cloudFormationTemplate, - parameters: parametersFile, - schema - } + // Schema + const schema = await readSchema(projectDirectory); + + // Get the template + const cloudFormationTemplatePath = path.join(projectDirectory, CLOUDFORMATION_FILE_NAME); + const cloudFormationTemplateExists = await fs.exists(cloudFormationTemplatePath); + if (!cloudFormationTemplateExists) { + throw new Error(`Could not find cloudformation template at ${cloudFormationTemplatePath}`); + } + const cloudFormationTemplateStr = await fs.readFile(cloudFormationTemplatePath); + const cloudFormationTemplate = JSON.parse(cloudFormationTemplateStr.toString()); + + // Get the params + const parametersFilePath = path.join(projectDirectory, 'parameters.json'); + const parametersFileExists = await fs.exists(parametersFilePath); + if (!parametersFileExists) { + throw new Error(`Could not find parameters.json at ${parametersFilePath}`); + } + const parametersFileStr = await fs.readFile(parametersFilePath); + const parametersFile = JSON.parse(parametersFileStr.toString()); + + return { + template: cloudFormationTemplate, + parameters: parametersFile, + schema, + }; } export function makeTransformConfigFromOldProject(project: AmplifyApiV1Project): TransformConfig { - const migrationResourceIds = []; - for (const key of Object.keys(project.template.Resources)) { - const resource = project.template.Resources[key]; - switch (resource.Type) { - case 'AWS::DynamoDB::Table': { - migrationResourceIds.push(key); - // When searchable is used we need to keep the output stream arn - // output at the top level as well. TODO: Only do this when searchable is enabled. - // migrationOutputIds.push(`GetAtt${key}StreamArn`); - break; - } - case 'AWS::Elasticsearch::Domain': { - migrationResourceIds.push(key); - break; - } - case 'AWS::IAM::Role': { - if (key === 'ElasticSearchAccessIAMRole') { - // A special case for deploying the migration to projects with @searchable. - // This keeps an IAM role needed by the old ES policy document around. - migrationResourceIds.push(key); - } - break; - } - default: { - break; - } - } - } - return { - Migration: { - V1: { - Resources: migrationResourceIds - } + const migrationResourceIds = []; + for (const key of Object.keys(project.template.Resources)) { + const resource = project.template.Resources[key]; + switch (resource.Type) { + case 'AWS::DynamoDB::Table': { + migrationResourceIds.push(key); + // When searchable is used we need to keep the output stream arn + // output at the top level as well. TODO: Only do this when searchable is enabled. + // migrationOutputIds.push(`GetAtt${key}StreamArn`); + break; + } + case 'AWS::Elasticsearch::Domain': { + migrationResourceIds.push(key); + break; + } + case 'AWS::IAM::Role': { + if (key === 'ElasticSearchAccessIAMRole') { + // A special case for deploying the migration to projects with @searchable. + // This keeps an IAM role needed by the old ES policy document around. + migrationResourceIds.push(key); } + break; + } + default: { + break; + } } + } + return { + Migration: { + V1: { + Resources: migrationResourceIds, + }, + }, + }; } function formatMigratedResource(obj: any) { - const jsonNode = obj && typeof obj.toJSON === 'function' ? obj.toJSON() : obj; - const withoutEncryption = removeSSE(jsonNode); - return withoutEncryption; + const jsonNode = obj && typeof obj.toJSON === 'function' ? obj.toJSON() : obj; + const withoutEncryption = removeSSE(jsonNode); + return withoutEncryption; } function removeSSE(resource: any) { - if (resource && resource.Properties && resource.Properties.SSESpecification) { - delete resource.Properties.SSESpecification; - } - return resource; + if (resource && resource.Properties && resource.Properties.SSESpecification) { + delete resource.Properties.SSESpecification; + } + return resource; } /** * Updates the project to a temporary configuration that stages the real migration. */ async function updateToIntermediateProject(projectDirectory: string, project: AmplifyApiV1Project, config: TransformConfig) { - // Write the config to disk. - await writeConfig(projectDirectory, config); - - const filteredResources = {}; - for (const key of Object.keys(project.template.Resources)) { - const resource = project.template.Resources[key]; - switch (resource.Type) { - case 'AWS::DynamoDB::Table': - case 'AWS::Elasticsearch::Domain': - case 'AWS::AppSync::GraphQLApi': - case 'AWS::AppSync::ApiKey': - case 'AWS::Cognito::UserPool': - case 'AWS::Cognito::UserPoolClient': - filteredResources[key] = formatMigratedResource(resource); - break; - case 'AWS::IAM::Role': { - if (key === 'ElasticSearchAccessIAMRole') { - // A special case for the ES migration case. - filteredResources[key] = resource; - } - break; - } - case 'AWS::AppSync::GraphQLSchema': - const alteredResource = { ...resource }; - alteredResource.Properties.DefinitionS3Location = { - "Fn::Sub": [ - "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/schema.graphql", - { - "S3DeploymentBucket": { - "Ref": "S3DeploymentBucket" - }, - "S3DeploymentRootKey": { - "Ref": "S3DeploymentRootKey" - } - } - ] - }; - filteredResources[key] = alteredResource; - break; - default: - break; // Everything else will live in a nested stack. + // Write the config to disk. + await writeConfig(projectDirectory, config); + + const filteredResources = {}; + for (const key of Object.keys(project.template.Resources)) { + const resource = project.template.Resources[key]; + switch (resource.Type) { + case 'AWS::DynamoDB::Table': + case 'AWS::Elasticsearch::Domain': + case 'AWS::AppSync::GraphQLApi': + case 'AWS::AppSync::ApiKey': + case 'AWS::Cognito::UserPool': + case 'AWS::Cognito::UserPoolClient': + filteredResources[key] = formatMigratedResource(resource); + break; + case 'AWS::IAM::Role': { + if (key === 'ElasticSearchAccessIAMRole') { + // A special case for the ES migration case. + filteredResources[key] = resource; } + break; + } + case 'AWS::AppSync::GraphQLSchema': + const alteredResource = { ...resource }; + alteredResource.Properties.DefinitionS3Location = { + 'Fn::Sub': [ + 's3://${S3DeploymentBucket}/${S3DeploymentRootKey}/schema.graphql', + { + S3DeploymentBucket: { + Ref: 'S3DeploymentBucket', + }, + S3DeploymentRootKey: { + Ref: 'S3DeploymentRootKey', + }, + }, + ], + }; + filteredResources[key] = alteredResource; + break; + default: + break; // Everything else will live in a nested stack. } - - const filteredParameterValues = { - DynamoDBBillingMode: 'PROVISIONED' - }; - const filteredTemplateParameters = { - env: { - Type: "String", - Description: "The environment name. e.g. Dev, Test, or Production", - Default: "NONE" - }, - S3DeploymentBucket: { - Type: "String", - Description: "The S3 bucket containing all deployment assets for the project." - }, - S3DeploymentRootKey: { - Type: "String", - Description: "An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory." + } + + const filteredParameterValues = { + DynamoDBBillingMode: 'PROVISIONED', + }; + const filteredTemplateParameters = { + env: { + Type: 'String', + Description: 'The environment name. e.g. Dev, Test, or Production', + Default: 'NONE', + }, + S3DeploymentBucket: { + Type: 'String', + Description: 'The S3 bucket containing all deployment assets for the project.', + }, + S3DeploymentRootKey: { + Type: 'String', + Description: 'An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory.', + }, + }; + for (const key of Object.keys(project.template.Parameters)) { + switch (key) { + case 'ResolverBucket': + case 'ResolverRootKey': + case 'DeploymentTimestamp': + case 'schemaGraphql': + break; + default: { + const param = project.template.Parameters[key]; + filteredTemplateParameters[key] = param; + if (project.parameters[key]) { + filteredParameterValues[key] = project.parameters[key]; } - }; - for (const key of Object.keys(project.template.Parameters)) { - switch (key) { - case 'ResolverBucket': - case 'ResolverRootKey': - case 'DeploymentTimestamp': - case 'schemaGraphql': - break; - default: { - const param = project.template.Parameters[key]; - filteredTemplateParameters[key] = param; - if (project.parameters[key]) { - filteredParameterValues[key] = project.parameters[key]; - } - break; - } - } - } - - const templateCopy = { - ...project.template, - Resources: filteredResources, - Parameters: filteredTemplateParameters + break; + } } - - // Remove the old cloudformation file. - const oldCloudFormationTemplatePath = path.join(projectDirectory, CLOUDFORMATION_FILE_NAME); - if (fs.existsSync(oldCloudFormationTemplatePath)) { - fs.unlinkSync(oldCloudFormationTemplatePath); - } - - // Write the new cloudformation file to the build. - const cloudFormationTemplateOutputPath = path.join(projectDirectory, 'build', CLOUDFORMATION_FILE_NAME); - fs.writeFileSync(cloudFormationTemplateOutputPath, JSON.stringify(templateCopy, null, 4)); - - // We write the filtered values at the top level and the deployment - // parameters in the build/ directory. We will no longer change the - // top level parameters.json to hold the promise that we do not change - // anything outside of build/ - const parametersInputPath = path.join(projectDirectory, PARAMETERS_FILE_NAME); - fs.writeFileSync(parametersInputPath, JSON.stringify(filteredParameterValues, null, 4)); - - // If the resolvers & stacks directories do not exist, create them. - initStacksAndResolversDirectories(projectDirectory); + } + + const templateCopy = { + ...project.template, + Resources: filteredResources, + Parameters: filteredTemplateParameters, + }; + + // Remove the old cloudformation file. + const oldCloudFormationTemplatePath = path.join(projectDirectory, CLOUDFORMATION_FILE_NAME); + if (fs.existsSync(oldCloudFormationTemplatePath)) { + fs.unlinkSync(oldCloudFormationTemplatePath); + } + + // Write the new cloudformation file to the build. + const cloudFormationTemplateOutputPath = path.join(projectDirectory, 'build', CLOUDFORMATION_FILE_NAME); + fs.writeFileSync(cloudFormationTemplateOutputPath, JSON.stringify(templateCopy, null, 4)); + + // We write the filtered values at the top level and the deployment + // parameters in the build/ directory. We will no longer change the + // top level parameters.json to hold the promise that we do not change + // anything outside of build/ + const parametersInputPath = path.join(projectDirectory, PARAMETERS_FILE_NAME); + fs.writeFileSync(parametersInputPath, JSON.stringify(filteredParameterValues, null, 4)); + + // If the resolvers & stacks directories do not exist, create them. + initStacksAndResolversDirectories(projectDirectory); } function initStacksAndResolversDirectories(directory: string) { - const resolverRootPath = resolverDirectoryPath(directory) - if (!fs.existsSync(resolverRootPath)) { - fs.mkdirSync(resolverRootPath); - } - const pipelineFunctionRootPath = pipelineFunctionDirectoryPath(directory); - if (!fs.existsSync(pipelineFunctionRootPath)) { - fs.mkdirSync(pipelineFunctionRootPath); - } - const stackRootPath = stacksDirectoryPath(directory) - if (!fs.existsSync(stackRootPath)) { - fs.mkdirSync(stackRootPath); - } + const resolverRootPath = resolverDirectoryPath(directory); + if (!fs.existsSync(resolverRootPath)) { + fs.mkdirSync(resolverRootPath); + } + const pipelineFunctionRootPath = pipelineFunctionDirectoryPath(directory); + if (!fs.existsSync(pipelineFunctionRootPath)) { + fs.mkdirSync(pipelineFunctionRootPath); + } + const stackRootPath = stacksDirectoryPath(directory); + if (!fs.existsSync(stackRootPath)) { + fs.mkdirSync(stackRootPath); + } } function pipelineFunctionDirectoryPath(rootPath: string) { - return path.normalize(rootPath + `/pipelineFunctions`) + return path.normalize(rootPath + `/pipelineFunctions`); } function resolverDirectoryPath(rootPath: string) { - return path.normalize(rootPath + `/resolvers`) + return path.normalize(rootPath + `/resolvers`); } function stacksDirectoryPath(rootPath: string) { - return path.normalize(rootPath + `/stacks`) + return path.normalize(rootPath + `/stacks`); } function getOrDefault(o: any, k: string, d: any) { - return o[k] || d; + return o[k] || d; } diff --git a/packages/graphql-transformer-core/src/util/blankTemplate.ts b/packages/graphql-transformer-core/src/util/blankTemplate.ts index 0cf8348385..4b357ea742 100644 --- a/packages/graphql-transformer-core/src/util/blankTemplate.ts +++ b/packages/graphql-transformer-core/src/util/blankTemplate.ts @@ -1,14 +1,14 @@ -import Template from 'cloudform-types/types/template' +import Template from 'cloudform-types/types/template'; export default function blankTemplate(def: Template = {}): Template { - return { - AWSTemplateFormatVersion: '2010-09-09', - Description: 'description', - Metadata: {}, - Parameters: {}, - Resources: {}, - Outputs: {}, - Mappings: {}, - ...def - } + return { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'description', + Metadata: {}, + Parameters: {}, + Resources: {}, + Outputs: {}, + Mappings: {}, + ...def, + }; } diff --git a/packages/graphql-transformer-core/src/util/fileUtils.ts b/packages/graphql-transformer-core/src/util/fileUtils.ts index d3116ba9a5..374fa11df5 100644 --- a/packages/graphql-transformer-core/src/util/fileUtils.ts +++ b/packages/graphql-transformer-core/src/util/fileUtils.ts @@ -6,37 +6,37 @@ const fs = require('fs-extra'); */ export async function emptyDirectory(directory: string) { - const pathExists = await fs.exists(directory); - if (!pathExists) { - return; - } - const dirStats = await fs.lstat(directory); - if (!dirStats.isDirectory()) { - return; - } - const files = await fs.readdir(directory); - for (const fileName of files) { - const fullPath = path.join(directory, fileName); - await fs.remove(fullPath); - } + const pathExists = await fs.exists(directory); + if (!pathExists) { + return; + } + const dirStats = await fs.lstat(directory); + if (!dirStats.isDirectory()) { + return; + } + const files = await fs.readdir(directory); + for (const fileName of files) { + const fullPath = path.join(directory, fileName); + await fs.remove(fullPath); + } } export async function writeToPath(directory: string, obj: any): Promise { - if (Array.isArray(obj)) { - await fs.ensureDir(directory); - for (let i = 0; i < obj.length; i++) { - const newDir = path.join(directory, `${i}`); - await writeToPath(newDir, obj[i]); - } - } else if (typeof obj === 'object') { - await fs.ensureDir(directory); - for (const key of Object.keys(obj)) { - const newDir = path.join(directory, key); - await writeToPath(newDir, obj[key]) - } - } else if (typeof obj === 'string') { - fs.writeFileSync(directory, obj) + if (Array.isArray(obj)) { + await fs.ensureDir(directory); + for (let i = 0; i < obj.length; i++) { + const newDir = path.join(directory, `${i}`); + await writeToPath(newDir, obj[i]); } + } else if (typeof obj === 'object') { + await fs.ensureDir(directory); + for (const key of Object.keys(obj)) { + const newDir = path.join(directory, key); + await writeToPath(newDir, obj[key]); + } + } else if (typeof obj === 'string') { + fs.writeFileSync(directory, obj); + } } /** @@ -44,26 +44,26 @@ export async function writeToPath(directory: string, obj: any): Promise { * @param directory The directory to read. */ export async function readFromPath(directory: string): Promise { - const pathExists = await fs.exists(directory); - if (!pathExists) { - return; - } - const dirStats = await fs.lstat(directory); - if (!dirStats.isDirectory()) { - const buf = await fs.readFile(directory); - return buf.toString(); - } - const files = await fs.readdir(directory); - const accum = {}; - for (const fileName of files) { - const fullPath = path.join(directory, fileName); - const value = await readFromPath(fullPath); - accum[fileName] = value; - } - return accum; + const pathExists = await fs.exists(directory); + if (!pathExists) { + return; + } + const dirStats = await fs.lstat(directory); + if (!dirStats.isDirectory()) { + const buf = await fs.readFile(directory); + return buf.toString(); + } + const files = await fs.readdir(directory); + const accum = {}; + for (const fileName of files) { + const fullPath = path.join(directory, fileName); + const value = await readFromPath(fullPath); + accum[fileName] = value; + } + return accum; } -export type FileHandler = (file: { Key: string, Body: Buffer | string}) => Promise; +export type FileHandler = (file: { Key: string; Body: Buffer | string }) => Promise; /** * Uploads a file with exponential backoff up to a point. * @param opts The deployment options @@ -73,50 +73,48 @@ export type FileHandler = (file: { Key: string, Body: Buffer | string}) => Promi * @param numTries The max number of tries */ export async function handleFile(handler: FileHandler, key: string, body: Buffer, backoffMS: number = 500, numTries: number = 3) { - try { - return await handler({ - Key: key, - Body: body - }) - } catch (e) { - if (numTries > 0) { - await new Promise((res, rej) => setTimeout(() => res(), backoffMS)) - await handleFile(handler, key, body, backoffMS * 2, numTries - 1) - } - throw e + try { + return await handler({ + Key: key, + Body: body, + }); + } catch (e) { + if (numTries > 0) { + await new Promise((res, rej) => setTimeout(() => res(), backoffMS)); + await handleFile(handler, key, body, backoffMS * 2, numTries - 1); } + throw e; + } } -export async function walkDirRec( - dir: string, handler: FileHandler, relativePath: string = '', joinPath: (...paths: string[]) => string -) { - const files = await fs.readdir(dir) - for (const file of files) { - const resourcePath = path.join(dir, file) - const newRelPath = joinPath(relativePath, file) - const isDirectory = (await fs.lstat(resourcePath)).isDirectory() - if (isDirectory) { - await walkDirRec(resourcePath, handler, newRelPath, joinPath) - } else { - const resourceContents = await fs.readFile(resourcePath); - await handleFile(handler, newRelPath, resourceContents) - } +export async function walkDirRec(dir: string, handler: FileHandler, relativePath: string = '', joinPath: (...paths: string[]) => string) { + const files = await fs.readdir(dir); + for (const file of files) { + const resourcePath = path.join(dir, file); + const newRelPath = joinPath(relativePath, file); + const isDirectory = (await fs.lstat(resourcePath)).isDirectory(); + if (isDirectory) { + await walkDirRec(resourcePath, handler, newRelPath, joinPath); + } else { + const resourceContents = await fs.readFile(resourcePath); + await handleFile(handler, newRelPath, resourceContents); } + } } -export async function walkDir(dir: string, handler: (file: { Key: string, Body: Buffer | string}) => Promise) { - return await walkDirRec(dir, handler, '', path.join); +export async function walkDir(dir: string, handler: (file: { Key: string; Body: Buffer | string }) => Promise) { + return await walkDirRec(dir, handler, '', path.join); } -export async function walkDirPosix(dir: string, handler: (file: { Key: string, Body: Buffer | string}) => Promise) { - return await walkDirRec(dir, handler, '', path.posix.join); +export async function walkDirPosix(dir: string, handler: (file: { Key: string; Body: Buffer | string }) => Promise) { + return await walkDirRec(dir, handler, '', path.posix.join); } export function throwIfNotJSONExt(stackFile: string) { - const extension = path.extname(stackFile); - if (extension === ".yaml" || extension === ".yml") { - throw new Error(`Yaml is not yet supported. Please convert the CloudFormation stack ${stackFile} to json.`) - } - if (extension !== ".json") { - throw new Error(`Invalid extension ${extension} for stack ${stackFile}`); - } + const extension = path.extname(stackFile); + if (extension === '.yaml' || extension === '.yml') { + throw new Error(`Yaml is not yet supported. Please convert the CloudFormation stack ${stackFile} to json.`); + } + if (extension !== '.json') { + throw new Error(`Invalid extension ${extension} for stack ${stackFile}`); + } } diff --git a/packages/graphql-transformer-core/src/util/getDirectiveArguments.ts b/packages/graphql-transformer-core/src/util/getDirectiveArguments.ts index a04cbd0684..182a7e6acd 100644 --- a/packages/graphql-transformer-core/src/util/getDirectiveArguments.ts +++ b/packages/graphql-transformer-core/src/util/getDirectiveArguments.ts @@ -1,18 +1,16 @@ -import { - DirectiveNode, - ArgumentNode, - valueFromASTUntyped, -} from 'graphql' +import { DirectiveNode, ArgumentNode, valueFromASTUntyped } from 'graphql'; /** -* Given a directive returns a plain JS map of its arguments -* @param arguments The list of argument nodes to reduce. -*/ + * Given a directive returns a plain JS map of its arguments + * @param arguments The list of argument nodes to reduce. + */ export function getDirectiveArguments(directive: DirectiveNode): any { - return directive.arguments ? directive.arguments.reduce( - (acc: {}, arg: ArgumentNode) => ({ - ...acc, - [arg.name.value]: valueFromASTUntyped(arg.value) - }), - {} - ) : [] -} \ No newline at end of file + return directive.arguments + ? directive.arguments.reduce( + (acc: {}, arg: ArgumentNode) => ({ + ...acc, + [arg.name.value]: valueFromASTUntyped(arg.value), + }), + {} + ) + : []; +} diff --git a/packages/graphql-transformer-core/src/util/getFieldArguments.ts b/packages/graphql-transformer-core/src/util/getFieldArguments.ts index 6381e85987..6c8455ff3a 100755 --- a/packages/graphql-transformer-core/src/util/getFieldArguments.ts +++ b/packages/graphql-transformer-core/src/util/getFieldArguments.ts @@ -1,17 +1,17 @@ import { getBaseType } from 'graphql-transformer-common'; -import { - FieldDefinitionNode, -} from 'graphql' +import { FieldDefinitionNode } from 'graphql'; /** -* Given a Type returns a plain JS map of its arguments -* @param arguments The list of argument nodes to reduce. -*/ + * Given a Type returns a plain JS map of its arguments + * @param arguments The list of argument nodes to reduce. + */ export function getFieldArguments(type: any): any { - return type.fields ? type.fields.reduce( - (acc: {}, arg: FieldDefinitionNode) => ({ - ...acc, - [arg.name.value]: getBaseType(arg.type) - }), - {} - ) : [] -} \ No newline at end of file + return type.fields + ? type.fields.reduce( + (acc: {}, arg: FieldDefinitionNode) => ({ + ...acc, + [arg.name.value]: getBaseType(arg.type), + }), + {} + ) + : []; +} diff --git a/packages/graphql-transformer-core/src/util/getIn.ts b/packages/graphql-transformer-core/src/util/getIn.ts index 94c0e8edad..0a626e3b53 100644 --- a/packages/graphql-transformer-core/src/util/getIn.ts +++ b/packages/graphql-transformer-core/src/util/getIn.ts @@ -4,13 +4,13 @@ * @param path The path. */ export default function getIn(obj: any, path: string[]): any { - let val = obj; - for (const elem of path) { - if (val[elem]) { - val = val[elem] - } else { - return null; - } + let val = obj; + for (const elem of path) { + if (val[elem]) { + val = val[elem]; + } else { + return null; } - return val; -} \ No newline at end of file + } + return val; +} diff --git a/packages/graphql-transformer-core/src/util/getTemplateReferences.ts b/packages/graphql-transformer-core/src/util/getTemplateReferences.ts index f4b7bbd9f5..86edd85494 100644 --- a/packages/graphql-transformer-core/src/util/getTemplateReferences.ts +++ b/packages/graphql-transformer-core/src/util/getTemplateReferences.ts @@ -1,11 +1,11 @@ -import Template from "cloudform-types/types/template"; +import Template from 'cloudform-types/types/template'; /** * A reference map maps a resource id to all locations that reference that * resource via a Fn.Ref or Fn.GetAtt intrinsic function. */ export interface ReferenceMap { - [referenceId: string]: string[][] + [referenceId: string]: string[][]; } /** * Returns a map where the key is the logical id of the resource and the @@ -13,49 +13,49 @@ export interface ReferenceMap { * @param template The template */ export function getTemplateReferences(template: Template): ReferenceMap { - return walk(template, []) + return walk(template, []); } function walk(node: any, path: string[]): ReferenceMap { - const jsonNode = node && typeof node.toJSON === 'function' ? node.toJSON() : node; - if (Array.isArray(jsonNode)) { - let refsFromAllKeys = {} - for (let i = 0; i < jsonNode.length; i++) { - const n = jsonNode[i] - const refsForKey = walk(n, path.concat(`${i}`)) - refsFromAllKeys = mergeReferenceMaps(refsFromAllKeys, refsForKey) - } - return refsFromAllKeys - } else if (typeof jsonNode === 'object') { - // tslint:disable-next-line - const refValue = jsonNode["Ref"]; - const getAtt = jsonNode["Fn::GetAtt"]; - if (refValue) { - return { - [refValue]: [path], - } - } else if (getAtt) { - return { - [getAtt[0]]: [path] - } - } - let refsFromAllKeys = {} - for (const key of Object.keys(jsonNode)) { - const refsForKey = walk(jsonNode[key], path.concat(key)) - refsFromAllKeys = mergeReferenceMaps(refsFromAllKeys, refsForKey) - } - return refsFromAllKeys - } else { - return {} + const jsonNode = node && typeof node.toJSON === 'function' ? node.toJSON() : node; + if (Array.isArray(jsonNode)) { + let refsFromAllKeys = {}; + for (let i = 0; i < jsonNode.length; i++) { + const n = jsonNode[i]; + const refsForKey = walk(n, path.concat(`${i}`)); + refsFromAllKeys = mergeReferenceMaps(refsFromAllKeys, refsForKey); + } + return refsFromAllKeys; + } else if (typeof jsonNode === 'object') { + // tslint:disable-next-line + const refValue = jsonNode['Ref']; + const getAtt = jsonNode['Fn::GetAtt']; + if (refValue) { + return { + [refValue]: [path], + }; + } else if (getAtt) { + return { + [getAtt[0]]: [path], + }; } + let refsFromAllKeys = {}; + for (const key of Object.keys(jsonNode)) { + const refsForKey = walk(jsonNode[key], path.concat(key)); + refsFromAllKeys = mergeReferenceMaps(refsFromAllKeys, refsForKey); + } + return refsFromAllKeys; + } else { + return {}; + } } function mergeReferenceMaps(a: ReferenceMap, b: ReferenceMap): ReferenceMap { - const bKeys = Object.keys(b); - for (const bKey of bKeys) { - if (a[bKey]) { - a[bKey] = a[bKey].concat(b[bKey]) - } else { - a[bKey] = b[bKey] - } + const bKeys = Object.keys(b); + for (const bKey of bKeys) { + if (a[bKey]) { + a[bKey] = a[bKey].concat(b[bKey]); + } else { + a[bKey] = b[bKey]; } - return a; + } + return a; } diff --git a/packages/graphql-transformer-core/src/util/gql.ts b/packages/graphql-transformer-core/src/util/gql.ts index 2d4a2a9ecc..97204fdd25 100644 --- a/packages/graphql-transformer-core/src/util/gql.ts +++ b/packages/graphql-transformer-core/src/util/gql.ts @@ -1,11 +1,11 @@ import { parse } from 'graphql'; export function gql(literals: TemplateStringsArray, ...placeholders: string[]) { - const interleaved = []; - for (let i = 0; i < placeholders.length; i++) { - interleaved.push(literals[i]); - interleaved.push(placeholders[i]); - } - interleaved.push(literals[literals.length - 1]); - const actualSchema = interleaved.join(''); - return parse(actualSchema); -} \ No newline at end of file + const interleaved = []; + for (let i = 0; i < placeholders.length; i++) { + interleaved.push(literals[i]); + interleaved.push(placeholders[i]); + } + interleaved.push(literals[literals.length - 1]); + const actualSchema = interleaved.join(''); + return parse(actualSchema); +} diff --git a/packages/graphql-transformer-core/src/util/makeExportName.ts b/packages/graphql-transformer-core/src/util/makeExportName.ts index b8088d4143..ffdab54ec3 100644 --- a/packages/graphql-transformer-core/src/util/makeExportName.ts +++ b/packages/graphql-transformer-core/src/util/makeExportName.ts @@ -1,2 +1,2 @@ -const makeExportName = (api: string, logicalId: string) => `${api}:${logicalId}` +const makeExportName = (api: string, logicalId: string) => `${api}:${logicalId}`; export default makeExportName; diff --git a/packages/graphql-transformer-core/src/util/sanity-check.ts b/packages/graphql-transformer-core/src/util/sanity-check.ts index 8d28b8dbea..f5c0b1d023 100644 --- a/packages/graphql-transformer-core/src/util/sanity-check.ts +++ b/packages/graphql-transformer-core/src/util/sanity-check.ts @@ -6,53 +6,53 @@ import { InvalidMigrationError } from '../errors'; import { Template } from 'cloudform-types'; interface Diff { - kind: 'N' | 'E' | 'D' | 'A', - path: string[], - lhs?: any, - rhs?: any, - index?: number, - item?: any + kind: 'N' | 'E' | 'D' | 'A'; + path: string[]; + lhs?: any; + rhs?: any; + index?: number; + item?: any; } /** * Calculates a diff between the last saved cloud backend's build directory * and the most recent build. */ -export async function check(currentCloudBackendDir: string, buildDirectory: string, - rootStackName: string = 'cloudformation-template.json') { - const cloudBackendDirectoryExists = await fs.exists(currentCloudBackendDir); - const buildDirectoryExists = await fs.exists(buildDirectory); +export async function check( + currentCloudBackendDir: string, + buildDirectory: string, + rootStackName: string = 'cloudformation-template.json' +) { + const cloudBackendDirectoryExists = await fs.exists(currentCloudBackendDir); + const buildDirectoryExists = await fs.exists(buildDirectory); - // Diff rules rule on a single Diff. - const diffRules: DiffRule[] = [ - cantEditKeySchema, - cantAddLSILater, - cantEditGSIKeySchema, - cantEditLSIKeySchema, - cantAddAndRemoveGSIAtSameTime - ]; - // Project rules run on the full set of diffs, the current build, and the next build. - const projectRules: ProjectRule[] = [ - cantHaveMoreThan200Resources - ]; - if (cloudBackendDirectoryExists && buildDirectoryExists) { - const current = await loadDiffableProject(currentCloudBackendDir, rootStackName); - const next = await loadDiffableProject(buildDirectory, rootStackName); - const diffs = getDiffs(current, next); - // Loop through the diffs and call each DiffRule. - // We loop once so each rule does not need to loop. - if (diffs) { - for (const diff of diffs) { - for (const rule of diffRules) { - rule(diff, current, next); - } - } - for (const projectRule of projectRules) { - projectRule(diffs, current, next); - } + // Diff rules rule on a single Diff. + const diffRules: DiffRule[] = [ + cantEditKeySchema, + cantAddLSILater, + cantEditGSIKeySchema, + cantEditLSIKeySchema, + cantAddAndRemoveGSIAtSameTime, + ]; + // Project rules run on the full set of diffs, the current build, and the next build. + const projectRules: ProjectRule[] = [cantHaveMoreThan200Resources]; + if (cloudBackendDirectoryExists && buildDirectoryExists) { + const current = await loadDiffableProject(currentCloudBackendDir, rootStackName); + const next = await loadDiffableProject(buildDirectory, rootStackName); + const diffs = getDiffs(current, next); + // Loop through the diffs and call each DiffRule. + // We loop once so each rule does not need to loop. + if (diffs) { + for (const diff of diffs) { + for (const rule of diffRules) { + rule(diff, current, next); } - + } + for (const projectRule of projectRules) { + projectRule(diffs, current, next); + } } + } } /** @@ -61,238 +61,246 @@ export async function check(currentCloudBackendDir: string, buildDirectory: stri type DiffRule = (diff: Diff, currentBuild: DiffableProject, nextBuild: DiffableProject) => void; type ProjectRule = (diffs: Diff[], currentBuild: DiffableProject, nextBuild: DiffableProject) => void; - /** - * Throws a helpful error when a customer is trying to complete an invalid migration. - * Users are unable to update a KeySchema after the table has been deployed. - * @param diffs The set of diffs between currentBuild and nextBuild. - * @param currentBuild The last deployed build. - * @param nextBuild The next build. - */ +/** + * Throws a helpful error when a customer is trying to complete an invalid migration. + * Users are unable to update a KeySchema after the table has been deployed. + * @param diffs The set of diffs between currentBuild and nextBuild. + * @param currentBuild The last deployed build. + * @param nextBuild The next build. + */ export function cantEditKeySchema(diff: Diff) { - if (diff.kind === 'E' && diff.path.length === 8 && diff.path[5] === 'KeySchema') { - // diff.path = [ "stacks", "Todo.json", "Resources", "TodoTable", "Properties", "KeySchema", 0, "AttributeName"] - const stackName = basename(diff.path[1], '.json') - const tableName = diff.path[3]; - throw new InvalidMigrationError( - `Attempting to edit the key schema of the ${tableName} table in the ${stackName} stack. `, - 'Adding a primary @key directive to an existing @model. ', - 'Remove the @key directive or provide a name e.g @key(name: "ByStatus", fields: ["status"]).' - ); - } + if (diff.kind === 'E' && diff.path.length === 8 && diff.path[5] === 'KeySchema') { + // diff.path = [ "stacks", "Todo.json", "Resources", "TodoTable", "Properties", "KeySchema", 0, "AttributeName"] + const stackName = basename(diff.path[1], '.json'); + const tableName = diff.path[3]; + throw new InvalidMigrationError( + `Attempting to edit the key schema of the ${tableName} table in the ${stackName} stack. `, + 'Adding a primary @key directive to an existing @model. ', + 'Remove the @key directive or provide a name e.g @key(name: "ByStatus", fields: ["status"]).' + ); + } } /** - * Throws a helpful error when a customer is trying to complete an invalid migration. - * Users are unable to add LSIs after the table has been created. - * @param diffs The set of diffs between currentBuild and nextBuild. - * @param currentBuild The last deployed build. - * @param nextBuild The next build. - */ + * Throws a helpful error when a customer is trying to complete an invalid migration. + * Users are unable to add LSIs after the table has been created. + * @param diffs The set of diffs between currentBuild and nextBuild. + * @param currentBuild The last deployed build. + * @param nextBuild The next build. + */ export function cantAddLSILater(diff: Diff) { - if ( - // When adding a LSI to a table that has 0 LSIs. - (diff.kind === 'N' && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes') || - // When adding a LSI to a table that already has at least one LSI. - (diff.kind === 'A' && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes' && diff.item.kind === 'N') - ) { - // diff.path = [ "stacks", "Todo.json", "Resources", "TodoTable", "Properties", "LocalSecondaryIndexes" ] - const stackName = basename(diff.path[1], '.json') - const tableName = diff.path[3]; - throw new InvalidMigrationError( - `Attempting to add a local secondary index to the ${tableName} table in the ${stackName} stack. ` + - 'Local secondary indexes must be created when the table is created.', - 'Adding a @key directive where the first field in \'fields\' is the same as the first field in the \'fields\' of the primary @key.', - 'Change the first field in \'fields\' such that a global secondary index is created or delete and recreate the model.' - ); - } + if ( + // When adding a LSI to a table that has 0 LSIs. + (diff.kind === 'N' && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes') || + // When adding a LSI to a table that already has at least one LSI. + (diff.kind === 'A' && diff.path.length === 6 && diff.path[5] === 'LocalSecondaryIndexes' && diff.item.kind === 'N') + ) { + // diff.path = [ "stacks", "Todo.json", "Resources", "TodoTable", "Properties", "LocalSecondaryIndexes" ] + const stackName = basename(diff.path[1], '.json'); + const tableName = diff.path[3]; + throw new InvalidMigrationError( + `Attempting to add a local secondary index to the ${tableName} table in the ${stackName} stack. ` + + 'Local secondary indexes must be created when the table is created.', + "Adding a @key directive where the first field in 'fields' is the same as the first field in the 'fields' of the primary @key.", + "Change the first field in 'fields' such that a global secondary index is created or delete and recreate the model." + ); + } } /** - * Throws a helpful error when a customer is trying to complete an invalid migration. - * Users are unable to change GSI KeySchemas after they are created. - * @param diffs The set of diffs between currentBuild and nextBuild. - * @param currentBuild The last deployed build. - * @param nextBuild The next build. - */ - export function cantEditGSIKeySchema(diff: Diff, currentBuild: DiffableProject, nextBuild: DiffableProject) { - function throwError(indexName: string, stackName: string, tableName: string) { - throw new InvalidMigrationError( - `Attempting to edit the global secondary index ${indexName} on the ${tableName} table in the ${stackName} stack. `, - 'The key schema of a global secondary index cannot be changed after being deployed.', - 'If using @key, first add a new @key, run `amplify push`, ' + - 'and then remove the old @key. If using @connection, first remove the @connection, run `amplify push`, ' + - 'and then add the new @connection with the new configuration.' - ); - } - if ( - // implies a field was changed in a GSI after it was created. - // Path like:["stacks","Todo.json","Resources","TodoTable","Properties","GlobalSecondaryIndexes",0,"KeySchema",0,"AttributeName"] - (diff.kind === 'E' && diff.path.length === 10 && diff.path[5] === 'GlobalSecondaryIndexes' && diff.path[7] === 'KeySchema') || - // implies a field was added to a GSI after it was created. - // Path like: [ "stacks", "Comment.json", "Resources", "CommentTable", "Properties", "GlobalSecondaryIndexes", 0, "KeySchema" ] - (diff.kind === 'A' && diff.path.length === 8 && diff.path[5] === 'GlobalSecondaryIndexes' && diff.path[7] === 'KeySchema') - ) { - // This error is symptomatic of a change to the GSI array but does not necessarily imply a breaking change. - const pathToGSIs = diff.path.slice(0, 6); - const oldIndexes = get(currentBuild, pathToGSIs); - const newIndexes = get(nextBuild, pathToGSIs); - const oldIndexesDiffable = keyBy(oldIndexes, 'IndexName'); - const newIndexesDiffable = keyBy(newIndexes, 'IndexName'); - const innerDiffs = getDiffs(oldIndexesDiffable, newIndexesDiffable); - // We must look at this inner diff or else we could confuse a situation - // where the user adds a GSI to the beginning of the GlobalSecondaryIndexes list in CFN. - // We re-key the indexes list so we can determine if a change occured to an index that - // already exists. - for (const innerDiff of innerDiffs) { - // path: ["AGSI","KeySchema",0,"AttributeName"] - if (innerDiff.kind === 'E' && innerDiff.path.length > 2 && innerDiff.path[1] === 'KeySchema') { - const indexName = innerDiff.path[0]; - const stackName = basename(diff.path[1], '.json') - const tableName = diff.path[3]; - throwError(indexName, stackName, tableName); - } else if (innerDiff.kind === 'A' && innerDiff.path.length === 2 && innerDiff.path[1] === 'KeySchema') { - // Path like - ["gsi-PostComments", "KeySchema" ] - const indexName = innerDiff.path[0]; - const stackName = basename(diff.path[1], '.json') - const tableName = diff.path[3]; - throwError(indexName, stackName, tableName); - } - } + * Throws a helpful error when a customer is trying to complete an invalid migration. + * Users are unable to change GSI KeySchemas after they are created. + * @param diffs The set of diffs between currentBuild and nextBuild. + * @param currentBuild The last deployed build. + * @param nextBuild The next build. + */ +export function cantEditGSIKeySchema(diff: Diff, currentBuild: DiffableProject, nextBuild: DiffableProject) { + function throwError(indexName: string, stackName: string, tableName: string) { + throw new InvalidMigrationError( + `Attempting to edit the global secondary index ${indexName} on the ${tableName} table in the ${stackName} stack. `, + 'The key schema of a global secondary index cannot be changed after being deployed.', + 'If using @key, first add a new @key, run `amplify push`, ' + + 'and then remove the old @key. If using @connection, first remove the @connection, run `amplify push`, ' + + 'and then add the new @connection with the new configuration.' + ); + } + if ( + // implies a field was changed in a GSI after it was created. + // Path like:["stacks","Todo.json","Resources","TodoTable","Properties","GlobalSecondaryIndexes",0,"KeySchema",0,"AttributeName"] + (diff.kind === 'E' && diff.path.length === 10 && diff.path[5] === 'GlobalSecondaryIndexes' && diff.path[7] === 'KeySchema') || + // implies a field was added to a GSI after it was created. + // Path like: [ "stacks", "Comment.json", "Resources", "CommentTable", "Properties", "GlobalSecondaryIndexes", 0, "KeySchema" ] + (diff.kind === 'A' && diff.path.length === 8 && diff.path[5] === 'GlobalSecondaryIndexes' && diff.path[7] === 'KeySchema') + ) { + // This error is symptomatic of a change to the GSI array but does not necessarily imply a breaking change. + const pathToGSIs = diff.path.slice(0, 6); + const oldIndexes = get(currentBuild, pathToGSIs); + const newIndexes = get(nextBuild, pathToGSIs); + const oldIndexesDiffable = keyBy(oldIndexes, 'IndexName'); + const newIndexesDiffable = keyBy(newIndexes, 'IndexName'); + const innerDiffs = getDiffs(oldIndexesDiffable, newIndexesDiffable); + // We must look at this inner diff or else we could confuse a situation + // where the user adds a GSI to the beginning of the GlobalSecondaryIndexes list in CFN. + // We re-key the indexes list so we can determine if a change occured to an index that + // already exists. + for (const innerDiff of innerDiffs) { + // path: ["AGSI","KeySchema",0,"AttributeName"] + if (innerDiff.kind === 'E' && innerDiff.path.length > 2 && innerDiff.path[1] === 'KeySchema') { + const indexName = innerDiff.path[0]; + const stackName = basename(diff.path[1], '.json'); + const tableName = diff.path[3]; + throwError(indexName, stackName, tableName); + } else if (innerDiff.kind === 'A' && innerDiff.path.length === 2 && innerDiff.path[1] === 'KeySchema') { + // Path like - ["gsi-PostComments", "KeySchema" ] + const indexName = innerDiff.path[0]; + const stackName = basename(diff.path[1], '.json'); + const tableName = diff.path[3]; + throwError(indexName, stackName, tableName); + } } + } } /** - * Throws a helpful error when a customer is trying to complete an invalid migration. - * Users are unable to add and remove GSIs at the same time. - * @param diffs The set of diffs between currentBuild and nextBuild. - * @param currentBuild The last deployed build. - * @param nextBuild The next build. - */ - export function cantAddAndRemoveGSIAtSameTime(diff: Diff, currentBuild: DiffableProject, nextBuild: DiffableProject) { - function throwError(stackName: string, tableName: string) { - throw new InvalidMigrationError( - `Attempting to add and remove a global secondary index at the same time on the ${tableName} table in the ${stackName} stack. `, - 'You may only change one global secondary index in a single CloudFormation stack update. ', - 'If using @key, change one @key at a time. ' + - 'If using @connection, add the new @connection, run `amplify push`, ' + - 'and then remove the new @connection with the new configuration.' - ); + * Throws a helpful error when a customer is trying to complete an invalid migration. + * Users are unable to add and remove GSIs at the same time. + * @param diffs The set of diffs between currentBuild and nextBuild. + * @param currentBuild The last deployed build. + * @param nextBuild The next build. + */ +export function cantAddAndRemoveGSIAtSameTime(diff: Diff, currentBuild: DiffableProject, nextBuild: DiffableProject) { + function throwError(stackName: string, tableName: string) { + throw new InvalidMigrationError( + `Attempting to add and remove a global secondary index at the same time on the ${tableName} table in the ${stackName} stack. `, + 'You may only change one global secondary index in a single CloudFormation stack update. ', + 'If using @key, change one @key at a time. ' + + 'If using @connection, add the new @connection, run `amplify push`, ' + + 'and then remove the new @connection with the new configuration.' + ); + } + if ( + // implies a field was changed in a GSI after it was created. + // Path like:["stacks","Todo.json","Resources","TodoTable","Properties","GlobalSecondaryIndexes", ... ] + diff.kind === 'E' && + diff.path.length > 6 && + diff.path[5] === 'GlobalSecondaryIndexes' + ) { + // This error is symptomatic of a change to the GSI array but does not necessarily imply a breaking change. + const pathToGSIs = diff.path.slice(0, 6); + const oldIndexes = get(currentBuild, pathToGSIs); + const newIndexes = get(nextBuild, pathToGSIs); + const oldIndexesDiffable = keyBy(oldIndexes, 'IndexName'); + const newIndexesDiffable = keyBy(newIndexes, 'IndexName'); + const innerDiffs = getDiffs(oldIndexesDiffable, newIndexesDiffable); + let sawDelete = false; + let sawNew = false; + for (const diff of innerDiffs) { + // A path of length 1 means an entire GSI was created or deleted. + if (diff.path.length === 1 && diff.kind === 'D') { + sawDelete = true; + } + if (diff.path.length === 1 && diff.kind === 'N') { + sawNew = true; + } } - if ( - // implies a field was changed in a GSI after it was created. - // Path like:["stacks","Todo.json","Resources","TodoTable","Properties","GlobalSecondaryIndexes", ... ] - diff.kind === 'E' && diff.path.length > 6 && diff.path[5] === 'GlobalSecondaryIndexes' - ) { - // This error is symptomatic of a change to the GSI array but does not necessarily imply a breaking change. - const pathToGSIs = diff.path.slice(0, 6); - const oldIndexes = get(currentBuild, pathToGSIs); - const newIndexes = get(nextBuild, pathToGSIs); - const oldIndexesDiffable = keyBy(oldIndexes, 'IndexName'); - const newIndexesDiffable = keyBy(newIndexes, 'IndexName'); - const innerDiffs = getDiffs(oldIndexesDiffable, newIndexesDiffable); - let sawDelete = false; - let sawNew = false; - for (const diff of innerDiffs) { - // A path of length 1 means an entire GSI was created or deleted. - if (diff.path.length === 1 && diff.kind === 'D') { - sawDelete = true; - } - if (diff.path.length === 1 && diff.kind === 'N') { - sawNew = true; - } - } - if (sawDelete && sawNew) { - const stackName = basename(diff.path[1], '.json') - const tableName = diff.path[3]; - throwError(stackName, tableName); - } + if (sawDelete && sawNew) { + const stackName = basename(diff.path[1], '.json'); + const tableName = diff.path[3]; + throwError(stackName, tableName); } + } } /** - * Throws a helpful error when a customer is trying to complete an invalid migration. - * Users are unable to change LSI KeySchemas after they are created. - * @param diffs The set of diffs between currentBuild and nextBuild. - * @param currentBuild The last deployed build. - * @param nextBuild The next build. - */ - export function cantEditLSIKeySchema(diff: Diff, currentBuild: DiffableProject, nextBuild: DiffableProject) { - if ( - // ["stacks","Todo.json","Resources","TodoTable","Properties","LocalSecondaryIndexes",0,"KeySchema",0,"AttributeName"] - diff.kind === 'E' && diff.path.length === 10 && diff.path[5] === 'LocalSecondaryIndexes' && diff.path[7] === 'KeySchema' - ) { - // This error is symptomatic of a change to the GSI array but does not necessarily imply a breaking change. - const pathToGSIs = diff.path.slice(0, 6); - const oldIndexes = get(currentBuild, pathToGSIs); - const newIndexes = get(nextBuild, pathToGSIs); - const oldIndexesDiffable = keyBy(oldIndexes, 'IndexName'); - const newIndexesDiffable = keyBy(newIndexes, 'IndexName'); - const innerDiffs = getDiffs(oldIndexesDiffable, newIndexesDiffable); - // We must look at this inner diff or else we could confuse a situation - // where the user adds a LSI to the beginning of the LocalSecondaryIndex list in CFN. - // We re-key the indexes list so we can determine if a change occured to an index that - // already exists. - for (const innerDiff of innerDiffs) { - // path: ["AGSI","KeySchema",0,"AttributeName"] - if (innerDiff.kind === 'E' && innerDiff.path.length > 2 && innerDiff.path[1] === 'KeySchema') { - const indexName = innerDiff.path[0]; - const stackName = basename(diff.path[1], '.json') - const tableName = diff.path[3]; - throw new InvalidMigrationError( - `Attempting to edit the local secondary index ${indexName} on the ${tableName} table in the ${stackName} stack. `, - 'The key schema of a local secondary index cannot be changed after being deployed.', - 'When enabling new access patterns you should: 1. Add a new @key 2. run amplify push ' + - '3. Verify the new access pattern and remove the old @key.' - ); - } - } + * Throws a helpful error when a customer is trying to complete an invalid migration. + * Users are unable to change LSI KeySchemas after they are created. + * @param diffs The set of diffs between currentBuild and nextBuild. + * @param currentBuild The last deployed build. + * @param nextBuild The next build. + */ +export function cantEditLSIKeySchema(diff: Diff, currentBuild: DiffableProject, nextBuild: DiffableProject) { + if ( + // ["stacks","Todo.json","Resources","TodoTable","Properties","LocalSecondaryIndexes",0,"KeySchema",0,"AttributeName"] + diff.kind === 'E' && + diff.path.length === 10 && + diff.path[5] === 'LocalSecondaryIndexes' && + diff.path[7] === 'KeySchema' + ) { + // This error is symptomatic of a change to the GSI array but does not necessarily imply a breaking change. + const pathToGSIs = diff.path.slice(0, 6); + const oldIndexes = get(currentBuild, pathToGSIs); + const newIndexes = get(nextBuild, pathToGSIs); + const oldIndexesDiffable = keyBy(oldIndexes, 'IndexName'); + const newIndexesDiffable = keyBy(newIndexes, 'IndexName'); + const innerDiffs = getDiffs(oldIndexesDiffable, newIndexesDiffable); + // We must look at this inner diff or else we could confuse a situation + // where the user adds a LSI to the beginning of the LocalSecondaryIndex list in CFN. + // We re-key the indexes list so we can determine if a change occured to an index that + // already exists. + for (const innerDiff of innerDiffs) { + // path: ["AGSI","KeySchema",0,"AttributeName"] + if (innerDiff.kind === 'E' && innerDiff.path.length > 2 && innerDiff.path[1] === 'KeySchema') { + const indexName = innerDiff.path[0]; + const stackName = basename(diff.path[1], '.json'); + const tableName = diff.path[3]; + throw new InvalidMigrationError( + `Attempting to edit the local secondary index ${indexName} on the ${tableName} table in the ${stackName} stack. `, + 'The key schema of a local secondary index cannot be changed after being deployed.', + 'When enabling new access patterns you should: 1. Add a new @key 2. run amplify push ' + + '3. Verify the new access pattern and remove the old @key.' + ); + } } + } } export function cantHaveMoreThan200Resources(diffs: Diff[], currentBuild: DiffableProject, nextBuild: DiffableProject) { - const stackKeys = Object.keys(nextBuild.stacks); - for (const stackName of stackKeys) { - const stack = nextBuild.stacks[stackName]; - if (stack && stack.Resources && Object.keys(stack.Resources).length > 200) { - throw new InvalidMigrationError( - `The ${stackName} stack defines more than 200 resources.`, - 'CloudFormation templates may contain at most 200 resources.', - 'If the stack is a custom stack, break the stack up into multiple files in stacks/. ' + - 'If the stack was generated, you have hit a limit and can use the StackMapping argument in ' + - 'transform.conf.json to fine tune how resources are assigned to stacks.' - ) - } + const stackKeys = Object.keys(nextBuild.stacks); + for (const stackName of stackKeys) { + const stack = nextBuild.stacks[stackName]; + if (stack && stack.Resources && Object.keys(stack.Resources).length > 200) { + throw new InvalidMigrationError( + `The ${stackName} stack defines more than 200 resources.`, + 'CloudFormation templates may contain at most 200 resources.', + 'If the stack is a custom stack, break the stack up into multiple files in stacks/. ' + + 'If the stack was generated, you have hit a limit and can use the StackMapping argument in ' + + 'transform.conf.json to fine tune how resources are assigned to stacks.' + ); } + } } // Takes a list of object and returns an object keyed by the given attribute. // This allows us to make more accurate diffs. function keyBy(objects: any[], attr: string) { - return objects.reduce((acc, obj) => ({ - ...acc, - [obj[attr]]: obj - }), {}); + return objects.reduce( + (acc, obj) => ({ + ...acc, + [obj[attr]]: obj, + }), + {} + ); } // Helpers interface DiffableProject { - stacks: { - [stackName: string]: Template - }, - root: Template + stacks: { + [stackName: string]: Template; + }; + root: Template; } async function loadDiffableProject(path: string, rootStackName: string): Promise { - const project = await readFromPath(path); - const currentStacks = project.stacks || {}; - const diffableProject: DiffableProject = { - stacks: {}, - root: {} - }; - for (const key of Object.keys(currentStacks)) { - diffableProject.stacks[key] = JSON.parse(project.stacks[key]); - } - diffableProject.root = JSON.parse(project[rootStackName]); - return diffableProject; + const project = await readFromPath(path); + const currentStacks = project.stacks || {}; + const diffableProject: DiffableProject = { + stacks: {}, + root: {}, + }; + for (const key of Object.keys(currentStacks)) { + diffableProject.stacks[key] = JSON.parse(project.stacks[key]); + } + diffableProject.root = JSON.parse(project[rootStackName]); + return diffableProject; } /** @@ -302,12 +310,12 @@ async function loadDiffableProject(path: string, rootStackName: string): Promise * @param path The path. */ function get(obj: any, path: string[]) { - let tmp = obj; - for (const part of path) { - tmp = tmp[part]; - if (!tmp) { - return undefined; - } + let tmp = obj; + for (const part of path) { + tmp = tmp[part]; + if (!tmp) { + return undefined; } - return tmp; -} \ No newline at end of file + } + return tmp; +} diff --git a/packages/graphql-transformer-core/src/util/setIn.ts b/packages/graphql-transformer-core/src/util/setIn.ts index b769c09620..213b0d6b69 100644 --- a/packages/graphql-transformer-core/src/util/setIn.ts +++ b/packages/graphql-transformer-core/src/util/setIn.ts @@ -4,13 +4,13 @@ * @param path The path. */ export default function setIn(obj: any, path: string[], value: any): any { - let val = obj; - for (let i = 0; i < path.length; i++) { - const key = path[i]; - if (val[key] && i === path.length - 1) { - val[key] = value - } else if (val[key]) { - val = val[key] - } + let val = obj; + for (let i = 0; i < path.length; i++) { + const key = path[i]; + if (val[key] && i === path.length - 1) { + val[key] = value; + } else if (val[key]) { + val = val[key]; } + } } diff --git a/packages/graphql-transformer-core/src/util/splitStack.ts b/packages/graphql-transformer-core/src/util/splitStack.ts index 0edc9a0b3e..a0b3d051fe 100644 --- a/packages/graphql-transformer-core/src/util/splitStack.ts +++ b/packages/graphql-transformer-core/src/util/splitStack.ts @@ -1,384 +1,335 @@ -import Template from "cloudform-types/types/template"; -import { Fn, CloudFormation, StringParameter } from "cloudform-types"; +import Template from 'cloudform-types/types/template'; +import { Fn, CloudFormation, StringParameter } from 'cloudform-types'; import { getTemplateReferences } from './getTemplateReferences'; import getIn from './getIn'; import setIn from './setIn'; import blankTemplate from './blankTemplate'; -import Output from "cloudform-types/types/output"; +import Output from 'cloudform-types/types/output'; /** * Stack resources */ export interface NestedStacks { - // The root stack template. - rootStack: Template, - // All the nested stack templates. - stacks: { - [name: string]: Template - }, - // The full stack mapping for the deployment. - stackMapping: { [resourceId: string]: string } + // The root stack template. + rootStack: Template; + // All the nested stack templates. + stacks: { + [name: string]: Template; + }; + // The full stack mapping for the deployment. + stackMapping: { [resourceId: string]: string }; } interface NestedStackInfo { - stackDependencyMap: { [k: string]: string[] } - stackParameterMap: { [k: string]: {[p: string]: any } } + stackDependencyMap: { [k: string]: string[] }; + stackParameterMap: { [k: string]: { [p: string]: any } }; } export type StackRules = Map; export interface SplitStackOptions { - stack: Template, - stackRules: StackRules, - rootStackName?: string, - defaultParameterValues?: { [k: string]: any }, - defaultParameterDefinitions?: { [k: string]: any } - defaultDependencies?: string[], - importExportPrefix: any, - deployment: { - deploymentBucketParameterName: string, - deploymentKeyParameterName: string - } + stack: Template; + stackRules: StackRules; + rootStackName?: string; + defaultParameterValues?: { [k: string]: any }; + defaultParameterDefinitions?: { [k: string]: any }; + defaultDependencies?: string[]; + importExportPrefix: any; + deployment: { + deploymentBucketParameterName: string; + deploymentKeyParameterName: string; + }; } export default function splitStack(opts: SplitStackOptions): NestedStacks { - const stack = opts.stack; - const stackRules = opts.stackRules; - const rootStackName = opts.rootStackName || 'root'; - const defaultParameterValues = opts.defaultParameterValues || {}; - const defaultParameterDefinitions = opts.defaultParameterDefinitions || {}; - const defaultDependencies = opts.defaultDependencies || []; - const importExportPrefix = opts.importExportPrefix; - - /** - * Returns a map where the keys are the Resource/Output ids and the values are - * the names of the stack where that Resource/Output belongs. This fills - * any missing values with that of the root stack and thus returns a full-mapping. - */ - function createMapByStackRules( - keys: string[] - ): { [key: string]: string } { - const stackMap = {}; - for (const key of keys) { - const mappedTo = stackRules.get(key); - if (mappedTo) { - stackMap[key] = mappedTo; - } else { - stackMap[key] = rootStackName; - } - } - return stackMap; - } - - /** - * Returns a map where the keys are the resource ids and the values are the - * names of the stack where that resource belongs. - */ - function mapResourcesToStack( - template: Template, - ): { [key: string]: string } { - return createMapByStackRules(Object.keys(template.Resources)); - } - - function mapMappingToStack( - template: Template, - ): { [key: string]: string } { - return createMapByStackRules(Object.keys(template.Mappings)); - } + const stack = opts.stack; + const stackRules = opts.stackRules; + const rootStackName = opts.rootStackName || 'root'; + const defaultParameterValues = opts.defaultParameterValues || {}; + const defaultParameterDefinitions = opts.defaultParameterDefinitions || {}; + const defaultDependencies = opts.defaultDependencies || []; + const importExportPrefix = opts.importExportPrefix; - /** - * Returns a map where the keys are the Outputs ids and the values are the - * names of the stack where that Output belongs. - */ - function mapOutputsToStack( - template: Template, - ): { [key: string]: string } { - return createMapByStackRules(Object.keys(template.Outputs)); + /** + * Returns a map where the keys are the Resource/Output ids and the values are + * the names of the stack where that Resource/Output belongs. This fills + * any missing values with that of the root stack and thus returns a full-mapping. + */ + function createMapByStackRules(keys: string[]): { [key: string]: string } { + const stackMap = {}; + for (const key of keys) { + const mappedTo = stackRules.get(key); + if (mappedTo) { + stackMap[key] = mappedTo; + } else { + stackMap[key] = rootStackName; + } } + return stackMap; + } - /** - * Uses the stackRules to split resources out into the different stacks. - */ - function collectTemplates(template: Template, resourceToStackMap: { [k: string]: string }, outputToStackMap: { [k: string]: string }, mappingsToStackMap: { [k: string]: string }) { - const resourceIds = Object.keys(resourceToStackMap); - const templateMap = {} - for (const resourceId of resourceIds) { - const stackName = resourceToStackMap[resourceId] - if (!templateMap[stackName]) { - templateMap[stackName] = blankTemplate({ - Description: 'An auto-generated nested stack.', - Parameters: { - ...template.Parameters, - ...defaultParameterDefinitions - }, - Conditions: template.Conditions - }) - } - const resource = template.Resources[resourceId]; - // Remove any dependsOn that will no longer be in the same template. - let depends: string | string[] = (resource.DependsOn as any); - if (depends && Array.isArray(depends)) { - resource.DependsOn = depends.filter(id => { - return resourceToStackMap[id] === stackName; - }) - } else if (depends && typeof depends === 'string') { - resource.DependsOn = resourceToStackMap[depends] === stackName ? depends : undefined; - } - templateMap[stackName].Resources[resourceId] = resource; - } + /** + * Returns a map where the keys are the resource ids and the values are the + * names of the stack where that resource belongs. + */ + function mapResourcesToStack(template: Template): { [key: string]: string } { + return createMapByStackRules(Object.keys(template.Resources)); + } - const outputIds = Object.keys(outputToStackMap); - for (const outputId of outputIds) { - const stackName = outputToStackMap[outputId]; - const output = template.Outputs[outputId]; - templateMap[stackName].Outputs[outputId] = output; - } + function mapMappingToStack(template: Template): { [key: string]: string } { + return createMapByStackRules(Object.keys(template.Mappings)); + } - const mappingIds = Object.keys(mappingToStackMap); - for (const mappingId of mappingIds) { - const stackName = mappingsToStackMap[mappingId]; - const mappings = template.Mappings[mappingId]; - templateMap[stackName].Mappings[mappingId] = mappings; - } + /** + * Returns a map where the keys are the Outputs ids and the values are the + * names of the stack where that Output belongs. + */ + function mapOutputsToStack(template: Template): { [key: string]: string } { + return createMapByStackRules(Object.keys(template.Outputs)); + } - - // The root stack exposes all parameters at the top level. - templateMap[rootStackName].Parameters = template.Parameters; - templateMap[rootStackName].Conditions = template.Conditions; - return templateMap; + /** + * Uses the stackRules to split resources out into the different stacks. + */ + function collectTemplates( + template: Template, + resourceToStackMap: { [k: string]: string }, + outputToStackMap: { [k: string]: string }, + mappingsToStackMap: { [k: string]: string } + ) { + const resourceIds = Object.keys(resourceToStackMap); + const templateMap = {}; + for (const resourceId of resourceIds) { + const stackName = resourceToStackMap[resourceId]; + if (!templateMap[stackName]) { + templateMap[stackName] = blankTemplate({ + Description: 'An auto-generated nested stack.', + Parameters: { + ...template.Parameters, + ...defaultParameterDefinitions, + }, + Conditions: template.Conditions, + }); + } + const resource = template.Resources[resourceId]; + // Remove any dependsOn that will no longer be in the same template. + let depends: string | string[] = resource.DependsOn as any; + if (depends && Array.isArray(depends)) { + resource.DependsOn = depends.filter(id => { + return resourceToStackMap[id] === stackName; + }); + } else if (depends && typeof depends === 'string') { + resource.DependsOn = resourceToStackMap[depends] === stackName ? depends : undefined; + } + templateMap[stackName].Resources[resourceId] = resource; } - /** - * Looks at each stack to find all its Ref and GetAtt expressions - * and relaces them with Import/Export (when siblings) and Parameter/Ref - * (when parent-child). - */ - function replaceReferences(stacks: {[name: string]: Template}, resourceToStackMap: { [key: string]: string }): NestedStackInfo { - // For each stack create a list of stacks that it depends on. - const stackDependsOnMap: { [k: string]: string[] } = Object.keys(stacks).reduce((acc, k) => ({ ...acc, [k]: []}), {}) - const stackParamsMap: { [k: string]: {[p: string]: any } } = Object.keys(stacks).reduce((acc, k) => ({ ...acc, [k]: {}}), {}) - for (const thisStackName of Object.keys(stacks)) { - const template = stacks[thisStackName] - const resourceToReferenceMap = getTemplateReferences(template); - for (const resourceId of Object.keys(resourceToReferenceMap)) { - const references = resourceToReferenceMap[resourceId]; - const referencedStackName = resourceToStackMap[resourceId] - for (const refList of references) { - const refNode = getIn(template, refList) - // Only update a Ref if it references a Resource in a different stack. - // Other Refs are params, conditions, or built in pseudo params which remain the same. - const refNeedsReplacing = - refNode - && refNode.Ref - && referencedStackName - && referencedStackName !== thisStackName; - // Do not update a GetAtt if resources are in the same stack. - // Do update a GetAtt if it ref's a resource in a different stack. - const getAttNeedsReplacing = - refNode - && refNode['Fn::GetAtt'] - && referencedStackName - && referencedStackName !== thisStackName; - const isChildReferencingRoot = thisStackName !== rootStackName && referencedStackName === rootStackName - if (refNeedsReplacing && isChildReferencingRoot) { - // Replace the Ref with a reference to the parameter that we will pass in. - // The stackParamsMap holds a map of parameter values that will be passed into - // the nested stack from the root. The values are the full Ref or GetAtt nodes. - const parameterName = `Ref${resourceId}` - stackParamsMap[thisStackName][parameterName] = refNode - template.Parameters[parameterName] = new StringParameter({ - Description: `Auto-generated parameter that forwards Fn.Ref(${resourceId}) through to nested stacks.` - }) - setIn(template, refList, Fn.Ref(parameterName)); - } else if (refNeedsReplacing) { - setIn(template, refList, makeImportValueForRef(resourceId)); - const outputForInput = makeOutputForRef(resourceId); - const referencedStack = stacks[referencedStackName]; - const exportLogicalId = `Ref${resourceId}` - if (referencedStack && referencedStack.Outputs && !referencedStack.Outputs[exportLogicalId]) { - if (template.Outputs[exportLogicalId]) { - // https://github.com/aws-amplify/amplify-cli/issues/1581 - // Export names are unique and the transformer libraries - // enforce resource id uniqueness as well. Delete the existing - // output if we are adding it to another stack to prevent push failures. - delete template.Outputs[exportLogicalId]; - } - referencedStack.Outputs[exportLogicalId] = outputForInput; - } - if (stackDependsOnMap[thisStackName] && !stackDependsOnMap[thisStackName].find(s => s === referencedStackName)) { - stackDependsOnMap[thisStackName].push(referencedStackName) - } - } else if (getAttNeedsReplacing && isChildReferencingRoot) { - // Replace the GetAtt with a reference to the parameter that we will pass in. - // The stackParamsMap holds a map of parameter values that will be passed into - // the nested stack from the root. The values are the full Ref or GetAtt nodes. - const [resId, attr] = refNode["Fn::GetAtt"]; - const parameterName = `GetAtt${resourceId}${attr}` - stackParamsMap[thisStackName][parameterName] = refNode - template.Parameters[parameterName] = new StringParameter({ - Description: `Auto-generated parameter that forwards Fn.GetAtt(${resourceId}, ${attr}) through to nested stacks.` - }) - setIn(template, refList, Fn.Ref(parameterName)); - } else if (getAttNeedsReplacing) { - const [resId, attr] = refNode["Fn::GetAtt"]; - setIn(template, refList, makeImportValueForGetAtt(resourceId, attr)); - const outputForInput = makeOutputForGetAtt(resourceId, attr); - const referencedStack = stacks[referencedStackName]; - const exportLogicalId = `GetAtt${resourceId}${attr}` - if (referencedStack && referencedStack.Outputs && !referencedStack.Outputs[exportLogicalId]) { - if (template.Outputs[exportLogicalId]) { - // https://github.com/aws-amplify/amplify-cli/issues/1581 - // Export names are unique and the transformer libraries - // enforce resource id uniqueness as well. Delete the existing - // output if we are adding it to another stack to prevent push failures. - delete template.Outputs[exportLogicalId]; - } - referencedStack.Outputs[exportLogicalId] = outputForInput; - } - if (stackDependsOnMap[thisStackName] && !stackDependsOnMap[thisStackName].find(s => s === referencedStackName)) { - stackDependsOnMap[thisStackName].push(referencedStackName) - } - } - } - } - } - return { - stackDependencyMap: stackDependsOnMap, - stackParameterMap: stackParamsMap - } + const outputIds = Object.keys(outputToStackMap); + for (const outputId of outputIds) { + const stackName = outputToStackMap[outputId]; + const output = template.Outputs[outputId]; + templateMap[stackName].Outputs[outputId] = output; } - /** - * Create an import value node that replaces a Ref. - */ - function makeImportValueForRef(resourceId: string): any { - return Fn.ImportValue( - Fn.Join( - ':', - [ - importExportPrefix, - 'Ref', - resourceId - ] - ) - ) + const mappingIds = Object.keys(mappingToStackMap); + for (const mappingId of mappingIds) { + const stackName = mappingsToStackMap[mappingId]; + const mappings = template.Mappings[mappingId]; + templateMap[stackName].Mappings[mappingId] = mappings; } - /** - * Make an ImportValue node that imports the corresponding export. - * @param resourceId The resource being got - * @param attribute The attribute on the resource - */ - function makeImportValueForGetAtt(resourceId: string, attribute: string): any { - return Fn.ImportValue( - Fn.Join( - ':', - [ - importExportPrefix, - 'GetAtt', - resourceId, - attribute - ] - ) - ) - } + // The root stack exposes all parameters at the top level. + templateMap[rootStackName].Parameters = template.Parameters; + templateMap[rootStackName].Conditions = template.Conditions; + return templateMap; + } - /** - * Make an output record that exports the GetAtt. - * @param resourceId The resource being got - * @param attribute The attribute on the resource - */ - function makeOutputForGetAtt(resourceId: string, attribute: string): Output { - return { - Value: Fn.GetAtt(resourceId, attribute), - Export: { - Name: Fn.Join( - ':', - [ - importExportPrefix, - 'GetAtt', - resourceId, - attribute - ] - ) + /** + * Looks at each stack to find all its Ref and GetAtt expressions + * and relaces them with Import/Export (when siblings) and Parameter/Ref + * (when parent-child). + */ + function replaceReferences(stacks: { [name: string]: Template }, resourceToStackMap: { [key: string]: string }): NestedStackInfo { + // For each stack create a list of stacks that it depends on. + const stackDependsOnMap: { [k: string]: string[] } = Object.keys(stacks).reduce((acc, k) => ({ ...acc, [k]: [] }), {}); + const stackParamsMap: { [k: string]: { [p: string]: any } } = Object.keys(stacks).reduce((acc, k) => ({ ...acc, [k]: {} }), {}); + for (const thisStackName of Object.keys(stacks)) { + const template = stacks[thisStackName]; + const resourceToReferenceMap = getTemplateReferences(template); + for (const resourceId of Object.keys(resourceToReferenceMap)) { + const references = resourceToReferenceMap[resourceId]; + const referencedStackName = resourceToStackMap[resourceId]; + for (const refList of references) { + const refNode = getIn(template, refList); + // Only update a Ref if it references a Resource in a different stack. + // Other Refs are params, conditions, or built in pseudo params which remain the same. + const refNeedsReplacing = refNode && refNode.Ref && referencedStackName && referencedStackName !== thisStackName; + // Do not update a GetAtt if resources are in the same stack. + // Do update a GetAtt if it ref's a resource in a different stack. + const getAttNeedsReplacing = refNode && refNode['Fn::GetAtt'] && referencedStackName && referencedStackName !== thisStackName; + const isChildReferencingRoot = thisStackName !== rootStackName && referencedStackName === rootStackName; + if (refNeedsReplacing && isChildReferencingRoot) { + // Replace the Ref with a reference to the parameter that we will pass in. + // The stackParamsMap holds a map of parameter values that will be passed into + // the nested stack from the root. The values are the full Ref or GetAtt nodes. + const parameterName = `Ref${resourceId}`; + stackParamsMap[thisStackName][parameterName] = refNode; + template.Parameters[parameterName] = new StringParameter({ + Description: `Auto-generated parameter that forwards Fn.Ref(${resourceId}) through to nested stacks.`, + }); + setIn(template, refList, Fn.Ref(parameterName)); + } else if (refNeedsReplacing) { + setIn(template, refList, makeImportValueForRef(resourceId)); + const outputForInput = makeOutputForRef(resourceId); + const referencedStack = stacks[referencedStackName]; + const exportLogicalId = `Ref${resourceId}`; + if (referencedStack && referencedStack.Outputs && !referencedStack.Outputs[exportLogicalId]) { + if (template.Outputs[exportLogicalId]) { + // https://github.com/aws-amplify/amplify-cli/issues/1581 + // Export names are unique and the transformer libraries + // enforce resource id uniqueness as well. Delete the existing + // output if we are adding it to another stack to prevent push failures. + delete template.Outputs[exportLogicalId]; + } + referencedStack.Outputs[exportLogicalId] = outputForInput; } - } - } - - /** - * Make an output record that exports the GetAtt. - * @param resourceId The resource being got - * @param attribute The attribute on the resource - */ - function makeOutputForRef(resourceId: string): Output { - return { - Value: Fn.Ref(resourceId), - Export: { - Name: Fn.Join( - ':', - [ - importExportPrefix, - 'Ref', - resourceId - ] - ) + if (stackDependsOnMap[thisStackName] && !stackDependsOnMap[thisStackName].find(s => s === referencedStackName)) { + stackDependsOnMap[thisStackName].push(referencedStackName); + } + } else if (getAttNeedsReplacing && isChildReferencingRoot) { + // Replace the GetAtt with a reference to the parameter that we will pass in. + // The stackParamsMap holds a map of parameter values that will be passed into + // the nested stack from the root. The values are the full Ref or GetAtt nodes. + const [resId, attr] = refNode['Fn::GetAtt']; + const parameterName = `GetAtt${resourceId}${attr}`; + stackParamsMap[thisStackName][parameterName] = refNode; + template.Parameters[parameterName] = new StringParameter({ + Description: `Auto-generated parameter that forwards Fn.GetAtt(${resourceId}, ${attr}) through to nested stacks.`, + }); + setIn(template, refList, Fn.Ref(parameterName)); + } else if (getAttNeedsReplacing) { + const [resId, attr] = refNode['Fn::GetAtt']; + setIn(template, refList, makeImportValueForGetAtt(resourceId, attr)); + const outputForInput = makeOutputForGetAtt(resourceId, attr); + const referencedStack = stacks[referencedStackName]; + const exportLogicalId = `GetAtt${resourceId}${attr}`; + if (referencedStack && referencedStack.Outputs && !referencedStack.Outputs[exportLogicalId]) { + if (template.Outputs[exportLogicalId]) { + // https://github.com/aws-amplify/amplify-cli/issues/1581 + // Export names are unique and the transformer libraries + // enforce resource id uniqueness as well. Delete the existing + // output if we are adding it to another stack to prevent push failures. + delete template.Outputs[exportLogicalId]; + } + referencedStack.Outputs[exportLogicalId] = outputForInput; } + if (stackDependsOnMap[thisStackName] && !stackDependsOnMap[thisStackName].find(s => s === referencedStackName)) { + stackDependsOnMap[thisStackName].push(referencedStackName); + } + } } + } } + return { + stackDependencyMap: stackDependsOnMap, + stackParameterMap: stackParamsMap, + }; + } - /** - * Forwards all root parameters to each nested stack and adds the GraphQL API - * reference as a parameter. - * @param root The root stack - * @param stacks The list of stacks keyed by filename. - */ - function updateRootWithNestedStacks(root: Template, stacks: { [key: string]: Template }, stackInfo: NestedStackInfo) { - const stackFileNames = Object.keys(stacks); - const allParamNames = Object.keys(root.Parameters); - // Forward all parent parameters - const allParamValues = allParamNames.reduce((acc: any, name: string) => ({ - ...acc, - [name]: Fn.Ref(name) - }), defaultParameterValues) - // Also forward the API id of the top level API. - // allParamValues[ResourceConstants.RESOURCES.GraphQLAPILogicalID] = Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId') - for (const stackName of stackFileNames) { - const dependsOnStacks = stackInfo.stackDependencyMap[stackName] || [] - const extraParams = stackInfo.stackParameterMap[stackName] || {} - let stackResource = new CloudFormation.Stack({ - Parameters: { - ...allParamValues, - ...extraParams - }, - TemplateURL: Fn.Join( - '/', - [ - "https://s3.amazonaws.com", - Fn.Ref(opts.deployment.deploymentBucketParameterName), - Fn.Ref(opts.deployment.deploymentKeyParameterName), - 'stacks', - stackName + ".json" - ] - ) - }).dependsOn([ - ...defaultDependencies, - ...dependsOnStacks - ]) - root.Resources[stackName] = stackResource - } - return root; - } + /** + * Create an import value node that replaces a Ref. + */ + function makeImportValueForRef(resourceId: string): any { + return Fn.ImportValue(Fn.Join(':', [importExportPrefix, 'Ref', resourceId])); + } - const templateJson: any = JSON.parse(JSON.stringify(stack)); - const resourceToStackMap = mapResourcesToStack(templateJson); - const outputToStackMap = mapOutputsToStack(templateJson); - const mappingToStackMap = mapMappingToStack(templateJson); - const stackMapping = { ...resourceToStackMap, ...outputToStackMap, ...mappingToStackMap }; - const stacks = collectTemplates(templateJson, resourceToStackMap, outputToStackMap, stackMapping); - const stackInfo = replaceReferences(stacks, resourceToStackMap); - let rootStack = stacks[rootStackName]; - delete(stacks[rootStackName]); - rootStack = updateRootWithNestedStacks(rootStack, stacks, stackInfo); + /** + * Make an ImportValue node that imports the corresponding export. + * @param resourceId The resource being got + * @param attribute The attribute on the resource + */ + function makeImportValueForGetAtt(resourceId: string, attribute: string): any { + return Fn.ImportValue(Fn.Join(':', [importExportPrefix, 'GetAtt', resourceId, attribute])); + } + + /** + * Make an output record that exports the GetAtt. + * @param resourceId The resource being got + * @param attribute The attribute on the resource + */ + function makeOutputForGetAtt(resourceId: string, attribute: string): Output { return { - rootStack, - stacks, - stackMapping + Value: Fn.GetAtt(resourceId, attribute), + Export: { + Name: Fn.Join(':', [importExportPrefix, 'GetAtt', resourceId, attribute]), + }, + }; + } + + /** + * Make an output record that exports the GetAtt. + * @param resourceId The resource being got + * @param attribute The attribute on the resource + */ + function makeOutputForRef(resourceId: string): Output { + return { + Value: Fn.Ref(resourceId), + Export: { + Name: Fn.Join(':', [importExportPrefix, 'Ref', resourceId]), + }, + }; + } + + /** + * Forwards all root parameters to each nested stack and adds the GraphQL API + * reference as a parameter. + * @param root The root stack + * @param stacks The list of stacks keyed by filename. + */ + function updateRootWithNestedStacks(root: Template, stacks: { [key: string]: Template }, stackInfo: NestedStackInfo) { + const stackFileNames = Object.keys(stacks); + const allParamNames = Object.keys(root.Parameters); + // Forward all parent parameters + const allParamValues = allParamNames.reduce( + (acc: any, name: string) => ({ + ...acc, + [name]: Fn.Ref(name), + }), + defaultParameterValues + ); + // Also forward the API id of the top level API. + // allParamValues[ResourceConstants.RESOURCES.GraphQLAPILogicalID] = Fn.GetAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId') + for (const stackName of stackFileNames) { + const dependsOnStacks = stackInfo.stackDependencyMap[stackName] || []; + const extraParams = stackInfo.stackParameterMap[stackName] || {}; + let stackResource = new CloudFormation.Stack({ + Parameters: { + ...allParamValues, + ...extraParams, + }, + TemplateURL: Fn.Join('/', [ + 'https://s3.amazonaws.com', + Fn.Ref(opts.deployment.deploymentBucketParameterName), + Fn.Ref(opts.deployment.deploymentKeyParameterName), + 'stacks', + stackName + '.json', + ]), + }).dependsOn([...defaultDependencies, ...dependsOnStacks]); + root.Resources[stackName] = stackResource; } + return root; + } + + const templateJson: any = JSON.parse(JSON.stringify(stack)); + const resourceToStackMap = mapResourcesToStack(templateJson); + const outputToStackMap = mapOutputsToStack(templateJson); + const mappingToStackMap = mapMappingToStack(templateJson); + const stackMapping = { ...resourceToStackMap, ...outputToStackMap, ...mappingToStackMap }; + const stacks = collectTemplates(templateJson, resourceToStackMap, outputToStackMap, stackMapping); + const stackInfo = replaceReferences(stacks, resourceToStackMap); + let rootStack = stacks[rootStackName]; + delete stacks[rootStackName]; + rootStack = updateRootWithNestedStacks(rootStack, stacks, stackInfo); + return { + rootStack, + stacks, + stackMapping, + }; } diff --git a/packages/graphql-transformer-core/src/util/transformConfig.ts b/packages/graphql-transformer-core/src/util/transformConfig.ts index 5136d61f7c..40e4568a68 100644 --- a/packages/graphql-transformer-core/src/util/transformConfig.ts +++ b/packages/graphql-transformer-core/src/util/transformConfig.ts @@ -1,60 +1,59 @@ const TRANSFORM_CONFIG_FILE_NAME = `transform.conf.json`; import * as path from 'path'; -import { Template } from "cloudform-types"; +import { Template } from 'cloudform-types'; import { throwIfNotJSONExt } from './fileUtils'; import { ProjectOptions } from './amplifyUtils'; const fs = require('fs-extra'); export interface TransformMigrationConfig { - V1?: { - Resources: string[]; - } + V1?: { + Resources: string[]; + }; } /** * The transform config is specified in transform.conf.json within an Amplify * API project directory. */ export interface TransformConfig { + /** + * The transform library uses a "StackMapping" to determine which stack + * a particular resource belongs to. This "StackMapping" allows individual + * transformer implementations to add resources to a single context and + * reference resources as if they were all members of the same stack. The + * transform formatter takes the single context and the stack mapping + * and splits the context into a valid nested stack where any Fn::Ref or Fn::GetAtt + * is replaced by a Import/Export or Parameter. Users may provide mapping + * overrides to get specific behavior out of the transformer. Users may + * override the default stack mapping to customize behavior. + */ + StackMapping?: { + [resourceId: string]: string; + }; - /** - * The transform library uses a "StackMapping" to determine which stack - * a particular resource belongs to. This "StackMapping" allows individual - * transformer implementations to add resources to a single context and - * reference resources as if they were all members of the same stack. The - * transform formatter takes the single context and the stack mapping - * and splits the context into a valid nested stack where any Fn::Ref or Fn::GetAtt - * is replaced by a Import/Export or Parameter. Users may provide mapping - * overrides to get specific behavior out of the transformer. Users may - * override the default stack mapping to customize behavior. - */ - StackMapping?: { - [resourceId: string]: string - }, - - /** - * Provide build time options to GraphQL Transformer constructor functions. - * Certain options cannot be configured via CloudFormation parameters and - * need to be set at build time. E.G. DeletionPolicies cannot depend on parameters. - */ - TransformerOptions?: { - [transformer: string]: { - [option: string]: any - } - }, + /** + * Provide build time options to GraphQL Transformer constructor functions. + * Certain options cannot be configured via CloudFormation parameters and + * need to be set at build time. E.G. DeletionPolicies cannot depend on parameters. + */ + TransformerOptions?: { + [transformer: string]: { + [option: string]: any; + }; + }; - /** - * For backwards compatibility we store a set of resource logical ids that - * should be preserved in the top level template to prevent deleting - * resources that holds data and that were created before the new nested stack config. - * This should not be used moving forwards. Moving forward, use the StackMapping instead which - * generalizes this behavior. - */ - Migration?: TransformMigrationConfig; + /** + * For backwards compatibility we store a set of resource logical ids that + * should be preserved in the top level template to prevent deleting + * resources that holds data and that were created before the new nested stack config. + * This should not be used moving forwards. Moving forward, use the StackMapping instead which + * generalizes this behavior. + */ + Migration?: TransformMigrationConfig; - /** - * Keeping a track of transformer version changes - */ - Version?: number; + /** + * Keeping a track of transformer version changes + */ + Version?: number; } /** * try to load transformer config from specified projectDir @@ -62,23 +61,23 @@ export interface TransformConfig { * */ export async function loadConfig(projectDir: string): Promise { - let config = {}; - try { - const configPath = path.join(projectDir, TRANSFORM_CONFIG_FILE_NAME); - const configExists = await fs.exists(configPath); - if (configExists) { - const configStr = await fs.readFile(configPath); - config = JSON.parse(configStr.toString()); - } - return config as TransformConfig; - } catch (err) { - return config; + let config = {}; + try { + const configPath = path.join(projectDir, TRANSFORM_CONFIG_FILE_NAME); + const configExists = await fs.exists(configPath); + if (configExists) { + const configStr = await fs.readFile(configPath); + config = JSON.parse(configStr.toString()); } + return config as TransformConfig; + } catch (err) { + return config; + } } export async function writeConfig(projectDir: string, config: TransformConfig): Promise { - const configFilePath = path.join(projectDir, TRANSFORM_CONFIG_FILE_NAME); - await fs.writeFile(configFilePath, JSON.stringify(config, null, 4)); - return config; + const configFilePath = path.join(projectDir, TRANSFORM_CONFIG_FILE_NAME); + await fs.writeFile(configFilePath, JSON.stringify(config, null, 4)); + return config; } /** @@ -86,62 +85,62 @@ export async function writeConfig(projectDir: string, config: TransformConfig): * user defined configuration. */ interface ProjectConfiguration { - schema: string; - resolvers: { - [k: string]: string, - }, - stacks: { - [k: string]: Template - }, - config: TransformConfig + schema: string; + resolvers: { + [k: string]: string; + }; + stacks: { + [k: string]: Template; + }; + config: TransformConfig; } export async function loadProject(projectDirectory: string, opts?: ProjectOptions): Promise { - // Schema - const schema = await readSchema(projectDirectory); - // Load the resolvers. - const resolvers = {} - if (!(opts && opts.disableResolverOverrides === true)) { - const resolverDirectory = path.join(projectDirectory, 'resolvers') - const resolverDirExists = await fs.exists(resolverDirectory); - if (resolverDirExists) { - const resolverFiles = await fs.readdir(resolverDirectory) - for (const resolverFile of resolverFiles) { - if (resolverFile.indexOf('.') === 0) { - continue; - } - const resolverFilePath = path.join(resolverDirectory, resolverFile) - resolvers[resolverFile] = await fs.readFile(resolverFilePath) - } + // Schema + const schema = await readSchema(projectDirectory); + // Load the resolvers. + const resolvers = {}; + if (!(opts && opts.disableResolverOverrides === true)) { + const resolverDirectory = path.join(projectDirectory, 'resolvers'); + const resolverDirExists = await fs.exists(resolverDirectory); + if (resolverDirExists) { + const resolverFiles = await fs.readdir(resolverDirectory); + for (const resolverFile of resolverFiles) { + if (resolverFile.indexOf('.') === 0) { + continue; } + const resolverFilePath = path.join(resolverDirectory, resolverFile); + resolvers[resolverFile] = await fs.readFile(resolverFilePath); + } } - const stacksDirectory = path.join(projectDirectory, 'stacks') - const stacksDirExists = await fs.exists(stacksDirectory) - const stacks = {} - if (stacksDirExists) { - const stackFiles = await fs.readdir(stacksDirectory) - for (const stackFile of stackFiles) { - if (stackFile.indexOf('.') === 0) { - continue; - } + } + const stacksDirectory = path.join(projectDirectory, 'stacks'); + const stacksDirExists = await fs.exists(stacksDirectory); + const stacks = {}; + if (stacksDirExists) { + const stackFiles = await fs.readdir(stacksDirectory); + for (const stackFile of stackFiles) { + if (stackFile.indexOf('.') === 0) { + continue; + } - const stackFilePath = path.join(stacksDirectory, stackFile) - throwIfNotJSONExt(stackFile); - const stackBuffer = await fs.readFile(stackFilePath); - try { - stacks[stackFile] = JSON.parse(stackBuffer.toString()); - } catch (e) { - throw new Error(`The CloudFormation template ${stackFiles} does not contain valid JSON.`) - } - } + const stackFilePath = path.join(stacksDirectory, stackFile); + throwIfNotJSONExt(stackFile); + const stackBuffer = await fs.readFile(stackFilePath); + try { + stacks[stackFile] = JSON.parse(stackBuffer.toString()); + } catch (e) { + throw new Error(`The CloudFormation template ${stackFiles} does not contain valid JSON.`); + } } + } - const config = await loadConfig(projectDirectory); - return { - stacks, - resolvers, - schema, - config - } + const config = await loadConfig(projectDirectory); + return { + stacks, + resolvers, + schema, + config, + }; } /** @@ -151,38 +150,38 @@ export async function loadProject(projectDirectory: string, opts?: ProjectOption * @param projectDirectory The project directory. */ export async function readSchema(projectDirectory: string): Promise { - const schemaFilePath = path.join(projectDirectory, 'schema.graphql') - const schemaDirectoryPath = path.join(projectDirectory, 'schema') - const schemaFileExists = await fs.exists(schemaFilePath); - const schemaDirectoryExists = await fs.exists(schemaDirectoryPath); - let schema; - if (schemaFileExists) { - schema = (await fs.readFile(schemaFilePath)).toString() - } else if (schemaDirectoryExists) { - schema = (await readSchemaDocuments(schemaDirectoryPath)).join('\n'); - } else { - throw new Error(`Could not find a schema at ${schemaFilePath}`) - } - return schema; + const schemaFilePath = path.join(projectDirectory, 'schema.graphql'); + const schemaDirectoryPath = path.join(projectDirectory, 'schema'); + const schemaFileExists = await fs.exists(schemaFilePath); + const schemaDirectoryExists = await fs.exists(schemaDirectoryPath); + let schema; + if (schemaFileExists) { + schema = (await fs.readFile(schemaFilePath)).toString(); + } else if (schemaDirectoryExists) { + schema = (await readSchemaDocuments(schemaDirectoryPath)).join('\n'); + } else { + throw new Error(`Could not find a schema at ${schemaFilePath}`); + } + return schema; } async function readSchemaDocuments(schemaDirectoryPath: string): Promise { - const files = await fs.readdir(schemaDirectoryPath); - let schemaDocuments = []; - for (const fileName of files) { - if (fileName.indexOf('.') === 0) { - continue; - } + const files = await fs.readdir(schemaDirectoryPath); + let schemaDocuments = []; + for (const fileName of files) { + if (fileName.indexOf('.') === 0) { + continue; + } - const fullPath = `${schemaDirectoryPath}/${fileName}`; - const stats = await fs.lstat(fullPath); - if (stats.isDirectory()) { - const childDocs = await readSchemaDocuments(fullPath); - schemaDocuments = schemaDocuments.concat(childDocs); - } else if (stats.isFile()) { - const schemaDoc = await fs.readFile(fullPath); - schemaDocuments.push(schemaDoc); - } + const fullPath = `${schemaDirectoryPath}/${fileName}`; + const stats = await fs.lstat(fullPath); + if (stats.isDirectory()) { + const childDocs = await readSchemaDocuments(fullPath); + schemaDocuments = schemaDocuments.concat(childDocs); + } else if (stats.isFile()) { + const schemaDoc = await fs.readFile(fullPath); + schemaDocuments.push(schemaDoc); } - return schemaDocuments; -} \ No newline at end of file + } + return schemaDocuments; +} diff --git a/packages/graphql-transformer-core/src/validation.ts b/packages/graphql-transformer-core/src/validation.ts index 5121ea56c2..5e86500d6f 100644 --- a/packages/graphql-transformer-core/src/validation.ts +++ b/packages/graphql-transformer-core/src/validation.ts @@ -1,12 +1,17 @@ -import { GraphQLScalarType } from 'graphql' +import { GraphQLScalarType } from 'graphql'; import { - Kind, DocumentNode, TypeSystemDefinitionNode, - DirectiveDefinitionNode, ScalarTypeDefinitionNode, parse, - SchemaDefinitionNode, TypeDefinitionNode -} from 'graphql/language' -import { GraphQLSchema, GraphQLObjectType, isOutputType } from 'graphql/type' -import { validate } from 'graphql/validation' -import { ASTDefinitionBuilder } from 'graphql/utilities/buildASTSchema' + Kind, + DocumentNode, + TypeSystemDefinitionNode, + DirectiveDefinitionNode, + ScalarTypeDefinitionNode, + parse, + SchemaDefinitionNode, + TypeDefinitionNode, +} from 'graphql/language'; +import { GraphQLSchema, GraphQLObjectType, isOutputType } from 'graphql/type'; +import { validate } from 'graphql/validation'; +import { ASTDefinitionBuilder } from 'graphql/utilities/buildASTSchema'; // Spec Section: "Subscriptions with Single Root Field" import { SingleFieldSubscriptions } from 'graphql/validation/rules/SingleFieldSubscriptions'; @@ -56,20 +61,20 @@ import { ProvidedNonNullArguments } from 'graphql/validation/rules/ProvidedNonNu * most clear output when encountering multiple validation errors. */ export const specifiedRules = [ - SingleFieldSubscriptions, - KnownTypeNames, - FragmentsOnCompositeTypes, - VariablesAreInputTypes, - ScalarLeafs, - FieldsOnCorrectType, - KnownDirectives, - KnownArgumentNames, - UniqueArgumentNames, - ValuesOfCorrectType, - VariablesInAllowedPosition, - OverlappingFieldsCanBeMerged, - UniqueInputFieldNames, - ProvidedNonNullArguments + SingleFieldSubscriptions, + KnownTypeNames, + FragmentsOnCompositeTypes, + VariablesAreInputTypes, + ScalarLeafs, + FieldsOnCorrectType, + KnownDirectives, + KnownArgumentNames, + UniqueArgumentNames, + ValuesOfCorrectType, + VariablesInAllowedPosition, + OverlappingFieldsCanBeMerged, + UniqueInputFieldNames, + ProvidedNonNullArguments, ]; const EXTRA_SCALARS_DOCUMENT = parse(` @@ -84,7 +89,7 @@ scalar AWSPhone scalar AWSIPAddress scalar BigInt scalar Double -`) +`); const EXTRA_DIRECTIVES_DOCUMENT = parse(` directive @aws_subscribe(mutations: [String!]!) on FIELD_DEFINITION @@ -96,61 +101,50 @@ directive @aws_cognito_user_pools(cognito_groups: [String!]) on FIELD_DEFINITION # Allows transformer libraries to deprecate directive arguments. directive @deprecated(reason: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ENUM | ENUM_VALUE -`) +`); export function astBuilder(doc: DocumentNode): ASTDefinitionBuilder { - const nodeMap = doc.definitions - .filter((def: TypeSystemDefinitionNode) => def.kind !== Kind.SCHEMA_DEFINITION && Boolean(def.name)) - .reduce( - (a: { [k: string]: TypeDefinitionNode }, def: TypeDefinitionNode) => ({ - ...a, - [def.name.value]: def - }), {}) - return new ASTDefinitionBuilder( - nodeMap, - {}, - typeRef => { - throw new Error(`Type "${typeRef.name.value}" not found in document.`); - }, - ) + const nodeMap = doc.definitions + .filter((def: TypeSystemDefinitionNode) => def.kind !== Kind.SCHEMA_DEFINITION && Boolean(def.name)) + .reduce( + (a: { [k: string]: TypeDefinitionNode }, def: TypeDefinitionNode) => ({ + ...a, + [def.name.value]: def, + }), + {} + ); + return new ASTDefinitionBuilder(nodeMap, {}, typeRef => { + throw new Error(`Type "${typeRef.name.value}" not found in document.`); + }); } export function validateModelSchema(doc: DocumentNode) { - const fullDocument = { - kind: Kind.DOCUMENT, - definitions: [ - ...EXTRA_DIRECTIVES_DOCUMENT.definitions, - ...doc.definitions, - ...EXTRA_SCALARS_DOCUMENT.definitions, - ] - } - const builder = astBuilder(fullDocument) - const directives = fullDocument.definitions - .filter(d => d.kind === Kind.DIRECTIVE_DEFINITION) - .map((d: DirectiveDefinitionNode) => { - return builder.buildDirective(d) - }) - const types = fullDocument.definitions - .filter(d => d.kind !== Kind.DIRECTIVE_DEFINITION && d.kind !== Kind.SCHEMA_DEFINITION) - .map((d: TypeDefinitionNode) => builder.buildType(d)) - const outputTypes = types.filter( - t => isOutputType(t) - ) - const fields = outputTypes.reduce( - (acc, t) => ({ ...acc, [t.name]: { type: t } }), - {} - ) - - const schemaRecord = doc.definitions.find(d => d.kind === Kind.SCHEMA_DEFINITION) as SchemaDefinitionNode; - const queryOp = schemaRecord ? schemaRecord.operationTypes.find(o => o.operation === 'query') : undefined; - const queryName = queryOp ? queryOp.type.name.value : 'Query'; - const existingQueryType = types.find(t => t.name === queryName) as GraphQLObjectType; - const queryType = existingQueryType ? - existingQueryType : - new GraphQLObjectType({ - name: queryName, - fields - }) - const schema = new GraphQLSchema({ query: queryType, types, directives }); - return validate(schema, fullDocument, specifiedRules) + const fullDocument = { + kind: Kind.DOCUMENT, + definitions: [...EXTRA_DIRECTIVES_DOCUMENT.definitions, ...doc.definitions, ...EXTRA_SCALARS_DOCUMENT.definitions], + }; + const builder = astBuilder(fullDocument); + const directives = fullDocument.definitions + .filter(d => d.kind === Kind.DIRECTIVE_DEFINITION) + .map((d: DirectiveDefinitionNode) => { + return builder.buildDirective(d); + }); + const types = fullDocument.definitions + .filter(d => d.kind !== Kind.DIRECTIVE_DEFINITION && d.kind !== Kind.SCHEMA_DEFINITION) + .map((d: TypeDefinitionNode) => builder.buildType(d)); + const outputTypes = types.filter(t => isOutputType(t)); + const fields = outputTypes.reduce((acc, t) => ({ ...acc, [t.name]: { type: t } }), {}); + + const schemaRecord = doc.definitions.find(d => d.kind === Kind.SCHEMA_DEFINITION) as SchemaDefinitionNode; + const queryOp = schemaRecord ? schemaRecord.operationTypes.find(o => o.operation === 'query') : undefined; + const queryName = queryOp ? queryOp.type.name.value : 'Query'; + const existingQueryType = types.find(t => t.name === queryName) as GraphQLObjectType; + const queryType = existingQueryType + ? existingQueryType + : new GraphQLObjectType({ + name: queryName, + fields, + }); + const schema = new GraphQLSchema({ query: queryType, types, directives }); + return validate(schema, fullDocument, specifiedRules); } diff --git a/packages/graphql-transformers-e2e-tests/src/CloudFormationClient.ts b/packages/graphql-transformers-e2e-tests/src/CloudFormationClient.ts index 1473513635..34a4cc5ae0 100644 --- a/packages/graphql-transformers-e2e-tests/src/CloudFormationClient.ts +++ b/packages/graphql-transformers-e2e-tests/src/CloudFormationClient.ts @@ -1,148 +1,153 @@ -import { CloudFormation } from 'aws-sdk' +import { CloudFormation } from 'aws-sdk'; import { DescribeStacksOutput, StackStatus } from 'aws-sdk/clients/cloudformation'; -import { ResourceConstants } from 'graphql-transformer-common' +import { ResourceConstants } from 'graphql-transformer-common'; -async function promisify( - fun: (arg: I, cb: (e: Error, d: O) => void) => void, - args: I, - that: any -): Promise { - return await new Promise((resolve, reject) => { - fun.apply( - that, - [ - args, - (err: Error, data: O) => { - if (err) { - return reject(err) - } - resolve(data) - } - ] - ) - }) +async function promisify(fun: (arg: I, cb: (e: Error, d: O) => void) => void, args: I, that: any): Promise { + return await new Promise((resolve, reject) => { + fun.apply(that, [ + args, + (err: Error, data: O) => { + if (err) { + return reject(err); + } + resolve(data); + }, + ]); + }); } export class CloudFormationClient { + client: CloudFormation; - client: CloudFormation - - constructor(public region: string) { - this.client = new CloudFormation({ apiVersion: '2010-05-15', region: this.region }); - } - - async createStack(template: any, name: string, defParams: any = {}, addAppSyncApiName: boolean = true) { - const params = []; + constructor(public region: string) { + this.client = new CloudFormation({ apiVersion: '2010-05-15', region: this.region }); + } - if (addAppSyncApiName === true) { - params.push({ - ParameterKey: ResourceConstants.PARAMETERS.AppSyncApiName, - ParameterValue: name - }); - } - - for (const key of Object.keys(defParams)) { - params.push({ - ParameterKey: key, - ParameterValue: defParams[key] - }) - } + async createStack(template: any, name: string, defParams: any = {}, addAppSyncApiName: boolean = true) { + const params = []; - // add env info to template - template.Parameters.env = { - "Type": "String", - "Description": "env name", - "Default": "NONE" - }; - - return await promisify( - this.client.createStack, - { - StackName: name, - Capabilities: ['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], - Parameters: params, - TemplateBody: JSON.stringify(template) - }, - this.client - ) + if (addAppSyncApiName === true) { + params.push({ + ParameterKey: ResourceConstants.PARAMETERS.AppSyncApiName, + ParameterValue: name, + }); } - async deleteStack(name: string) { - return await promisify( - this.client.deleteStack, - { StackName: name }, - this.client - ) + for (const key of Object.keys(defParams)) { + params.push({ + ParameterKey: key, + ParameterValue: defParams[key], + }); } - async describeStack(name: string): Promise { - return await new Promise((resolve, reject) => { - this.client.describeStacks({ - StackName: name - }, (err: Error, data: DescribeStacksOutput) => { - if (err) { - return reject(err) - } - if (data.Stacks.length !== 1) { - return reject(`No stack named: ${name}`) - } - resolve(data.Stacks[0]) - }) - }) - } + // add env info to template + template.Parameters.env = { + Type: 'String', + Description: 'env name', + Default: 'NONE', + }; + + return await promisify( + this.client.createStack, + { + StackName: name, + Capabilities: ['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], + Parameters: params, + TemplateBody: JSON.stringify(template), + }, + this.client + ); + } - /** - * Periodically polls a stack waiting for a status change. If the status - * changes to success then this resolves if it changes to error then it rejects. - * @param name: The stack name to wait for - * @param success: The status' that indicate success. - * @param failure: The status' that indicate failure. - * @param poll: The status' that indicate to keep polling. - * @param maxPolls: The max number of times to poll. - * @param pollInterval: The frequency of polling. - */ - async waitForStack( - name: string, - success: StackStatus[] = ["CREATE_COMPLETE", "ROLLBACK_COMPLETE", "DELETE_COMPLETE", "UPDATE_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"], - failure: StackStatus[] = ["CREATE_FAILED", "ROLLBACK_FAILED", "DELETE_FAILED", "UPDATE_ROLLBACK_FAILED"], - poll: StackStatus[] = [ - "CREATE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS", "UPDATE_IN_PROGRESS", "REVIEW_IN_PROGRESS", "DELETE_IN_PROGRESS", - "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", "UPDATE_ROLLBACK_IN_PROGRESS"], - maxPolls: number = 1000, - pollInterval: number = 20, - ): Promise { - const stack = await this.describeStack(name); - if (success.includes(stack.StackStatus)) { - console.log(`Cloudformation successfully deployed...`) - return Promise.resolve(stack) - } else if (failure.includes(stack.StackStatus)) { - console.log(`Cloudformation failed...`) - console.log(JSON.stringify(stack, null, 4)) - return Promise.reject(new Error(`Stack ${stack.StackName} failed with status "${stack.StackStatus}"`)) - } else if (poll.includes(stack.StackStatus)) { - console.log(`Polling cloudformation...`) - if (maxPolls === 0) { - return Promise.reject(new Error(`Stack did not finish before hitting the max poll count.`)) - } else { - return await this.wait( - pollInterval, this.waitForStack, name, success, failure, poll, maxPolls - 1, pollInterval - ) - } + async deleteStack(name: string) { + return await promisify(this.client.deleteStack, { StackName: name }, this.client); + } + + async describeStack(name: string): Promise { + return await new Promise((resolve, reject) => { + this.client.describeStacks( + { + StackName: name, + }, + (err: Error, data: DescribeStacksOutput) => { + if (err) { + return reject(err); + } + if (data.Stacks.length !== 1) { + return reject(`No stack named: ${name}`); + } + resolve(data.Stacks[0]); } - return Promise.reject(new Error('Invalid stack status: ' + stack.StackStatus)) - } + ); + }); + } - /** - * Promise wrapper around setTimeout. - * @param secs The number of seconds to wait. - * @param fun The function to call after waiting. - * @param args The arguments to pass to the function after the wait. - */ - public async wait(secs: number, fun: (...args: any[]) => Promise, ...args: any[]): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve(fun.apply(this, args)) - }, 1000 * secs) - }) + /** + * Periodically polls a stack waiting for a status change. If the status + * changes to success then this resolves if it changes to error then it rejects. + * @param name: The stack name to wait for + * @param success: The status' that indicate success. + * @param failure: The status' that indicate failure. + * @param poll: The status' that indicate to keep polling. + * @param maxPolls: The max number of times to poll. + * @param pollInterval: The frequency of polling. + */ + async waitForStack( + name: string, + success: StackStatus[] = ['CREATE_COMPLETE', 'ROLLBACK_COMPLETE', 'DELETE_COMPLETE', 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_COMPLETE'], + failure: StackStatus[] = ['CREATE_FAILED', 'ROLLBACK_FAILED', 'DELETE_FAILED', 'UPDATE_ROLLBACK_FAILED'], + poll: StackStatus[] = [ + 'CREATE_IN_PROGRESS', + 'ROLLBACK_IN_PROGRESS', + 'UPDATE_IN_PROGRESS', + 'REVIEW_IN_PROGRESS', + 'DELETE_IN_PROGRESS', + 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_ROLLBACK_IN_PROGRESS', + ], + maxPolls: number = 1000, + pollInterval: number = 20 + ): Promise { + const stack = await this.describeStack(name); + if (success.includes(stack.StackStatus)) { + console.log(`Cloudformation successfully deployed...`); + return Promise.resolve(stack); + } else if (failure.includes(stack.StackStatus)) { + console.log(`Cloudformation failed...`); + console.log(JSON.stringify(stack, null, 4)); + return Promise.reject(new Error(`Stack ${stack.StackName} failed with status "${stack.StackStatus}"`)); + } else if (poll.includes(stack.StackStatus)) { + console.log(`Polling cloudformation...`); + if (maxPolls === 0) { + return Promise.reject(new Error(`Stack did not finish before hitting the max poll count.`)); + } else { + return await this.wait( + pollInterval, + this.waitForStack, + name, + success, + failure, + poll, + maxPolls - 1, + pollInterval + ); + } } + return Promise.reject(new Error('Invalid stack status: ' + stack.StackStatus)); + } + + /** + * Promise wrapper around setTimeout. + * @param secs The number of seconds to wait. + * @param fun The function to call after waiting. + * @param args The arguments to pass to the function after the wait. + */ + public async wait(secs: number, fun: (...args: any[]) => Promise, ...args: any[]): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(fun.apply(this, args)); + }, 1000 * secs); + }); + } } diff --git a/packages/graphql-transformers-e2e-tests/src/GraphQLClient.ts b/packages/graphql-transformers-e2e-tests/src/GraphQLClient.ts index 1c229cb694..0a0ab01bec 100644 --- a/packages/graphql-transformers-e2e-tests/src/GraphQLClient.ts +++ b/packages/graphql-transformers-e2e-tests/src/GraphQLClient.ts @@ -1,28 +1,30 @@ -import axios from 'axios' +import axios from 'axios'; export interface GraphQLLocation { - line: number; - column: number; + line: number; + column: number; } export interface GraphQLError { - message: string; - locations: GraphQLLocation[]; - path: string[] + message: string; + locations: GraphQLLocation[]; + path: string[]; } export interface GraphQLResponse { - data: any; - errors: GraphQLError[] + data: any; + errors: GraphQLError[]; } export class GraphQLClient { - constructor(private url: string, private headers: any) { } + constructor(private url: string, private headers: any) {} - async query(query: string, variables: any): Promise { - const axRes = await axios.post( - this.url, { - query, - variables - }, { headers: this.headers } - ) - return axRes.data - } -} \ No newline at end of file + async query(query: string, variables: any): Promise { + const axRes = await axios.post( + this.url, + { + query, + variables, + }, + { headers: this.headers } + ); + return axRes.data; + } +} diff --git a/packages/graphql-transformers-e2e-tests/src/IAMHelper.ts b/packages/graphql-transformers-e2e-tests/src/IAMHelper.ts index 0c4e7a241c..85a488c898 100644 --- a/packages/graphql-transformers-e2e-tests/src/IAMHelper.ts +++ b/packages/graphql-transformers-e2e-tests/src/IAMHelper.ts @@ -1,16 +1,17 @@ import { IAM } from 'aws-sdk'; export class IAMHelper { - client: IAM; - constructor(region: string = 'us-west-2') { - this.client = new IAM({ - region - }) - } + client: IAM; + constructor(region: string = 'us-west-2') { + this.client = new IAM({ + region, + }); + } - async createLambdaExecutionRole(name: string) { - return await this.client.createRole({ - AssumeRolePolicyDocument: `{ + async createLambdaExecutionRole(name: string) { + return await this.client + .createRole({ + AssumeRolePolicyDocument: `{ "Version": "2012-10-17", "Statement": [ { @@ -22,13 +23,15 @@ export class IAMHelper { } ] }`, - RoleName: name - }).promise(); - } + RoleName: name, + }) + .promise(); + } - async createLambdaExecutionPolicy(name: string) { - return await this.client.createPolicy({ - PolicyDocument: `{ + async createLambdaExecutionPolicy(name: string) { + return await this.client + .createPolicy({ + PolicyDocument: `{ "Version": "2012-10-17", "Statement": [ { @@ -42,29 +45,34 @@ export class IAMHelper { } ] }`, - PolicyName: name - }).promise() - } + PolicyName: name, + }) + .promise(); + } - async attachLambdaExecutionPolicy(policyArn: string, roleName: string) { - return await this.client.attachRolePolicy({ - PolicyArn: policyArn, - RoleName: roleName - }).promise() - } + async attachLambdaExecutionPolicy(policyArn: string, roleName: string) { + return await this.client + .attachRolePolicy({ + PolicyArn: policyArn, + RoleName: roleName, + }) + .promise(); + } - async deletePolicy(policyArn: string) { - return await this.client.deletePolicy({PolicyArn: policyArn}).promise(); - } + async deletePolicy(policyArn: string) { + return await this.client.deletePolicy({ PolicyArn: policyArn }).promise(); + } - async deleteRole(roleName: string) { - return await this.client.deleteRole({RoleName: roleName}).promise(); - } + async deleteRole(roleName: string) { + return await this.client.deleteRole({ RoleName: roleName }).promise(); + } - async detachLambdaExecutionPolicy(policyArn: string, roleName: string) { - return await this.client.detachRolePolicy({ - PolicyArn: policyArn, - RoleName: roleName - }).promise() - } -} \ No newline at end of file + async detachLambdaExecutionPolicy(policyArn: string, roleName: string) { + return await this.client + .detachRolePolicy({ + PolicyArn: policyArn, + RoleName: roleName, + }) + .promise(); + } +} diff --git a/packages/graphql-transformers-e2e-tests/src/LambdaHelper.ts b/packages/graphql-transformers-e2e-tests/src/LambdaHelper.ts index ec60927dcc..cd8925298b 100644 --- a/packages/graphql-transformers-e2e-tests/src/LambdaHelper.ts +++ b/packages/graphql-transformers-e2e-tests/src/LambdaHelper.ts @@ -3,29 +3,31 @@ import * as fs from 'fs'; import * as path from 'path'; export class LambdaHelper { - client: Lambda; - constructor(region: string = 'us-west-2') { - this.client = new Lambda({ - region - }) - } + client: Lambda; + constructor(region: string = 'us-west-2') { + this.client = new Lambda({ + region, + }); + } - async createFunction(name: string, roleArn: string, filePrefix: string) { - const filePath = path.join(__dirname, 'testfunctions', `${filePrefix}.zip`); - console.log(`Uploading lambda from path: ${filePath}`) - const zipContents = fs.readFileSync(filePath); - return await this.client.createFunction({ - FunctionName: name, - Code: { - ZipFile: zipContents, - }, - Runtime: 'nodejs8.10', - Handler: `${filePrefix}.handler`, - Role: roleArn - }).promise(); - } + async createFunction(name: string, roleArn: string, filePrefix: string) { + const filePath = path.join(__dirname, 'testfunctions', `${filePrefix}.zip`); + console.log(`Uploading lambda from path: ${filePath}`); + const zipContents = fs.readFileSync(filePath); + return await this.client + .createFunction({ + FunctionName: name, + Code: { + ZipFile: zipContents, + }, + Runtime: 'nodejs8.10', + Handler: `${filePrefix}.handler`, + Role: roleArn, + }) + .promise(); + } - async deleteFunction(name: string) { - return await this.client.deleteFunction({FunctionName: name}).promise(); - } -} \ No newline at end of file + async deleteFunction(name: string) { + return await this.client.deleteFunction({ FunctionName: name }).promise(); + } +} diff --git a/packages/graphql-transformers-e2e-tests/src/S3Client.ts b/packages/graphql-transformers-e2e-tests/src/S3Client.ts index 4d0c23e8c9..e77dd2f1b4 100644 --- a/packages/graphql-transformers-e2e-tests/src/S3Client.ts +++ b/packages/graphql-transformers-e2e-tests/src/S3Client.ts @@ -1,169 +1,160 @@ -import { S3 } from 'aws-sdk' +import { S3 } from 'aws-sdk'; import fs = require('fs'); -async function promisify( - fun: (arg: I, cb: (e: Error, d: O) => void) => void, - args: I, - that: any -): Promise { - return await new Promise((resolve, reject) => { - fun.apply( - that, - [ - args, - (err: Error, data: O) => { - if (err) { - return reject(err) - } - resolve(data) - } - ] - ) - }) +async function promisify(fun: (arg: I, cb: (e: Error, d: O) => void) => void, args: I, that: any): Promise { + return await new Promise((resolve, reject) => { + fun.apply(that, [ + args, + (err: Error, data: O) => { + if (err) { + return reject(err); + } + resolve(data); + }, + ]); + }); } export class S3Client { - - client: S3 - - constructor(public region: string) { - this.client = new S3({ region: this.region }); - } - - async createBucket(bucketName: string) { - return await promisify( - this.client.createBucket, - { - Bucket: bucketName, - }, - this.client - ) - } - - async putBucketVersioning(bucketName: string) { - return await promisify( - this.client.putBucketVersioning, - { - Bucket: bucketName, - VersioningConfiguration: { - Status: "Enabled" - } - }, - this.client - ) - } - - async uploadZIPFile(bucketName: string, filePath: string, s3key: string, contentType: string = 'application/zip') { - const fileContent = this.readZIPFile(filePath) - - return await promisify( - this.client.putObject, - { - Bucket: bucketName, - Key: s3key, - Body: fileContent, - ContentType: contentType - }, - this.client - ) - } - - async uploadFile(bucketName: string, filePath: string, s3key: string) { - - const fileContent = this.readFile(filePath) - - return await promisify( - this.client.putObject, - { - Bucket: bucketName, - Key: s3key, - Body: fileContent - }, - this.client - ) - } - - async getFileVersion(bucketName: string, s3key: string) { - return await promisify( - this.client.getObject, - { - Bucket: bucketName, - Key: s3key - }, - this.client - ) - } - - async getAllObjectVersions(bucketName: string) { - return await promisify( - this.client.listObjectVersions, - { - Bucket: bucketName - }, - this.client - ) + client: S3; + + constructor(public region: string) { + this.client = new S3({ region: this.region }); + } + + async createBucket(bucketName: string) { + return await promisify( + this.client.createBucket, + { + Bucket: bucketName, + }, + this.client + ); + } + + async putBucketVersioning(bucketName: string) { + return await promisify( + this.client.putBucketVersioning, + { + Bucket: bucketName, + VersioningConfiguration: { + Status: 'Enabled', + }, + }, + this.client + ); + } + + async uploadZIPFile(bucketName: string, filePath: string, s3key: string, contentType: string = 'application/zip') { + const fileContent = this.readZIPFile(filePath); + + return await promisify( + this.client.putObject, + { + Bucket: bucketName, + Key: s3key, + Body: fileContent, + ContentType: contentType, + }, + this.client + ); + } + + async uploadFile(bucketName: string, filePath: string, s3key: string) { + const fileContent = this.readFile(filePath); + + return await promisify( + this.client.putObject, + { + Bucket: bucketName, + Key: s3key, + Body: fileContent, + }, + this.client + ); + } + + async getFileVersion(bucketName: string, s3key: string) { + return await promisify( + this.client.getObject, + { + Bucket: bucketName, + Key: s3key, + }, + this.client + ); + } + + async getAllObjectVersions(bucketName: string) { + return await promisify( + this.client.listObjectVersions, + { + Bucket: bucketName, + }, + this.client + ); + } + + async deleteObjectVersion(bucketName: string, versionId: string, s3key: string) { + return await promisify( + this.client.deleteObject, + { + Bucket: bucketName, + Key: s3key, + VersionId: versionId, + }, + this.client + ); + } + + async deleteFile(bucketName: string, s3key: string) { + const response = await this.getAllObjectVersions(bucketName); + const versions = response.Versions; + for (const version of versions) { + await this.deleteObjectVersion(bucketName, version.VersionId, s3key); } - - async deleteObjectVersion(bucketName: string, versionId: string, s3key: string) { - return await promisify( - this.client.deleteObject, - { - Bucket: bucketName, - Key: s3key, - VersionId: versionId - }, - this.client - ) - } - - async deleteFile(bucketName: string, s3key: string) { - const response = await this.getAllObjectVersions(bucketName) - const versions = response.Versions - for (const version of versions) { - await this.deleteObjectVersion(bucketName, version.VersionId, s3key) - } - } - - async deleteBucket(bucketName: string) { - return await promisify( - this.client.deleteBucket, - { - Bucket: bucketName - }, - this.client - ) - } - - async setUpS3Resources(bucketName: string, filePath: string, s3key: string, zip?: boolean) { - await this.createBucket(bucketName) - await this.putBucketVersioning(bucketName) - if (zip) { - await this.uploadZIPFile(bucketName, filePath, s3key) - } else { - await this.uploadFile(bucketName, filePath, s3key) - } - return await this.getFileVersion(bucketName, s3key) - } - - async cleanUpS3Resources(bucketName: string, s3key: string) { - await this.deleteFile(bucketName, s3key) - await this.deleteBucket(bucketName) - } - - private readFile(filePath: string) { - return fs.readFileSync(filePath, "utf8") - } - - private readZIPFile(filePath: string) { - return fs.createReadStream(filePath) - } - - public async wait(secs: number, fun: (...args: any[]) => Promise, ...args: any[]): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve(fun.apply(this, args)) - }, 1000 * secs) - }) + } + + async deleteBucket(bucketName: string) { + return await promisify( + this.client.deleteBucket, + { + Bucket: bucketName, + }, + this.client + ); + } + + async setUpS3Resources(bucketName: string, filePath: string, s3key: string, zip?: boolean) { + await this.createBucket(bucketName); + await this.putBucketVersioning(bucketName); + if (zip) { + await this.uploadZIPFile(bucketName, filePath, s3key); + } else { + await this.uploadFile(bucketName, filePath, s3key); } + return await this.getFileVersion(bucketName, s3key); + } + + async cleanUpS3Resources(bucketName: string, s3key: string) { + await this.deleteFile(bucketName, s3key); + await this.deleteBucket(bucketName); + } + + private readFile(filePath: string) { + return fs.readFileSync(filePath, 'utf8'); + } + + private readZIPFile(filePath: string) { + return fs.createReadStream(filePath); + } + + public async wait(secs: number, fun: (...args: any[]) => Promise, ...args: any[]): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(fun.apply(this, args)); + }, 1000 * secs); + }); + } } diff --git a/packages/graphql-transformers-e2e-tests/src/TestStorage.ts b/packages/graphql-transformers-e2e-tests/src/TestStorage.ts index 154c2ac4af..4069410c44 100644 --- a/packages/graphql-transformers-e2e-tests/src/TestStorage.ts +++ b/packages/graphql-transformers-e2e-tests/src/TestStorage.ts @@ -1,28 +1,28 @@ export default class TestStorage { - private data: any - constructor() { - this.data = {} - } - // set item with the key - public setItem(key: string, value: string): string { - this.data[key] = value - return value - } - // get item with the key - public getItem(key: string): string { - return this.data[key] - } - // remove item with the key - public removeItem(key: string): void { - this.data[key] = undefined - } - // clear out the storage - public clear(): void { - this.data = {} - } - // If the storage operations are async(i.e AsyncStorage) - // Then you need to sync those items into the memory in this method - public sync(): Promise { - return Promise.resolve(this.data) - } -} \ No newline at end of file + private data: any; + constructor() { + this.data = {}; + } + // set item with the key + public setItem(key: string, value: string): string { + this.data[key] = value; + return value; + } + // get item with the key + public getItem(key: string): string { + return this.data[key]; + } + // remove item with the key + public removeItem(key: string): void { + this.data[key] = undefined; + } + // clear out the storage + public clear(): void { + this.data = {}; + } + // If the storage operations are async(i.e AsyncStorage) + // Then you need to sync those items into the memory in this method + public sync(): Promise { + return Promise.resolve(this.data); + } +} diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts index 74590dd28f..ebf68023b8 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ConnectionsWithAuthTests.e2e.test.ts @@ -1,38 +1,42 @@ import Amplify from 'aws-amplify'; -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import ModelConnectionTransformer from 'graphql-connection-transformer' -import * as fs from 'fs' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import * as S3 from 'aws-sdk/clients/s3' -import { CreateBucketRequest } from 'aws-sdk/clients/s3' -import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import ModelConnectionTransformer from 'graphql-connection-transformer'; +import * as fs from 'fs'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import * as S3 from 'aws-sdk/clients/s3'; +import { CreateBucketRequest } from 'aws-sdk/clients/s3'; +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; +import { GraphQLClient } from '../GraphQLClient'; import { S3Client } from '../S3Client'; -import { deploy } from '../deployNestedStacks' +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; import { - createUserPool, createUserPoolClient, deleteUserPool, - signupAndAuthenticateUser, createGroup, addUserToGroup, - configureAmplify + createUserPool, + createUserPoolClient, + deleteUserPool, + signupAndAuthenticateUser, + createGroup, + addUserToGroup, + configureAmplify, } from '../cognitoUtils'; // to deal with bug in cognito-identity-js -(global as any).fetch = require("node-fetch"); +(global as any).fetch = require('node-fetch'); jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') +const cf = new CloudFormationClient('us-west-2'); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `ConnectionsWithAuthTests-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `connections-with-auth-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_BUILD_ROOT = '/tmp/connections_with_auth_test/' -const DEPLOYMENT_ROOT_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `ConnectionsWithAuthTests-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `connections-with-auth-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_BUILD_ROOT = '/tmp/connections_with_auth_test/'; +const DEPLOYMENT_ROOT_KEY = 'deployments'; let GRAPHQL_ENDPOINT = undefined; @@ -53,53 +57,53 @@ let GRAPHQL_CLIENT_3 = undefined; let USER_POOL_ID = undefined; -const USERNAME1 = 'user1@test.com' -const USERNAME2 = 'user2@test.com' -const USERNAME3 = 'user3@test.com' -const TMP_PASSWORD = 'Password123!' -const REAL_PASSWORD = 'Password1234!' +const USERNAME1 = 'user1@test.com'; +const USERNAME2 = 'user2@test.com'; +const USERNAME3 = 'user3@test.com'; +const TMP_PASSWORD = 'Password123!'; +const REAL_PASSWORD = 'Password1234!'; const ADMIN_GROUP_NAME = 'Admin'; const DEVS_GROUP_NAME = 'Devs'; const PARTICIPANT_GROUP_NAME = 'Participant'; const WATCHER_GROUP_NAME = 'Watcher'; -const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }) -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } async function createBucket(name: string) { - return new Promise((res, rej) => { - const params: CreateBucketRequest = { - Bucket: name, - } - awsS3Client.createBucket(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + }; + awsS3Client.createBucket(params, (err, data) => (err ? rej(err) : res(data))); + }); } async function deleteBucket(name: string) { - return new Promise((res, rej) => { - const params: CreateBucketRequest = { - Bucket: name, - } - awsS3Client.deleteBucket(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + }; + awsS3Client.deleteBucket(params, (err, data) => (err ? rej(err) : res(data))); + }); } beforeAll(async () => { - // Create a stack for the post model with auth enabled. - if (!fs.existsSync(LOCAL_BUILD_ROOT)) { - fs.mkdirSync(LOCAL_BUILD_ROOT); - } - await createBucket(BUCKET_NAME) - const validSchema = ` + // Create a stack for the post model with auth enabled. + if (!fs.existsSync(LOCAL_BUILD_ROOT)) { + fs.mkdirSync(LOCAL_BUILD_ROOT); + } + await createBucket(BUCKET_NAME); + const validSchema = ` type Post @model( subscriptions: { level: public @@ -144,129 +148,142 @@ beforeAll(async () => { owner: String topLevel: OpenTopLevel @connection(name: "ProtectedConnection") } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}), - ] - }) - const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); - USER_POOL_ID = userPoolResponse.UserPool.Id; - const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); - const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; - try { - // Clean the bucket - const out = transformer.transform(validSchema) - - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { AuthCognitoUserPoolId: USER_POOL_ID }, LOCAL_BUILD_ROOT, BUCKET_NAME, DEPLOYMENT_ROOT_KEY, - BUILD_TIMESTAMP - ) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy() - expect(USER_POOL_ID).toBeTruthy() - expect(userPoolClientId).toBeTruthy() - - // Configure Amplify, create users, and sign in. - configureAmplify(USER_POOL_ID, userPoolClientId) - - const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - - await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME) - await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME) - await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME) - await createGroup(USER_POOL_ID, DEVS_GROUP_NAME) - await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID) - const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - - const idToken = authResAfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }) - - const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }) - - const idToken3 = authRes3.getIdToken().getJwtToken() - GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }) - - // Wait for any propagation to avoid random - // "The security token included in the request is invalid" errors - await new Promise((res) => setTimeout(() => res(), 5000)) - } catch (e) { - console.error(e) - throw e; - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelConnectionTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + try { + // Clean the bucket + const out = transformer.transform(validSchema); + + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { AuthCognitoUserPoolId: USER_POOL_ID }, + LOCAL_BUILD_ROOT, + BUCKET_NAME, + DEPLOYMENT_ROOT_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + expect(USER_POOL_ID).toBeTruthy(); + expect(userPoolClientId).toBeTruthy(); + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId); + + const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + + await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME); + await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME); + await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME); + await createGroup(USER_POOL_ID, DEVS_GROUP_NAME); + await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID); + const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + + const idToken = authResAfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }); + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }); + + const idToken3 = authRes3.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }); + + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + throw e; + } }); - afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await deleteUserPool(cognitoClient, USER_POOL_ID) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e); - throw e; - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await deleteUserPool(cognitoClient, USER_POOL_ID); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + throw e; } -}) - + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Tests */ test('Test creating a post and immediately view it via the User.posts connection.', async () => { - const createUser1 = await GRAPHQL_CLIENT_1.query(`mutation { + const createUser1 = await GRAPHQL_CLIENT_1.query( + `mutation { createUser(input: { id: "user1@test.com" }) { id } - }`, {}) - console.log(createUser1); - expect(createUser1.data.createUser.id).toEqual("user1@test.com") - - const response = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(createUser1); + expect(createUser1.data.createUser.id).toEqual('user1@test.com'); + + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title owner } - }`, {}) - console.log(response); - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.owner).toBeDefined() - - const getResponse = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + console.log(response); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.owner).toBeDefined(); + + const getResponse = await GRAPHQL_CLIENT_1.query( + `query { getUser(id: "user1@test.com") { posts { items { @@ -279,77 +296,95 @@ test('Test creating a post and immediately view it via the User.posts connection } } } - }`, {}) - console.log(JSON.stringify(getResponse, null, 4)); - expect(getResponse.data.getUser.posts.items[0].id).toBeDefined() - expect(getResponse.data.getUser.posts.items[0].title).toEqual("Hello, World!") - expect(getResponse.data.getUser.posts.items[0].owner).toEqual("user1@test.com") - expect(getResponse.data.getUser.posts.items[0].author.id).toEqual("user1@test.com") -}) + }`, + {} + ); + console.log(JSON.stringify(getResponse, null, 4)); + expect(getResponse.data.getUser.posts.items[0].id).toBeDefined(); + expect(getResponse.data.getUser.posts.items[0].title).toEqual('Hello, World!'); + expect(getResponse.data.getUser.posts.items[0].owner).toEqual('user1@test.com'); + expect(getResponse.data.getUser.posts.items[0].author.id).toEqual('user1@test.com'); +}); test('Testing reading an owner protected field as a non owner', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "1", owner: "${USERNAME1}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(response1); - expect(response1.data.createFieldProtected.id).toEqual("1") - expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1) - expect(response1.data.createFieldProtected.ownerOnly).toEqual("owner-protected") - - const response2 = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + console.log(response1); + expect(response1.data.createFieldProtected.id).toEqual('1'); + expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1); + expect(response1.data.createFieldProtected.ownerOnly).toEqual('owner-protected'); + + const response2 = await GRAPHQL_CLIENT_2.query( + `query { getFieldProtected(id: "1") { id owner ownerOnly } - }`, {}) - console.log(response2); - expect(response2.data.getFieldProtected.ownerOnly).toBeNull() - expect(response2.errors).toHaveLength(1) - - const response3 = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + console.log(response2); + expect(response2.data.getFieldProtected.ownerOnly).toBeNull(); + expect(response2.errors).toHaveLength(1); + + const response3 = await GRAPHQL_CLIENT_1.query( + `query { getFieldProtected(id: "1") { id owner ownerOnly } - }`, {}) - console.log(response3); - expect(response3.data.getFieldProtected.id).toEqual("1") - expect(response3.data.getFieldProtected.owner).toEqual(USERNAME1) - expect(response3.data.getFieldProtected.ownerOnly).toEqual("owner-protected") -}) + }`, + {} + ); + console.log(response3); + expect(response3.data.getFieldProtected.id).toEqual('1'); + expect(response3.data.getFieldProtected.owner).toEqual(USERNAME1); + expect(response3.data.getFieldProtected.ownerOnly).toEqual('owner-protected'); +}); test('Test that @connection resolvers respect @model read operations.', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createOpenTopLevel(input: { id: "1", owner: "${USERNAME1}", name: "open" }) { id owner name } - }`, {}) - console.log(response1); - expect(response1.data.createOpenTopLevel.id).toEqual("1") - expect(response1.data.createOpenTopLevel.owner).toEqual(USERNAME1) - expect(response1.data.createOpenTopLevel.name).toEqual("open") - - const response2 = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(response1); + expect(response1.data.createOpenTopLevel.id).toEqual('1'); + expect(response1.data.createOpenTopLevel.owner).toEqual(USERNAME1); + expect(response1.data.createOpenTopLevel.name).toEqual('open'); + + const response2 = await GRAPHQL_CLIENT_2.query( + `mutation { createConnectionProtected(input: { id: "1", owner: "${USERNAME2}", name: "closed", connectionProtectedTopLevelId: "1" }) { id owner name } - }`, {}) - console.log(response2); - expect(response2.data.createConnectionProtected.id).toEqual("1") - expect(response2.data.createConnectionProtected.owner).toEqual(USERNAME2) - expect(response2.data.createConnectionProtected.name).toEqual("closed") - - const response3 = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + console.log(response2); + expect(response2.data.createConnectionProtected.id).toEqual('1'); + expect(response2.data.createConnectionProtected.owner).toEqual(USERNAME2); + expect(response2.data.createConnectionProtected.name).toEqual('closed'); + + const response3 = await GRAPHQL_CLIENT_1.query( + `query { getOpenTopLevel(id: "1") { id protected { @@ -360,12 +395,15 @@ test('Test that @connection resolvers respect @model read operations.', async () } } } - }`, {}) - console.log(response3); - expect(response3.data.getOpenTopLevel.id).toEqual("1") - expect(response3.data.getOpenTopLevel.protected.items).toHaveLength(0) - - const response4 = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + console.log(response3); + expect(response3.data.getOpenTopLevel.id).toEqual('1'); + expect(response3.data.getOpenTopLevel.protected.items).toHaveLength(0); + + const response4 = await GRAPHQL_CLIENT_2.query( + `query { getOpenTopLevel(id: "1") { id protected { @@ -376,103 +414,126 @@ test('Test that @connection resolvers respect @model read operations.', async () } } } - }`, {}) - console.log(response4); - expect(response4.data.getOpenTopLevel.id).toEqual("1") - expect(response4.data.getOpenTopLevel.protected.items).toHaveLength(1) -}) + }`, + {} + ); + console.log(response4); + expect(response4.data.getOpenTopLevel.id).toEqual('1'); + expect(response4.data.getOpenTopLevel.protected.items).toHaveLength(1); +}); // Per field auth in mutations test('Test that owners cannot set the field of a FieldProtected object unless authorized.', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "2", owner: "${USERNAME1}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(JSON.stringify(response1)); - expect(response1.data.createFieldProtected.id).toEqual("2") - expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1) - expect(response1.data.createFieldProtected.ownerOnly).toEqual("owner-protected") - - const response2 = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(JSON.stringify(response1)); + expect(response1.data.createFieldProtected.id).toEqual('2'); + expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1); + expect(response1.data.createFieldProtected.ownerOnly).toEqual('owner-protected'); + + const response2 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "3", owner: "${USERNAME2}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(response2); - expect(response2.data.createFieldProtected).toBeNull() - expect(response2.errors).toHaveLength(1) - - // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will - // not trigger the @auth check - const response3 = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(response2); + expect(response2.data.createFieldProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); + + // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will + // not trigger the @auth check + const response3 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "4", owner: "${USERNAME2}" }) { id owner ownerOnly } - }`, {}) - console.log(response3); - expect(response3.data.createFieldProtected.id).toEqual("4") - expect(response3.data.createFieldProtected.owner).toEqual(USERNAME2) - // The length is one because the 'ownerOnly' field is protected on reads. - // Since the caller is not the owner this will throw after the mutation succeeds - // and return partial results. - expect(response3.errors).toHaveLength(1) -}) + }`, + {} + ); + console.log(response3); + expect(response3.data.createFieldProtected.id).toEqual('4'); + expect(response3.data.createFieldProtected.owner).toEqual(USERNAME2); + // The length is one because the 'ownerOnly' field is protected on reads. + // Since the caller is not the owner this will throw after the mutation succeeds + // and return partial results. + expect(response3.errors).toHaveLength(1); +}); test('Test that owners cannot update the field of a FieldProtected object unless authorized.', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { owner: "${USERNAME1}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(JSON.stringify(response1)); - expect(response1.data.createFieldProtected.id).not.toBeNull() - expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1) - expect(response1.data.createFieldProtected.ownerOnly).toEqual("owner-protected") - - const response2 = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(JSON.stringify(response1)); + expect(response1.data.createFieldProtected.id).not.toBeNull(); + expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1); + expect(response1.data.createFieldProtected.ownerOnly).toEqual('owner-protected'); + + const response2 = await GRAPHQL_CLIENT_2.query( + `mutation { updateFieldProtected(input: { id: "${response1.data.createFieldProtected.id}", ownerOnly: "owner2-protected" }) { id owner ownerOnly } - }`, {}) - console.log(response2); - expect(response2.data.updateFieldProtected).toBeNull() - expect(response2.errors).toHaveLength(1) - - // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will - // not trigger the @auth check - const response3 = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(response2); + expect(response2.data.updateFieldProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); + + // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will + // not trigger the @auth check + const response3 = await GRAPHQL_CLIENT_1.query( + `mutation { updateFieldProtected(input: { id: "${response1.data.createFieldProtected.id}", ownerOnly: "updated" }) { id owner ownerOnly } - }`, {}) - console.log(response3); - expect(response3.data.updateFieldProtected.id).toEqual(response1.data.createFieldProtected.id) - expect(response3.data.updateFieldProtected.owner).toEqual(USERNAME1) - expect(response3.data.updateFieldProtected.ownerOnly).toEqual("updated") - - // This request should succeed since we are not updating the protected field. - const response4 = await GRAPHQL_CLIENT_3.query(`mutation { + }`, + {} + ); + console.log(response3); + expect(response3.data.updateFieldProtected.id).toEqual(response1.data.createFieldProtected.id); + expect(response3.data.updateFieldProtected.owner).toEqual(USERNAME1); + expect(response3.data.updateFieldProtected.ownerOnly).toEqual('updated'); + + // This request should succeed since we are not updating the protected field. + const response4 = await GRAPHQL_CLIENT_3.query( + `mutation { updateFieldProtected(input: { id: "${response1.data.createFieldProtected.id}", owner: "${USERNAME3}" }) { id owner ownerOnly } - }`, {}) - console.log(response4); - expect(response4.data.updateFieldProtected.id).toEqual(response1.data.createFieldProtected.id) - expect(response4.data.updateFieldProtected.owner).toEqual(USERNAME3) - expect(response4.data.updateFieldProtected.ownerOnly).toEqual("updated") -}) + }`, + {} + ); + console.log(response4); + expect(response4.data.updateFieldProtected.id).toEqual(response1.data.createFieldProtected.id); + expect(response4.data.updateFieldProtected.owner).toEqual(USERNAME3); + expect(response4.data.updateFieldProtected.ownerOnly).toEqual('updated'); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts index 703b1f2758..4a72c334e4 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/CustomRoots.e2e.test.ts @@ -1,9 +1,14 @@ import { - ObjectTypeDefinitionNode, parse, FieldDefinitionNode, DocumentNode, - DefinitionNode, Kind, InputObjectTypeDefinitionNode -} from 'graphql' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' + ObjectTypeDefinitionNode, + parse, + FieldDefinitionNode, + DocumentNode, + DefinitionNode, + Kind, + InputObjectTypeDefinitionNode, +} from 'graphql'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; import fs = require('fs'); import path = require('path'); @@ -11,7 +16,7 @@ import path = require('path'); jest.setTimeout(2000000); test('Test custom root types with additional fields.', () => { - const validSchema = ` + const validSchema = ` type Query { additionalQueryField: String } @@ -25,27 +30,25 @@ test('Test custom root types with additional fields.', () => { id: ID! title: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query'); - expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']) - const mutationType = getObjectType(parsed, 'Mutation'); - expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']) - const subscriptionType = getObjectType(parsed, 'Subscription'); - expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost', 'additionalSubscriptionField']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query'); + expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']); + const mutationType = getObjectType(parsed, 'Mutation'); + expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost', 'additionalSubscriptionField']); }); test('Test custom root query with no mutations/subscriptions.', () => { - const validSchema = ` + const validSchema = ` # If I intentionally leave out mutation/subscription then no mutations/subscriptions # will be created even if @model is used. schema { @@ -58,27 +61,25 @@ test('Test custom root query with no mutations/subscriptions.', () => { id: ID! title: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query'); - expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']) - const mutationType = getObjectType(parsed, 'Mutation'); - expect(mutationType).toBeUndefined(); - const subscriptionType = getObjectType(parsed, 'Subscription'); - expect(subscriptionType).toBeUndefined(); + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query'); + expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeUndefined(); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).toBeUndefined(); }); test('Test custom root query & mutation with no subscriptions.', () => { - const validSchema = ` + const validSchema = ` # If I intentionally leave out mutation/subscription then no mutations/subscriptions # will be created even if @model is used. schema { @@ -95,27 +96,25 @@ test('Test custom root query & mutation with no subscriptions.', () => { id: ID! title: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query2'); - expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']) - const mutationType = getObjectType(parsed, 'Mutation2'); - expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']) - const subscriptionType = getObjectType(parsed, 'Subscription'); - expect(subscriptionType).toBeUndefined(); + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query2'); + expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']); + const mutationType = getObjectType(parsed, 'Mutation2'); + expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).toBeUndefined(); }); test('Test custom root query, mutation, and subscriptions.', () => { - const validSchema = ` + const validSchema = ` # If I intentionally leave out mutation/subscription then no mutations/subscriptions # will be created even if @model is used. schema { @@ -140,30 +139,28 @@ test('Test custom root query, mutation, and subscriptions.', () => { id: ID! title: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query2'); - expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField', 'authedField']) - const authedField = queryType.fields.find(f => f.name.value === 'authedField') - expect(authedField.directives.length).toEqual(1) - expect(authedField.directives[0].name.value).toEqual('aws_auth') - const mutationType = getObjectType(parsed, 'Mutation2'); - expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']) - const subscriptionType = getObjectType(parsed, 'Subscription2'); - expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost', 'onCreateOrUpdate']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query2'); + expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField', 'authedField']); + const authedField = queryType.fields.find(f => f.name.value === 'authedField'); + expect(authedField.directives.length).toEqual(1); + expect(authedField.directives[0].name.value).toEqual('aws_auth'); + const mutationType = getObjectType(parsed, 'Mutation2'); + expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']); + const subscriptionType = getObjectType(parsed, 'Subscription2'); + expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost', 'onCreateOrUpdate']); }); test('Test custom roots without any directives. This should still be valid.', () => { - const validSchema = ` + const validSchema = ` schema { query: Query2 mutation: Mutation2 @@ -182,69 +179,65 @@ test('Test custom roots without any directives. This should still be valid.', () id: ID! title: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - expect(out).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query2'); - expectFields(queryType, ['getPost']) - const mutationType = getObjectType(parsed, 'Mutation2'); - expectFields(mutationType, ['putPost']) - const subscriptionType = getObjectType(parsed, 'Subscription2'); - expectFields(subscriptionType, ['onPutPost']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query2'); + expectFields(queryType, ['getPost']); + const mutationType = getObjectType(parsed, 'Mutation2'); + expectFields(mutationType, ['putPost']); + const subscriptionType = getObjectType(parsed, 'Subscription2'); + expectFields(subscriptionType, ['onPutPost']); }); function expectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } function doNotExpectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - expect( - type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - ).toBeUndefined() - } + for (const fieldName of fields) { + expect(type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName)).toBeUndefined(); + } } function getObjectType(doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type - ) as ObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; } function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type - ) as InputObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type) as + | InputObjectTypeDefinitionNode + | undefined; } function verifyInputCount(doc: DocumentNode, type: string, count: number): boolean { - return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; + return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; } function cleanUpFiles(directory: string) { - var files = fs.readdirSync(directory) - for (const file of files) { - const dir = path.join(directory, file) - if (!fs.lstatSync(dir).isDirectory()) { - fs.unlinkSync(dir) - } else { - cleanUpFiles(dir) - } - } - fs.rmdirSync(directory) + var files = fs.readdirSync(directory); + for (const file of files) { + const dir = path.join(directory, file); + if (!fs.lstatSync(dir).isDirectory()) { + fs.unlinkSync(dir); + } else { + cleanUpFiles(dir); + } + } + fs.rmdirSync(directory); } function readFile(filePath: string) { - return fs.readFileSync(filePath, "utf8") -} \ No newline at end of file + return fs.readFileSync(filePath, 'utf8'); +} diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts index fdfaa73064..e27577729c 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/DynamoDBModelTransformer.e2e.test.ts @@ -1,48 +1,45 @@ -import { - ObjectTypeDefinitionNode, DirectiveNode, parse, FieldDefinitionNode, DocumentNode, DefinitionNode, - Kind -} from 'graphql' -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' +import { ObjectTypeDefinitionNode, DirectiveNode, parse, FieldDefinitionNode, DocumentNode, DefinitionNode, Kind } from 'graphql'; +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; import { deploy } from '../deployNestedStacks'; import { S3Client } from '../S3Client'; -import * as S3 from 'aws-sdk/clients/s3' +import * as S3 from 'aws-sdk/clients/s3'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; import * as fs from 'fs'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cf = new CloudFormationClient('us-west-2'); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `DynamoDBModelTransformerTest-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-model-transformer-test-bucket-${BUILD_TIMESTAMP}` +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `DynamoDBModelTransformerTest-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-model-transformer-test-bucket-${BUILD_TIMESTAMP}`; let GRAPHQL_CLIENT = undefined; -const TMP_ROOT = '/tmp/model_transform_tests/' +const TMP_ROOT = '/tmp/model_transform_tests/'; -const ROOT_KEY = 'deployments' +const ROOT_KEY = 'deployments'; let GRAPHQL_ENDPOINT = undefined; function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Post @model { id: ID! title: String! @@ -74,106 +71,121 @@ beforeAll(async () => { EMPIRE JEDI } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema); - // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); - try { - await awsS3Client.createBucket({ - Bucket: BUCKET_NAME, - }).promise() - } catch (e) { - console.error(`Failed to create S3 bucket: ${e}`) - } - try { - console.log('Creating Stack ' + STACK_NAME) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1', DynamoDBEnablePointInTimeRecovery: 'true' }, TMP_ROOT, BUCKET_NAME, ROOT_KEY, - BUILD_TIMESTAMP - ) - expect(finishedStack).toBeDefined() - console.log(JSON.stringify(finishedStack, null, 4)) - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - const apiKey = getApiKey(finishedStack.Outputs) - console.log(`API KEY: ${apiKey}`); - expect(apiKey).toBeTruthy() - expect(GRAPHQL_ENDPOINT).toBeTruthy() - GRAPHQL_CLIENT = new GraphQLClient(GRAPHQL_ENDPOINT, { 'x-api-key': apiKey }) - } catch (e) { - console.log(e) - expect(true).toEqual(false) - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); + try { + await awsS3Client + .createBucket({ + Bucket: BUCKET_NAME, + }) + .promise(); + } catch (e) { + console.error(`Failed to create S3 bucket: ${e}`); + } + try { + console.log('Creating Stack ' + STACK_NAME); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1', DynamoDBEnablePointInTimeRecovery: 'true' }, + TMP_ROOT, + BUCKET_NAME, + ROOT_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); + console.log(JSON.stringify(finishedStack, null, 4)); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + const apiKey = getApiKey(finishedStack.Outputs); + console.log(`API KEY: ${apiKey}`); + expect(apiKey).toBeTruthy(); + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + GRAPHQL_CLIENT = new GraphQLClient(GRAPHQL_ENDPOINT, { 'x-api-key': apiKey }); + } catch (e) { + console.log(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.log(e) - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.log(e); } -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); afterEach(async () => { try { // delete all the records console.log('deleting posts'); - const response = await GRAPHQL_CLIENT.query(` + const response = await GRAPHQL_CLIENT.query( + ` query { listPosts { items { id } } - }`, {}) + }`, + {} + ); const rows = response.data.listPosts.items || []; const deletePromises = []; rows.forEach(row => { - deletePromises.push(GRAPHQL_CLIENT.query(`mutation delete{ + deletePromises.push( + GRAPHQL_CLIENT.query(`mutation delete{ deletePost(input: {id: "${row.id}"}) { id } - }`)) - }) - await Promise.all(deletePromises) + }`) + ); + }); + await Promise.all(deletePromises); } catch (e) { console.log(e); } -}) - +}); /** * Test queries below */ test('Test createAuthor mutation', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation($input: CreateAuthorInput!) { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation($input: CreateAuthorInput!) { createAuthor(input: $input) { id name @@ -181,237 +193,288 @@ test('Test createAuthor mutation', async () => { isActive } } - }`, { - input: { - name: 'Jeff B', - entityMetadata: { - isActive: true - } - } - }) - expect(response.data.createAuthor.id).toBeDefined() - expect(response.data.createAuthor.name).toEqual('Jeff B') - expect(response.data.createAuthor.entityMetadata).toBeDefined() - expect(response.data.createAuthor.entityMetadata.isActive).toEqual(true) - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + { + input: { + name: 'Jeff B', + entityMetadata: { + isActive: true, + }, + }, + } + ); + expect(response.data.createAuthor.id).toBeDefined(); + expect(response.data.createAuthor.name).toEqual('Jeff B'); + expect(response.data.createAuthor.entityMetadata).toBeDefined(); + expect(response.data.createAuthor.entityMetadata.isActive).toEqual(true); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test createPost mutation', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title createdAt updatedAt } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test updatePost mutation', async () => { - try { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + try { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Update" }) { id title createdAt updatedAt } - }`, {}) - console.log(JSON.stringify(createResponse, null, 4)) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test Update') - const updateResponse = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + console.log(JSON.stringify(createResponse, null, 4)); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test Update'); + const updateResponse = await GRAPHQL_CLIENT.query( + `mutation { updatePost(input: { id: "${createResponse.data.createPost.id}", title: "Bye, World!" }) { id title } - }`, {}) - console.log(JSON.stringify(updateResponse, null, 4)) - expect(updateResponse.data.updatePost.title).toEqual('Bye, World!') - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + console.log(JSON.stringify(updateResponse, null, 4)); + expect(updateResponse.data.updatePost.title).toEqual('Bye, World!'); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test createPost and updatePost mutation with a client generated id.', async () => { - try { - const clientId = 'a-client-side-generated-id'; - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + try { + const clientId = 'a-client-side-generated-id'; + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { id: "${clientId}" title: "Test Update" }) { id title createdAt updatedAt } - }`, {}) - console.log(JSON.stringify(createResponse, null, 4)) - expect(createResponse.data.createPost.id).toEqual(clientId) - expect(createResponse.data.createPost.title).toEqual('Test Update') - const updateResponse = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + console.log(JSON.stringify(createResponse, null, 4)); + expect(createResponse.data.createPost.id).toEqual(clientId); + expect(createResponse.data.createPost.title).toEqual('Test Update'); + const updateResponse = await GRAPHQL_CLIENT.query( + `mutation { updatePost(input: { id: "${clientId}", title: "Bye, World!" }) { id title } - }`, {}) - console.log(JSON.stringify(updateResponse, null, 4)) - expect(updateResponse.data.updatePost.id).toEqual(clientId) - expect(updateResponse.data.updatePost.title).toEqual('Bye, World!') - const getResponse = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + console.log(JSON.stringify(updateResponse, null, 4)); + expect(updateResponse.data.updatePost.id).toEqual(clientId); + expect(updateResponse.data.updatePost.title).toEqual('Bye, World!'); + const getResponse = await GRAPHQL_CLIENT.query( + `query { getPost(id: "${clientId}") { id title } - }`, {}) - console.log(JSON.stringify(getResponse, null, 4)) - expect(getResponse.data.getPost.id).toEqual(clientId) - expect(getResponse.data.getPost.title).toEqual('Bye, World!') + }`, + {} + ); + console.log(JSON.stringify(getResponse, null, 4)); + expect(getResponse.data.getPost.id).toEqual(clientId); + expect(getResponse.data.getPost.title).toEqual('Bye, World!'); - const deleteResponse = await GRAPHQL_CLIENT.query(`mutation { + const deleteResponse = await GRAPHQL_CLIENT.query( + `mutation { deletePost(input: { id: "${clientId}" }) { id title } - }`, {}) - console.log(JSON.stringify(deleteResponse, null, 4)) - expect(deleteResponse.data.deletePost.id).toEqual(clientId) - expect(deleteResponse.data.deletePost.title).toEqual('Bye, World!') + }`, + {} + ); + console.log(JSON.stringify(deleteResponse, null, 4)); + expect(deleteResponse.data.deletePost.id).toEqual(clientId); + expect(deleteResponse.data.deletePost.title).toEqual('Bye, World!'); - const getResponse2 = await GRAPHQL_CLIENT.query(`query { + const getResponse2 = await GRAPHQL_CLIENT.query( + `query { getPost(id: "${clientId}") { id title } - }`, {}) - console.log(JSON.stringify(getResponse2, null, 4)) - expect(getResponse2.data.getPost).toBeNull() - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + console.log(JSON.stringify(getResponse2, null, 4)); + expect(getResponse2.data.getPost).toBeNull(); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test deletePost mutation', async () => { - try { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + try { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Delete" }) { id title createdAt updatedAt } - }`, {}) - console.log(JSON.stringify(createResponse, null, 4)) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test Delete') - const deleteResponse = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + console.log(JSON.stringify(createResponse, null, 4)); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test Delete'); + const deleteResponse = await GRAPHQL_CLIENT.query( + `mutation { deletePost(input: { id: "${createResponse.data.createPost.id}" }) { id title } - }`, {}) - console.log(JSON.stringify(deleteResponse, null, 4)) - expect(deleteResponse.data.deletePost.title).toEqual('Test Delete') - const getResponse = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + console.log(JSON.stringify(deleteResponse, null, 4)); + expect(deleteResponse.data.deletePost.title).toEqual('Test Delete'); + const getResponse = await GRAPHQL_CLIENT.query( + `query { getPost(id: "${createResponse.data.createPost.id}") { id title } - }`, {}) - console.log(JSON.stringify(getResponse, null, 4)) - expect(getResponse.data.getPost).toBeNull() - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + console.log(JSON.stringify(getResponse, null, 4)); + expect(getResponse.data.getPost).toBeNull(); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test getPost query', async () => { - try { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + try { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Get" }) { id title createdAt updatedAt } - }`, {}) - expect(createResponse.data.createPost.id).toBeTruthy() - expect(createResponse.data.createPost.title).toEqual('Test Get') - const getResponse = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createResponse.data.createPost.id).toBeTruthy(); + expect(createResponse.data.createPost.title).toEqual('Test Get'); + const getResponse = await GRAPHQL_CLIENT.query( + `query { getPost(id: "${createResponse.data.createPost.id}") { id title } - }`, {}) - expect(getResponse.data.getPost.title).toEqual('Test Get') - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(getResponse.data.getPost.title).toEqual('Test Get'); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test listPosts query', async () => { - try { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + try { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test List" }) { id title createdAt updatedAt } - }`, {}) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test List') - const listResponse = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test List'); + const listResponse = await GRAPHQL_CLIENT.query( + `query { listPosts { items { id title } } - }`, {}) - expect(listResponse.data.listPosts.items).toBeDefined() - const items = listResponse.data.listPosts.items - expect(items.length).toBeGreaterThan(0) - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(listResponse.data.listPosts.items).toBeDefined(); + const items = listResponse.data.listPosts.items; + expect(items.length).toBeGreaterThan(0); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test listPosts query with filter', async () => { - try { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + try { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test List with filter" }) { id title createdAt updatedAt } - }`, {}) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test List with filter') - const listWithFilterResponse = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test List with filter'); + const listWithFilterResponse = await GRAPHQL_CLIENT.query( + `query { listPosts(filter: { title: { contains: "List with filter" @@ -422,18 +485,20 @@ test('Test listPosts query with filter', async () => { title } } - }`, {}) - console.log(JSON.stringify(listWithFilterResponse, null, 4)) - expect(listWithFilterResponse.data.listPosts.items).toBeDefined() - const items = listWithFilterResponse.data.listPosts.items - expect(items.length).toEqual(1) - expect(items[0].title).toEqual('Test List with filter') - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + console.log(JSON.stringify(listWithFilterResponse, null, 4)); + expect(listWithFilterResponse.data.listPosts.items).toBeDefined(); + const items = listWithFilterResponse.data.listPosts.items; + expect(items.length).toEqual(1); + expect(items[0].title).toEqual('Test List with filter'); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test enum filters List', async () => { try { @@ -460,7 +525,7 @@ test('Test enum filters List', async () => { {} ); await GRAPHQL_CLIENT.query( - `mutation { + `mutation { createPost(input: { title: "Appears in Empire", appearsIn: [EMPIRE], episode: EMPIRE }) { id title @@ -468,11 +533,11 @@ test('Test enum filters List', async () => { updatedAt } }`, - {} - ); + {} + ); await GRAPHQL_CLIENT.query( - `mutation { + `mutation { createPost(input: { title: "Appears in Empire & JEDI", appearsIn: [EMPIRE, JEDI] }) { id title @@ -480,7 +545,7 @@ test('Test enum filters List', async () => { updatedAt } }`, - {} + {} ); // filter list of enums @@ -516,10 +581,9 @@ test('Test enum filters List', async () => { expect(appearsInWithFilterResponseNonJedi.data.listPosts.items).toBeDefined(); const appearsInNonJediItems = appearsInWithFilterResponseNonJedi.data.listPosts.items; expect(appearsInNonJediItems.length).toEqual(3); - appearsInNonJediItems.forEach((item) => { - expect(['Appears in Empire & JEDI', 'Appears in New Hope', 'Appears in Empire'].includes(item.title)) - .toBeTruthy(); - }) + appearsInNonJediItems.forEach(item => { + expect(['Appears in Empire & JEDI', 'Appears in New Hope', 'Appears in Empire'].includes(item.title)).toBeTruthy(); + }); const appearsInContainingJedi = await GRAPHQL_CLIENT.query( `query { @@ -536,10 +600,9 @@ test('Test enum filters List', async () => { expect(appearsInContainingJedi.data.listPosts.items).toBeDefined(); const appearsInWithJediItems = appearsInContainingJedi.data.listPosts.items; expect(appearsInWithJediItems.length).toEqual(2); - appearsInWithJediItems.forEach((item) => { - expect(['Appears in Empire & JEDI', 'Appears in Jedi'].includes(item.title)) - .toBeTruthy(); - }) + appearsInWithJediItems.forEach(item => { + expect(['Appears in Empire & JEDI', 'Appears in Jedi'].includes(item.title)).toBeTruthy(); + }); const appearsInNotContainingJedi = await GRAPHQL_CLIENT.query( `query { @@ -556,10 +619,9 @@ test('Test enum filters List', async () => { expect(appearsInNotContainingJedi.data.listPosts.items).toBeDefined(); const appearsInWithNonJediItems = appearsInNotContainingJedi.data.listPosts.items; expect(appearsInWithNonJediItems.length).toEqual(2); - appearsInWithNonJediItems.forEach((item) => { - expect(['Appears in New Hope', 'Appears in Empire'].includes(item.title)) - .toBeTruthy(); - }) + appearsInWithNonJediItems.forEach(item => { + expect(['Appears in New Hope', 'Appears in Empire'].includes(item.title)).toBeTruthy(); + }); // enum filter const jediEpisode = await GRAPHQL_CLIENT.query( @@ -577,7 +639,7 @@ test('Test enum filters List', async () => { expect(jediEpisode.data.listPosts.items).toBeDefined(); const jediEpisodeItems = jediEpisode.data.listPosts.items; expect(jediEpisodeItems.length).toEqual(1); - expect(jediEpisodeItems[0].title).toEqual('Appears in Jedi') + expect(jediEpisodeItems[0].title).toEqual('Appears in Jedi'); const nonJediEpisode = await GRAPHQL_CLIENT.query( `query { @@ -594,10 +656,9 @@ test('Test enum filters List', async () => { expect(nonJediEpisode.data.listPosts.items).toBeDefined(); const nonJediEpisodeItems = nonJediEpisode.data.listPosts.items; expect(nonJediEpisodeItems.length).toEqual(3); - nonJediEpisodeItems.forEach((item) => { - expect(['Appears in New Hope', 'Appears in Empire', 'Appears in Empire & JEDI'].includes(item.title)) - .toBeTruthy(); - }) + nonJediEpisodeItems.forEach(item => { + expect(['Appears in New Hope', 'Appears in Empire', 'Appears in Empire & JEDI'].includes(item.title)).toBeTruthy(); + }); } catch (e) { console.log(e); // fail @@ -606,8 +667,9 @@ test('Test enum filters List', async () => { }); test('Test createPost mutation with non-model types', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation CreatePost($input: CreatePostInput!) { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title @@ -625,51 +687,57 @@ test('Test createPost mutation with non-model types', async () => { } appearsIn } - }`, { - input: { - title: 'Check that metadata exists', - metadata: { - tags: { - published: true, - metadata: { - tags: { - published: false - } - } - } - }, - appearsIn: ['NEWHOPE'] - } - }) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Check that metadata exists') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.metadata).toBeDefined() - expect(response.data.createPost.metadata.tags.published).toEqual(true) - expect(response.data.createPost.metadata.tags.metadata.tags.published).toEqual(false) - expect(response.data.createPost.appearsIn).toEqual(['NEWHOPE']) - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + { + input: { + title: 'Check that metadata exists', + metadata: { + tags: { + published: true, + metadata: { + tags: { + published: false, + }, + }, + }, + }, + appearsIn: ['NEWHOPE'], + }, + } + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Check that metadata exists'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.metadata).toBeDefined(); + expect(response.data.createPost.metadata.tags.published).toEqual(true); + expect(response.data.createPost.metadata.tags.metadata.tags.published).toEqual(false); + expect(response.data.createPost.appearsIn).toEqual(['NEWHOPE']); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test updatePost mutation with non-model types', async () => { - try { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + try { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Update" }) { id title createdAt updatedAt } - }`, {}) - console.log(JSON.stringify(createResponse, null, 4)) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test Update') - const updateResponse = await GRAPHQL_CLIENT.query(`mutation UpdatePost($input: UpdatePostInput!) { + }`, + {} + ); + console.log(JSON.stringify(createResponse, null, 4)); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test Update'); + const updateResponse = await GRAPHQL_CLIENT.query( + `mutation UpdatePost($input: UpdatePostInput!) { updatePost(input: $input) { id title @@ -687,32 +755,34 @@ test('Test updatePost mutation with non-model types', async () => { } appearsIn } - }`, { - input: { - id: createResponse.data.createPost.id, - title: 'Add some metadata', - metadata: { - tags: { - published: true, - metadata: { - tags: { - published: false - } - } - } - }, - appearsIn: ['NEWHOPE', 'EMPIRE'] - } - }) - console.log(JSON.stringify(updateResponse, null, 4)) - expect(updateResponse.data.updatePost.title).toEqual('Add some metadata') - expect(updateResponse.data.updatePost.metadata).toBeDefined() - expect(updateResponse.data.updatePost.metadata.tags.published).toEqual(true) - expect(updateResponse.data.updatePost.metadata.tags.metadata.tags.published).toEqual(false) - expect(updateResponse.data.updatePost.appearsIn).toEqual(['NEWHOPE', 'EMPIRE']) - } catch (e) { - console.log(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + { + input: { + id: createResponse.data.createPost.id, + title: 'Add some metadata', + metadata: { + tags: { + published: true, + metadata: { + tags: { + published: false, + }, + }, + }, + }, + appearsIn: ['NEWHOPE', 'EMPIRE'], + }, + } + ); + console.log(JSON.stringify(updateResponse, null, 4)); + expect(updateResponse.data.updatePost.title).toEqual('Add some metadata'); + expect(updateResponse.data.updatePost.metadata).toBeDefined(); + expect(updateResponse.data.updatePost.metadata.tags.published).toEqual(true); + expect(updateResponse.data.updatePost.metadata.tags.metadata.tags.published).toEqual(false); + expect(updateResponse.data.updatePost.appearsIn).toEqual(['NEWHOPE', 'EMPIRE']); + } catch (e) { + console.log(e); + // fail + expect(e).toBeUndefined(); + } +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts index 1b35655185..d34793a720 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/FunctionTransformerTests.e2e.test.ts @@ -1,34 +1,34 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import ModelTransformer from 'graphql-dynamodb-transformer' -import FunctionTransformer from 'graphql-function-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import ModelTransformer from 'graphql-dynamodb-transformer'; +import FunctionTransformer from 'graphql-function-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; -import { deploy } from '../deployNestedStacks' +import { deploy } from '../deployNestedStacks'; import { S3Client } from '../S3Client'; -import * as S3 from 'aws-sdk/clients/s3' +import * as S3 from 'aws-sdk/clients/s3'; import { LambdaHelper } from '../LambdaHelper'; import { IAMHelper } from '../IAMHelper'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cf = new CloudFormationClient('us-west-2'); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `FunctionTransformerTests-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-function-transformer-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/function_transformer_tests/' -const S3_ROOT_DIR_KEY = 'deployments' -const ECHO_FUNCTION_NAME = `e2e-tests-echo-dev-${BUILD_TIMESTAMP}` -const HELLO_FUNCTION_NAME = `e2e-tests-hello-${BUILD_TIMESTAMP}` -const LAMBDA_EXECUTION_ROLE_NAME = `amplify_e2e_tests_lambda_basic_${BUILD_TIMESTAMP}` -const LAMBDA_EXECUTION_POLICY_NAME = `amplify_e2e_tests_lambda_basic_access_${BUILD_TIMESTAMP}` +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `FunctionTransformerTests-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-function-transformer-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/function_transformer_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; +const ECHO_FUNCTION_NAME = `e2e-tests-echo-dev-${BUILD_TIMESTAMP}`; +const HELLO_FUNCTION_NAME = `e2e-tests-hello-${BUILD_TIMESTAMP}`; +const LAMBDA_EXECUTION_ROLE_NAME = `amplify_e2e_tests_lambda_basic_${BUILD_TIMESTAMP}`; +const LAMBDA_EXECUTION_POLICY_NAME = `amplify_e2e_tests_lambda_basic_access_${BUILD_TIMESTAMP}`; let LAMBDA_EXECUTION_POLICY_ARN = ''; let GRAPHQL_CLIENT = undefined; @@ -37,14 +37,14 @@ const LAMBDA_HELPER = new LambdaHelper(); const IAM_HELPER = new IAMHelper(); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Query { echo(msg: String!): Context @function(name: "e2e-tests-echo-dev-${BUILD_TIMESTAMP}") echoEnv(msg: String!): Context @function(name: "e2e-tests-echo-\${env}-${BUILD_TIMESTAMP}") @@ -64,94 +64,119 @@ beforeAll(async () => { type Arguments { msg: String! } - ` - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { console.warn(`Could not create bucket: ${e}`) } - try { - const role = await IAM_HELPER.createLambdaExecutionRole(LAMBDA_EXECUTION_ROLE_NAME); - await wait(5000); - const policy = await IAM_HELPER.createLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_NAME); - await wait(5000); - LAMBDA_EXECUTION_POLICY_ARN = policy.Policy.Arn; - await IAM_HELPER.attachLambdaExecutionPolicy(policy.Policy.Arn, role.Role.RoleName) - await wait(10000); - await LAMBDA_HELPER.createFunction(ECHO_FUNCTION_NAME, role.Role.Arn, 'echoFunction'); - await LAMBDA_HELPER.createFunction(HELLO_FUNCTION_NAME, role.Role.Arn, 'hello'); - } catch (e) { console.warn(`Could not setup function: ${e}`) } - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new FunctionTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema); - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1', env: 'dev' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - // Arbitrary wait to make sure everything is ready. - await cf.wait(5, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - console.log(finishedStack) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) + `; + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.warn(`Could not create bucket: ${e}`); + } + try { + const role = await IAM_HELPER.createLambdaExecutionRole(LAMBDA_EXECUTION_ROLE_NAME); + await wait(5000); + const policy = await IAM_HELPER.createLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_NAME); + await wait(5000); + LAMBDA_EXECUTION_POLICY_ARN = policy.Policy.Arn; + await IAM_HELPER.attachLambdaExecutionPolicy(policy.Policy.Arn, role.Role.RoleName); + await wait(10000); + await LAMBDA_HELPER.createFunction(ECHO_FUNCTION_NAME, role.Role.Arn, 'echoFunction'); + await LAMBDA_HELPER.createFunction(HELLO_FUNCTION_NAME, role.Role.Arn, 'hello'); + } catch (e) { + console.warn(`Could not setup function: ${e}`); + } + const transformer = new GraphQLTransform({ + transformers: [ + new ModelTransformer(), + new FunctionTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1', env: 'dev' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + // Arbitrary wait to make sure everything is ready. + await cf.wait(5, () => Promise.resolve()); + console.log('Successfully created stack ' + STACK_NAME); + console.log(finishedStack); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { console.warn(`Error during bucket cleanup: ${e}`)} - try { - await LAMBDA_HELPER.deleteFunction(ECHO_FUNCTION_NAME); - } catch (e) { console.warn(`Error during function cleanup: ${e}`)} - try { - await LAMBDA_HELPER.deleteFunction(HELLO_FUNCTION_NAME); - } catch (e) { console.warn(`Error during function cleanup: ${e}`)} - try { - await IAM_HELPER.detachLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_ARN, LAMBDA_EXECUTION_ROLE_NAME) - } catch (e) { console.warn(`Error during policy dissociation: ${e}`)} - try { - await IAM_HELPER.deleteRole(LAMBDA_EXECUTION_ROLE_NAME); - } catch (e) { console.warn(`Error during role cleanup: ${e}`)} - try { - await IAM_HELPER.deletePolicy(LAMBDA_EXECUTION_POLICY_ARN); - } catch (e) { console.warn(`Error during policy cleanup: ${e}`)} -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.warn(`Error during bucket cleanup: ${e}`); + } + try { + await LAMBDA_HELPER.deleteFunction(ECHO_FUNCTION_NAME); + } catch (e) { + console.warn(`Error during function cleanup: ${e}`); + } + try { + await LAMBDA_HELPER.deleteFunction(HELLO_FUNCTION_NAME); + } catch (e) { + console.warn(`Error during function cleanup: ${e}`); + } + try { + await IAM_HELPER.detachLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_ARN, LAMBDA_EXECUTION_ROLE_NAME); + } catch (e) { + console.warn(`Error during policy dissociation: ${e}`); + } + try { + await IAM_HELPER.deleteRole(LAMBDA_EXECUTION_ROLE_NAME); + } catch (e) { + console.warn(`Error during role cleanup: ${e}`); + } + try { + await IAM_HELPER.deletePolicy(LAMBDA_EXECUTION_POLICY_ARN); + } catch (e) { + console.warn(`Error during policy cleanup: ${e}`); + } +}); /** * Test queries below */ test('Test simple echo function', async () => { - const response = await GRAPHQL_CLIENT.query(`query { + const response = await GRAPHQL_CLIENT.query( + `query { echo(msg: "Hello") { arguments { msg @@ -159,15 +184,18 @@ test('Test simple echo function', async () => { typeName fieldName } - }`, {}) - console.log(JSON.stringify(response, null, 4)); - expect(response.data.echo.arguments.msg).toEqual("Hello") - expect(response.data.echo.typeName).toEqual("Query") - expect(response.data.echo.fieldName).toEqual("echo") -}) + }`, + {} + ); + console.log(JSON.stringify(response, null, 4)); + expect(response.data.echo.arguments.msg).toEqual('Hello'); + expect(response.data.echo.typeName).toEqual('Query'); + expect(response.data.echo.fieldName).toEqual('echo'); +}); test('Test simple echoEnv function', async () => { - const response = await GRAPHQL_CLIENT.query(`query { + const response = await GRAPHQL_CLIENT.query( + `query { echoEnv(msg: "Hello") { arguments { msg @@ -175,15 +203,18 @@ test('Test simple echoEnv function', async () => { typeName fieldName } - }`, {}) - console.log(JSON.stringify(response, null, 4)); - expect(response.data.echoEnv.arguments.msg).toEqual("Hello") - expect(response.data.echoEnv.typeName).toEqual("Query") - expect(response.data.echoEnv.fieldName).toEqual("echoEnv") -}) + }`, + {} + ); + console.log(JSON.stringify(response, null, 4)); + expect(response.data.echoEnv.arguments.msg).toEqual('Hello'); + expect(response.data.echoEnv.typeName).toEqual('Query'); + expect(response.data.echoEnv.fieldName).toEqual('echoEnv'); +}); test('Test simple duplicate function', async () => { - const response = await GRAPHQL_CLIENT.query(`query { + const response = await GRAPHQL_CLIENT.query( + `query { duplicate(msg: "Hello") { arguments { msg @@ -191,23 +222,29 @@ test('Test simple duplicate function', async () => { typeName fieldName } - }`, {}) - console.log(JSON.stringify(response, null, 4)); - expect(response.data.duplicate.arguments.msg).toEqual("Hello") - expect(response.data.duplicate.typeName).toEqual("Query") - expect(response.data.duplicate.fieldName).toEqual("duplicate") -}) + }`, + {} + ); + console.log(JSON.stringify(response, null, 4)); + expect(response.data.duplicate.arguments.msg).toEqual('Hello'); + expect(response.data.duplicate.typeName).toEqual('Query'); + expect(response.data.duplicate.fieldName).toEqual('duplicate'); +}); test('Test pipeline of @function(s)', async () => { - const response = await GRAPHQL_CLIENT.query(`query { + const response = await GRAPHQL_CLIENT.query( + `query { pipeline(msg: "IGNORED") - }`, {}) - console.log(JSON.stringify(response, null, 4)); - expect(response.data.pipeline).toEqual("Hello, world!") -}) + }`, + {} + ); + console.log(JSON.stringify(response, null, 4)); + expect(response.data.pipeline).toEqual('Hello, world!'); +}); test('Test pipelineReverse of @function(s)', async () => { - const response = await GRAPHQL_CLIENT.query(`query { + const response = await GRAPHQL_CLIENT.query( + `query { pipelineReverse(msg: "Hello") { arguments { msg @@ -215,15 +252,17 @@ test('Test pipelineReverse of @function(s)', async () => { typeName fieldName } - }`, {}) - console.log(JSON.stringify(response, null, 4)); - expect(response.data.pipelineReverse.arguments.msg).toEqual("Hello") - expect(response.data.pipelineReverse.typeName).toEqual("Query") - expect(response.data.pipelineReverse.fieldName).toEqual("pipelineReverse") -}) + }`, + {} + ); + console.log(JSON.stringify(response, null, 4)); + expect(response.data.pipelineReverse.arguments.msg).toEqual('Hello'); + expect(response.data.pipelineReverse.typeName).toEqual('Query'); + expect(response.data.pipelineReverse.fieldName).toEqual('pipelineReverse'); +}); function wait(ms: number) { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), ms) - }) + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), ms); + }); } diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts index 4ca8bb6b0c..194a57390f 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/HttpTransformer.e2e.test.ts @@ -1,41 +1,41 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import HttpTransformer from '../../../graphql-http-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import HttpTransformer from '../../../graphql-http-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; -import { deploy } from '../deployNestedStacks' +import { deploy } from '../deployNestedStacks'; import { S3Client } from '../S3Client'; -import * as S3 from 'aws-sdk/clients/s3' -import * as fs from 'fs' +import * as S3 from 'aws-sdk/clients/s3'; +import * as fs from 'fs'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cf = new CloudFormationClient('us-west-2'); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `HttpTransformerTest-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-http-transformer-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/http_transformer_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `HttpTransformerTest-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-http-transformer-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/http_transformer_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; let GRAPHQL_CLIENT = undefined; function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Comment @model { id: ID! title: String @@ -79,79 +79,88 @@ beforeAll(async () => { email: String body: String } - ` - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { - console.error(`Failed to create bucket: ${e}`) - } - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}), - new HttpTransformer() - ] - }) - const out = transformer.transform(validSchema); - // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); - try { - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - // Arbitrary wait to make sure everything is ready. - await cf.wait(5, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - console.log(finishedStack) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + `; + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.error(`Failed to create bucket: ${e}`); + } + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + new HttpTransformer(), + ], + }); + const out = transformer.transform(validSchema); + // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); + try { + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + // Arbitrary wait to make sure everything is ready. + await cf.wait(5, () => Promise.resolve()); + console.log('Successfully created stack ' + STACK_NAME); + console.log(finishedStack); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Test queries below */ test('Test HTTP GET request', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -161,22 +170,25 @@ test('Test HTTP GET request', async () => { body } } - }`, {}) - const post1Title = 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit' - expect(response.data.createComment.id).toBeDefined() - expect(response.data.createComment.title).toEqual('Hello, World!') - expect(response.data.createComment.simpleGet).toBeDefined() - expect(response.data.createComment.simpleGet.title).toEqual(post1Title) - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + const post1Title = 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit'; + expect(response.data.createComment.id).toBeDefined(); + expect(response.data.createComment.title).toEqual('Hello, World!'); + expect(response.data.createComment.simpleGet).toBeDefined(); + expect(response.data.createComment.simpleGet.title).toEqual(post1Title); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test HTTP GET request 2', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -186,22 +198,25 @@ test('Test HTTP GET request 2', async () => { body } } - }`, {}) - const post2Title = 'qui est esse' - expect(response.data.createComment.id).toBeDefined() - expect(response.data.createComment.title).toEqual('Hello, World!') - expect(response.data.createComment.simpleGet2).toBeDefined() - expect(response.data.createComment.simpleGet2.title).toEqual(post2Title) - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + const post2Title = 'qui est esse'; + expect(response.data.createComment.id).toBeDefined(); + expect(response.data.createComment.title).toEqual('Hello, World!'); + expect(response.data.createComment.simpleGet2).toBeDefined(); + expect(response.data.createComment.simpleGet2.title).toEqual(post2Title); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test HTTP POST request', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -218,22 +233,25 @@ test('Test HTTP POST request', async () => { userId } } - }`, {}) - expect(response.data.createComment.id).toBeDefined() - expect(response.data.createComment.title).toEqual('Hello, World!') - expect(response.data.createComment.complexPost).toBeDefined() - expect(response.data.createComment.complexPost.title).toEqual('foo') - expect(response.data.createComment.complexPost.userId).toEqual(2) - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data.createComment.id).toBeDefined(); + expect(response.data.createComment.title).toEqual('Hello, World!'); + expect(response.data.createComment.complexPost).toBeDefined(); + expect(response.data.createComment.complexPost.title).toEqual('foo'); + expect(response.data.createComment.complexPost.userId).toEqual(2); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test HTTP PUT request', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -253,41 +271,47 @@ test('Test HTTP PUT request', async () => { userId } } - }`, {}) - expect(response.data.createComment.id).toBeDefined() - expect(response.data.createComment.title).toEqual('Hello, World!') - expect(response.data.createComment.complexPut).toBeDefined() - expect(response.data.createComment.complexPut.title).toEqual('foo') - expect(response.data.createComment.complexPut.userId).toEqual(2) - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data.createComment.id).toBeDefined(); + expect(response.data.createComment.title).toEqual('Hello, World!'); + expect(response.data.createComment.complexPut).toBeDefined(); + expect(response.data.createComment.complexPut.title).toEqual('foo'); + expect(response.data.createComment.complexPut.userId).toEqual(2); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test HTTP DELETE request', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title deleter } - }`, {}) - expect(response.data.createComment.id).toBeDefined() - expect(response.data.createComment.title).toEqual('Hello, World!') - expect(response.data.createComment.deleter).toBeDefined() - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data.createComment.id).toBeDefined(); + expect(response.data.createComment.title).toEqual('Hello, World!'); + expect(response.data.createComment.deleter).toBeDefined(); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test GET with URL param and query values', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -305,21 +329,24 @@ test('Test GET with URL param and query values', async () => { body } } - }`, {}) - expect(response.data.createComment.id).toBeDefined() - expect(response.data.createComment.title).toEqual('Hello, World!') - expect(response.data.createComment.complexGet).toBeDefined() - expect(response.data.createComment.complexGet.length).toEqual(7) - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data.createComment.id).toBeDefined(); + expect(response.data.createComment.title).toEqual('Hello, World!'); + expect(response.data.createComment.complexGet).toBeDefined(); + expect(response.data.createComment.complexGet.length).toEqual(7); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test GET with multiple URL params and query values', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -338,21 +365,24 @@ test('Test GET with multiple URL params and query values', async () => { email } } - }`, {}) - expect(response.data.createComment.id).toBeDefined() - expect(response.data.createComment.title).toEqual('Hello, World!') - expect(response.data.createComment.complexGet2).toBeDefined() - expect(response.data.createComment.complexGet2[0].email).toEqual('Jayne_Kuhic@sydney.com') - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data.createComment.id).toBeDefined(); + expect(response.data.createComment.title).toEqual('Hello, World!'); + expect(response.data.createComment.complexGet2).toBeDefined(); + expect(response.data.createComment.complexGet2[0].email).toEqual('Jayne_Kuhic@sydney.com'); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test that GET errors when missing a required Query input object', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -366,18 +396,21 @@ test('Test that GET errors when missing a required Query input object', async () body } } - }`, {}) - expect(response.data).toBeNull() - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data).toBeNull(); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); test('Test that POST errors when missing a non-null arg in query/body', async () => { - try { - const response = await GRAPHQL_CLIENT.query(`mutation { + try { + const response = await GRAPHQL_CLIENT.query( + `mutation { createComment(input: { title: "Hello, World!" }) { id title @@ -392,11 +425,13 @@ test('Test that POST errors when missing a non-null arg in query/body', async () body } } - }`, {}) - expect(response.data.createComment.complexPost).toBeNull() - } catch (e) { - console.error(e) - // fail - expect(e).toBeUndefined() - } -}) + }`, + {} + ); + expect(response.data.createComment.complexPost).toBeNull(); + } catch (e) { + console.error(e); + // fail + expect(e).toBeUndefined(); + } +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts index 67c6bde0c5..80afcd14a7 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformer.e2e.test.ts @@ -1,40 +1,40 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import ModelTransformer from 'graphql-dynamodb-transformer' -import KeyTransformer from 'graphql-key-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import ModelTransformer from 'graphql-dynamodb-transformer'; +import KeyTransformer from 'graphql-key-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; -import { deploy } from '../deployNestedStacks' +import { deploy } from '../deployNestedStacks'; import { S3Client } from '../S3Client'; -import * as S3 from 'aws-sdk/clients/s3' +import * as S3 from 'aws-sdk/clients/s3'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cf = new CloudFormationClient('us-west-2'); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `KeyTransformerTests-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-key-transformer-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/key_transformer_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `KeyTransformerTests-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-key-transformer-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/key_transformer_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; let GRAPHQL_CLIENT = undefined; function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Order @model @key(fields: ["customerEmail", "createdAt"]) { customerEmail: String! createdAt: String! @@ -67,394 +67,440 @@ beforeAll(async () => { status: Status name: String } - ` - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { console.warn(`Could not create bucket: ${e}`) } - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema); - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1', env: 'dev' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - // Arbitrary wait to make sure everything is ready. - await cf.wait(5, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - console.log(finishedStack) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) + `; + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.warn(`Could not create bucket: ${e}`); + } + const transformer = new GraphQLTransform({ + transformers: [ + new ModelTransformer(), + new KeyTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1', env: 'dev' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + // Arbitrary wait to make sure everything is ready. + await cf.wait(5, () => Promise.resolve()); + console.log('Successfully created stack ' + STACK_NAME); + console.log(finishedStack); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - // await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + // await cf.waitForStack(STACK_NAME) + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { console.warn(`Error during bucket cleanup: ${e}`)} -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.warn(`Error during bucket cleanup: ${e}`); + } +}); /** * Test queries below */ test('Test getX with a two part primary key.', async () => { - const order1 = await createOrder('test@gmail.com', '1'); - const getOrder1 = await getOrder('test@gmail.com', order1.data.createOrder.createdAt) - expect(getOrder1.data.getOrder.orderId).toEqual('1'); -}) + const order1 = await createOrder('test@gmail.com', '1'); + const getOrder1 = await getOrder('test@gmail.com', order1.data.createOrder.createdAt); + expect(getOrder1.data.getOrder.orderId).toEqual('1'); +}); test('Test updateX with a two part primary key.', async () => { - const order2 = await createOrder('test3@gmail.com', '2'); - let getOrder2 = await getOrder('test3@gmail.com', order2.data.createOrder.createdAt) - expect(getOrder2.data.getOrder.orderId).toEqual('2'); - const updateOrder2 = await updateOrder('test3@gmail.com', order2.data.createOrder.createdAt, '3') - expect(updateOrder2.data.updateOrder.orderId).toEqual('3'); - getOrder2 = await getOrder('test3@gmail.com', order2.data.createOrder.createdAt) - expect(getOrder2.data.getOrder.orderId).toEqual('3'); -}) + const order2 = await createOrder('test3@gmail.com', '2'); + let getOrder2 = await getOrder('test3@gmail.com', order2.data.createOrder.createdAt); + expect(getOrder2.data.getOrder.orderId).toEqual('2'); + const updateOrder2 = await updateOrder('test3@gmail.com', order2.data.createOrder.createdAt, '3'); + expect(updateOrder2.data.updateOrder.orderId).toEqual('3'); + getOrder2 = await getOrder('test3@gmail.com', order2.data.createOrder.createdAt); + expect(getOrder2.data.getOrder.orderId).toEqual('3'); +}); test('Test deleteX with a two part primary key.', async () => { - const order2 = await createOrder('test2@gmail.com', '2'); - let getOrder2 = await getOrder('test2@gmail.com', order2.data.createOrder.createdAt) - expect(getOrder2.data.getOrder.orderId).toEqual('2'); - const delOrder2 = await deleteOrder('test2@gmail.com', order2.data.createOrder.createdAt) - expect(delOrder2.data.deleteOrder.orderId).toEqual('2'); - getOrder2 = await getOrder('test2@gmail.com', order2.data.createOrder.createdAt) - expect(getOrder2.data.getOrder).toBeNull(); -}) + const order2 = await createOrder('test2@gmail.com', '2'); + let getOrder2 = await getOrder('test2@gmail.com', order2.data.createOrder.createdAt); + expect(getOrder2.data.getOrder.orderId).toEqual('2'); + const delOrder2 = await deleteOrder('test2@gmail.com', order2.data.createOrder.createdAt); + expect(delOrder2.data.deleteOrder.orderId).toEqual('2'); + getOrder2 = await getOrder('test2@gmail.com', order2.data.createOrder.createdAt); + expect(getOrder2.data.getOrder).toBeNull(); +}); test('Test getX with a three part primary key', async () => { - const item1 = await createItem('1', 'PENDING', 'item1'); - const getItem1 = await getItem('1', 'PENDING', item1.data.createItem.createdAt); - expect(getItem1.data.getItem.orderId).toEqual('1'); - expect(getItem1.data.getItem.status).toEqual('PENDING'); -}) + const item1 = await createItem('1', 'PENDING', 'item1'); + const getItem1 = await getItem('1', 'PENDING', item1.data.createItem.createdAt); + expect(getItem1.data.getItem.orderId).toEqual('1'); + expect(getItem1.data.getItem.status).toEqual('PENDING'); +}); test('Test updateX with a three part primary key.', async () => { - const item2 = await createItem('2', 'PENDING', 'item2'); - let getItem2 = await getItem('2', 'PENDING', item2.data.createItem.createdAt) - expect(getItem2.data.getItem.orderId).toEqual('2'); - const updateItem2 = await updateItem('2', 'PENDING', item2.data.createItem.createdAt, 'item2.1') - expect(updateItem2.data.updateItem.name).toEqual('item2.1'); - getItem2 = await getItem('2', 'PENDING', item2.data.createItem.createdAt) - expect(getItem2.data.getItem.name).toEqual('item2.1'); -}) + const item2 = await createItem('2', 'PENDING', 'item2'); + let getItem2 = await getItem('2', 'PENDING', item2.data.createItem.createdAt); + expect(getItem2.data.getItem.orderId).toEqual('2'); + const updateItem2 = await updateItem('2', 'PENDING', item2.data.createItem.createdAt, 'item2.1'); + expect(updateItem2.data.updateItem.name).toEqual('item2.1'); + getItem2 = await getItem('2', 'PENDING', item2.data.createItem.createdAt); + expect(getItem2.data.getItem.name).toEqual('item2.1'); +}); test('Test deleteX with a three part primary key.', async () => { - const item3 = await createItem('3', 'IN_TRANSIT', 'item3'); - let getItem3 = await getItem('3', 'IN_TRANSIT', item3.data.createItem.createdAt) - expect(getItem3.data.getItem.name).toEqual('item3'); - const delItem3 = await deleteItem('3', 'IN_TRANSIT', item3.data.createItem.createdAt) - expect(delItem3.data.deleteItem.name).toEqual('item3'); - getItem3 = await getItem('3', 'IN_TRANSIT', item3.data.createItem.createdAt); - expect(getItem3.data.getItem).toBeNull(); -}) + const item3 = await createItem('3', 'IN_TRANSIT', 'item3'); + let getItem3 = await getItem('3', 'IN_TRANSIT', item3.data.createItem.createdAt); + expect(getItem3.data.getItem.name).toEqual('item3'); + const delItem3 = await deleteItem('3', 'IN_TRANSIT', item3.data.createItem.createdAt); + expect(delItem3.data.deleteItem.name).toEqual('item3'); + getItem3 = await getItem('3', 'IN_TRANSIT', item3.data.createItem.createdAt); + expect(getItem3.data.getItem).toBeNull(); +}); test('Test listX with three part primary key.', async () => { - const hashKey = 'TEST_LIST_ID'; - await createItem(hashKey, 'IN_TRANSIT', 'list1', '2018-01-01T00:01:01.000Z'); - await createItem(hashKey, 'PENDING', 'list2', '2018-06-01T00:01:01.000Z'); - await createItem(hashKey, 'PENDING', 'item3', '2018-09-01T00:01:01.000Z'); - let items = await listItem(undefined); - expect(items.data.listItems.items.length).toBeGreaterThan(0); - items = await listItem(hashKey); - expect(items.data.listItems.items).toHaveLength(3) - items = await listItem(hashKey, { beginsWith: { status: 'PENDING' } }); - expect(items.data.listItems.items).toHaveLength(2) - items = await listItem(hashKey, { beginsWith: { status: 'IN_TRANSIT' } }); - expect(items.data.listItems.items).toHaveLength(1) - items = await listItem(hashKey, { beginsWith: { status: 'PENDING', createdAt: '2018-09' } }); - expect(items.data.listItems.items).toHaveLength(1) - items = await listItem(hashKey, { eq: { status: 'PENDING', createdAt: '2018-09-01T00:01:01.000Z' } }); - expect(items.data.listItems.items).toHaveLength(1) - items = await listItem(hashKey, { between: [{ status: 'PENDING', createdAt: '2018-08-01' }, { status: 'PENDING', createdAt: '2018-10-01' }] }); - expect(items.data.listItems.items).toHaveLength(1) - items = await listItem(hashKey, { gt: { status: 'PENDING', createdAt: '2018-08-1'}}); - expect(items.data.listItems.items).toHaveLength(1) - items = await listItem(hashKey, { ge: { status: 'PENDING', createdAt: '2018-09-01T00:01:01.000Z'}}); - expect(items.data.listItems.items).toHaveLength(1) - items = await listItem(hashKey, { lt: { status: 'IN_TRANSIT', createdAt: '2018-01-02'}}); - expect(items.data.listItems.items).toHaveLength(1) - items = await listItem(hashKey, { le: { status: 'IN_TRANSIT', createdAt: '2018-01-01T00:01:01.000Z'}}); - expect(items.data.listItems.items).toHaveLength(1) - await deleteItem(hashKey, 'IN_TRANSIT', '2018-01-01T00:01:01.000Z'); - await deleteItem(hashKey, 'PENDING', '2018-06-01T00:01:01.000Z'); - await deleteItem(hashKey, 'PENDING', '2018-09-01T00:01:01.000Z'); -}) + const hashKey = 'TEST_LIST_ID'; + await createItem(hashKey, 'IN_TRANSIT', 'list1', '2018-01-01T00:01:01.000Z'); + await createItem(hashKey, 'PENDING', 'list2', '2018-06-01T00:01:01.000Z'); + await createItem(hashKey, 'PENDING', 'item3', '2018-09-01T00:01:01.000Z'); + let items = await listItem(undefined); + expect(items.data.listItems.items.length).toBeGreaterThan(0); + items = await listItem(hashKey); + expect(items.data.listItems.items).toHaveLength(3); + items = await listItem(hashKey, { beginsWith: { status: 'PENDING' } }); + expect(items.data.listItems.items).toHaveLength(2); + items = await listItem(hashKey, { beginsWith: { status: 'IN_TRANSIT' } }); + expect(items.data.listItems.items).toHaveLength(1); + items = await listItem(hashKey, { beginsWith: { status: 'PENDING', createdAt: '2018-09' } }); + expect(items.data.listItems.items).toHaveLength(1); + items = await listItem(hashKey, { eq: { status: 'PENDING', createdAt: '2018-09-01T00:01:01.000Z' } }); + expect(items.data.listItems.items).toHaveLength(1); + items = await listItem(hashKey, { + between: [{ status: 'PENDING', createdAt: '2018-08-01' }, { status: 'PENDING', createdAt: '2018-10-01' }], + }); + expect(items.data.listItems.items).toHaveLength(1); + items = await listItem(hashKey, { gt: { status: 'PENDING', createdAt: '2018-08-1' } }); + expect(items.data.listItems.items).toHaveLength(1); + items = await listItem(hashKey, { ge: { status: 'PENDING', createdAt: '2018-09-01T00:01:01.000Z' } }); + expect(items.data.listItems.items).toHaveLength(1); + items = await listItem(hashKey, { lt: { status: 'IN_TRANSIT', createdAt: '2018-01-02' } }); + expect(items.data.listItems.items).toHaveLength(1); + items = await listItem(hashKey, { le: { status: 'IN_TRANSIT', createdAt: '2018-01-01T00:01:01.000Z' } }); + expect(items.data.listItems.items).toHaveLength(1); + await deleteItem(hashKey, 'IN_TRANSIT', '2018-01-01T00:01:01.000Z'); + await deleteItem(hashKey, 'PENDING', '2018-06-01T00:01:01.000Z'); + await deleteItem(hashKey, 'PENDING', '2018-09-01T00:01:01.000Z'); +}); test('Test query with three part secondary key.', async () => { - const hashKey = 'UNKNOWN'; - await createItem('order1', 'UNKNOWN', 'list1', '2018-01-01T00:01:01.000Z'); - await createItem('order2', 'UNKNOWN', 'list2', '2018-06-01T00:01:01.000Z'); - await createItem('order3', 'UNKNOWN', 'item3', '2018-09-01T00:01:01.000Z'); - let items = await itemsByStatus(undefined); - expect(items.data).toBeNull(); - expect(items.errors.length).toBeGreaterThan(0); - items = await itemsByStatus(hashKey); - expect(items.data.itemsByStatus.items).toHaveLength(3) - items = await itemsByStatus(hashKey, { beginsWith: '2018-09' }); - expect(items.data.itemsByStatus.items).toHaveLength(1) - items = await itemsByStatus(hashKey, { eq: '2018-09-01T00:01:01.000Z' }); - expect(items.data.itemsByStatus.items).toHaveLength(1) - items = await itemsByStatus(hashKey, { between: ['2018-08-01', '2018-10-01'] }); - expect(items.data.itemsByStatus.items).toHaveLength(1) - items = await itemsByStatus(hashKey, { gt: '2018-08-01' }); - expect(items.data.itemsByStatus.items).toHaveLength(1) - items = await itemsByStatus(hashKey, { ge: '2018-09-01' }); - expect(items.data.itemsByStatus.items).toHaveLength(1) - items = await itemsByStatus(hashKey, { lt: '2018-07-01' }); - expect(items.data.itemsByStatus.items).toHaveLength(2) - items = await itemsByStatus(hashKey, { le: '2018-06-01' }); - expect(items.data.itemsByStatus.items).toHaveLength(1) - items = await itemsByStatus(undefined, { le: '2018-09-01' }); - expect(items.data).toBeNull() - expect(items.errors.length).toBeGreaterThan(0); - await deleteItem('order1', hashKey, '2018-01-01T00:01:01.000Z'); - await deleteItem('order2', hashKey, '2018-06-01T00:01:01.000Z'); - await deleteItem('order3', hashKey, '2018-09-01T00:01:01.000Z'); -}) + const hashKey = 'UNKNOWN'; + await createItem('order1', 'UNKNOWN', 'list1', '2018-01-01T00:01:01.000Z'); + await createItem('order2', 'UNKNOWN', 'list2', '2018-06-01T00:01:01.000Z'); + await createItem('order3', 'UNKNOWN', 'item3', '2018-09-01T00:01:01.000Z'); + let items = await itemsByStatus(undefined); + expect(items.data).toBeNull(); + expect(items.errors.length).toBeGreaterThan(0); + items = await itemsByStatus(hashKey); + expect(items.data.itemsByStatus.items).toHaveLength(3); + items = await itemsByStatus(hashKey, { beginsWith: '2018-09' }); + expect(items.data.itemsByStatus.items).toHaveLength(1); + items = await itemsByStatus(hashKey, { eq: '2018-09-01T00:01:01.000Z' }); + expect(items.data.itemsByStatus.items).toHaveLength(1); + items = await itemsByStatus(hashKey, { between: ['2018-08-01', '2018-10-01'] }); + expect(items.data.itemsByStatus.items).toHaveLength(1); + items = await itemsByStatus(hashKey, { gt: '2018-08-01' }); + expect(items.data.itemsByStatus.items).toHaveLength(1); + items = await itemsByStatus(hashKey, { ge: '2018-09-01' }); + expect(items.data.itemsByStatus.items).toHaveLength(1); + items = await itemsByStatus(hashKey, { lt: '2018-07-01' }); + expect(items.data.itemsByStatus.items).toHaveLength(2); + items = await itemsByStatus(hashKey, { le: '2018-06-01' }); + expect(items.data.itemsByStatus.items).toHaveLength(1); + items = await itemsByStatus(undefined, { le: '2018-09-01' }); + expect(items.data).toBeNull(); + expect(items.errors.length).toBeGreaterThan(0); + await deleteItem('order1', hashKey, '2018-01-01T00:01:01.000Z'); + await deleteItem('order2', hashKey, '2018-06-01T00:01:01.000Z'); + await deleteItem('order3', hashKey, '2018-09-01T00:01:01.000Z'); +}); test('Test query with three part secondary key, where sort key is an enum.', async () => { - const hashKey = '2018-06-01T00:01:01.000Z'; - const sortKey = 'UNKNOWN'; - await createItem('order1', sortKey, 'list1', '2018-01-01T00:01:01.000Z'); - await createItem('order2', sortKey, 'list2', hashKey); - await createItem('order3', sortKey, 'item3', '2018-09-01T00:01:01.000Z'); - let items = await itemsByCreatedAt(undefined); - expect(items.data).toBeNull(); - expect(items.errors.length).toBeGreaterThan(0); - items = await itemsByCreatedAt(hashKey); - expect(items.data.itemsByCreatedAt.items).toHaveLength(1) - items = await itemsByCreatedAt(hashKey, { beginsWith: sortKey }); - expect(items.data.itemsByCreatedAt.items).toHaveLength(1) - items = await itemsByCreatedAt(hashKey, { eq: sortKey }); - expect(items.data.itemsByCreatedAt.items).toHaveLength(1) - items = await itemsByCreatedAt(hashKey, { between: [sortKey, sortKey] }); - expect(items.data.itemsByCreatedAt.items).toHaveLength(1) - items = await itemsByCreatedAt(hashKey, { gt: sortKey }); - expect(items.data.itemsByCreatedAt.items).toHaveLength(0) - items = await itemsByCreatedAt(hashKey, { ge: sortKey }); - expect(items.data.itemsByCreatedAt.items).toHaveLength(1) - items = await itemsByCreatedAt(hashKey, { lt: sortKey }); - expect(items.data.itemsByCreatedAt.items).toHaveLength(0) - items = await itemsByCreatedAt(hashKey, { le: sortKey }); - expect(items.data.itemsByCreatedAt.items).toHaveLength(1) - items = await itemsByCreatedAt(undefined, { le: sortKey }); - expect(items.data).toBeNull() - expect(items.errors.length).toBeGreaterThan(0); - await deleteItem('order1', sortKey, '2018-01-01T00:01:01.000Z'); - await deleteItem('order2', sortKey, hashKey); - await deleteItem('order3', sortKey, '2018-09-01T00:01:01.000Z'); -}) + const hashKey = '2018-06-01T00:01:01.000Z'; + const sortKey = 'UNKNOWN'; + await createItem('order1', sortKey, 'list1', '2018-01-01T00:01:01.000Z'); + await createItem('order2', sortKey, 'list2', hashKey); + await createItem('order3', sortKey, 'item3', '2018-09-01T00:01:01.000Z'); + let items = await itemsByCreatedAt(undefined); + expect(items.data).toBeNull(); + expect(items.errors.length).toBeGreaterThan(0); + items = await itemsByCreatedAt(hashKey); + expect(items.data.itemsByCreatedAt.items).toHaveLength(1); + items = await itemsByCreatedAt(hashKey, { beginsWith: sortKey }); + expect(items.data.itemsByCreatedAt.items).toHaveLength(1); + items = await itemsByCreatedAt(hashKey, { eq: sortKey }); + expect(items.data.itemsByCreatedAt.items).toHaveLength(1); + items = await itemsByCreatedAt(hashKey, { between: [sortKey, sortKey] }); + expect(items.data.itemsByCreatedAt.items).toHaveLength(1); + items = await itemsByCreatedAt(hashKey, { gt: sortKey }); + expect(items.data.itemsByCreatedAt.items).toHaveLength(0); + items = await itemsByCreatedAt(hashKey, { ge: sortKey }); + expect(items.data.itemsByCreatedAt.items).toHaveLength(1); + items = await itemsByCreatedAt(hashKey, { lt: sortKey }); + expect(items.data.itemsByCreatedAt.items).toHaveLength(0); + items = await itemsByCreatedAt(hashKey, { le: sortKey }); + expect(items.data.itemsByCreatedAt.items).toHaveLength(1); + items = await itemsByCreatedAt(undefined, { le: sortKey }); + expect(items.data).toBeNull(); + expect(items.errors.length).toBeGreaterThan(0); + await deleteItem('order1', sortKey, '2018-01-01T00:01:01.000Z'); + await deleteItem('order2', sortKey, hashKey); + await deleteItem('order3', sortKey, '2018-09-01T00:01:01.000Z'); +}); test('Test update mutation validation with three part secondary key.', async () => { - await createShippingUpdate('order1', 'item1', 'PENDING', 'name1'); - const items = await getShippingUpdates('order1'); - expect(items.data.shippingUpdates.items).toHaveLength(1); - const item = items.data.shippingUpdates.items[0]; - expect(item.name).toEqual('name1') - - const itemsWithFilter = await getShippingUpdatesWithNameFilter('order1', 'name1') - expect(itemsWithFilter.data.shippingUpdates.items).toHaveLength(1); - const itemWithFilter = itemsWithFilter.data.shippingUpdates.items[0]; - expect(itemWithFilter.name).toEqual('name1') - - const itemsWithUnknownFilter = await getShippingUpdatesWithNameFilter('order1', 'unknownname') - expect(itemsWithUnknownFilter.data.shippingUpdates.items).toHaveLength(0); - - const updateResponseMissingLastSortKey = await updateShippingUpdate({ id: item.id, orderId: 'order1', itemId: 'item1', name: 'name2'}); - expect(updateResponseMissingLastSortKey.data.updateShippingUpdate).toBeNull(); - expect(updateResponseMissingLastSortKey.errors).toHaveLength(1); - const updateResponseMissingFirstSortKey = await updateShippingUpdate({ id: item.id, orderId: 'order1', status: 'PENDING', name: 'name3'}); - expect(updateResponseMissingFirstSortKey.data.updateShippingUpdate).toBeNull(); - expect(updateResponseMissingFirstSortKey.errors).toHaveLength(1); - const updateResponseMissingAllSortKeys = await updateShippingUpdate({ id: item.id, orderId: 'order1', name: 'testing'}); - expect(updateResponseMissingAllSortKeys.data.updateShippingUpdate.name).toEqual('testing') - const updateResponseMissingNoKeys = await updateShippingUpdate( - { id: item.id, orderId: 'order1', itemId: 'item1', status: 'PENDING', name: 'testing2' }); - expect(updateResponseMissingNoKeys.data.updateShippingUpdate.name).toEqual('testing2') -}) + await createShippingUpdate('order1', 'item1', 'PENDING', 'name1'); + const items = await getShippingUpdates('order1'); + expect(items.data.shippingUpdates.items).toHaveLength(1); + const item = items.data.shippingUpdates.items[0]; + expect(item.name).toEqual('name1'); + + const itemsWithFilter = await getShippingUpdatesWithNameFilter('order1', 'name1'); + expect(itemsWithFilter.data.shippingUpdates.items).toHaveLength(1); + const itemWithFilter = itemsWithFilter.data.shippingUpdates.items[0]; + expect(itemWithFilter.name).toEqual('name1'); + + const itemsWithUnknownFilter = await getShippingUpdatesWithNameFilter('order1', 'unknownname'); + expect(itemsWithUnknownFilter.data.shippingUpdates.items).toHaveLength(0); + + const updateResponseMissingLastSortKey = await updateShippingUpdate({ id: item.id, orderId: 'order1', itemId: 'item1', name: 'name2' }); + expect(updateResponseMissingLastSortKey.data.updateShippingUpdate).toBeNull(); + expect(updateResponseMissingLastSortKey.errors).toHaveLength(1); + const updateResponseMissingFirstSortKey = await updateShippingUpdate({ + id: item.id, + orderId: 'order1', + status: 'PENDING', + name: 'name3', + }); + expect(updateResponseMissingFirstSortKey.data.updateShippingUpdate).toBeNull(); + expect(updateResponseMissingFirstSortKey.errors).toHaveLength(1); + const updateResponseMissingAllSortKeys = await updateShippingUpdate({ id: item.id, orderId: 'order1', name: 'testing' }); + expect(updateResponseMissingAllSortKeys.data.updateShippingUpdate.name).toEqual('testing'); + const updateResponseMissingNoKeys = await updateShippingUpdate({ + id: item.id, + orderId: 'order1', + itemId: 'item1', + status: 'PENDING', + name: 'testing2', + }); + expect(updateResponseMissingNoKeys.data.updateShippingUpdate.name).toEqual('testing2'); +}); test('Test Customer Create with list member and secondary key', async () => { - const createCustomer1 = await createCustomer("customer1@email.com", ["thing1", "thing2"], "customerusr1"); - const getCustomer1 = await getCustomer("customer1@email.com"); - expect(getCustomer1.data.getCustomer.addresslist).toEqual(["thing1", "thing2"]); - // const items = await onCreateCustomer -}) + const createCustomer1 = await createCustomer('customer1@email.com', ['thing1', 'thing2'], 'customerusr1'); + const getCustomer1 = await getCustomer('customer1@email.com'); + expect(getCustomer1.data.getCustomer.addresslist).toEqual(['thing1', 'thing2']); + // const items = await onCreateCustomer +}); test('Test Customer Mutation with list member', async () => { - const updateCustomer1 = await updateCustomer("customer1@email.com", ["thing3", "thing4"], "new_customerusr1"); - const getCustomer1 = await getCustomer("customer1@email.com"); - expect(getCustomer1.data.getCustomer.addresslist).toEqual(["thing3", "thing4"]); -}) + const updateCustomer1 = await updateCustomer('customer1@email.com', ['thing3', 'thing4'], 'new_customerusr1'); + const getCustomer1 = await getCustomer('customer1@email.com'); + expect(getCustomer1.data.getCustomer.addresslist).toEqual(['thing3', 'thing4']); +}); test('Test @key directive with customer sortDirection', async () => { - await createOrder('testorder1@email.com', '1', '2016-03-10'); - await createOrder('testorder1@email.com', '2', '2018-05-22'); - await createOrder('testorder1@email.com', '3', '2019-06-27'); - const newOrders = await listOrders('testorder1@email.com', { beginsWith: "201" }, "DESC"); - const oldOrders = await listOrders('testorder1@email.com', { beginsWith: "201" }, "ASC"); - expect(newOrders.data.listOrders.items[0].createdAt).toEqual('2019-06-27'); - expect(newOrders.data.listOrders.items[0].orderId).toEqual('3'); - expect(oldOrders.data.listOrders.items[0].createdAt).toEqual('2016-03-10'); - expect(oldOrders.data.listOrders.items[0].orderId).toEqual('1'); -}) + await createOrder('testorder1@email.com', '1', '2016-03-10'); + await createOrder('testorder1@email.com', '2', '2018-05-22'); + await createOrder('testorder1@email.com', '3', '2019-06-27'); + const newOrders = await listOrders('testorder1@email.com', { beginsWith: '201' }, 'DESC'); + const oldOrders = await listOrders('testorder1@email.com', { beginsWith: '201' }, 'ASC'); + expect(newOrders.data.listOrders.items[0].createdAt).toEqual('2019-06-27'); + expect(newOrders.data.listOrders.items[0].orderId).toEqual('3'); + expect(oldOrders.data.listOrders.items[0].createdAt).toEqual('2016-03-10'); + expect(oldOrders.data.listOrders.items[0].orderId).toEqual('1'); +}); // orderId: string, itemId: string, status: string, name?: string // DELIVERED IN_TRANSIT PENDING UNKNOWN // (orderId: string, itemId: string, sortDirection: string) test('Test @key directive with sortDirection on GSI', async () => { - await createShippingUpdate("order1", "product1", "PENDING", "order1Name1"); - await createShippingUpdate("order1", "product2", "IN_TRANSIT", "order1Name2"); - await createShippingUpdate("order1", "product3", "DELIVERED", "order1Name3"); - await createShippingUpdate("order1", "product4", "DELIVERED", "order1Name4"); - const newShippingUpdates = await listGSIShippingUpdate('order1', { beginsWith: { itemId: 'product' } }, 'DESC'); - const oldShippingUpdates = await listGSIShippingUpdate('order1', { beginsWith: { itemId: 'product' } }, 'ASC'); - expect(oldShippingUpdates.data.shippingUpdates.items[0].status).toEqual('PENDING'); - expect(oldShippingUpdates.data.shippingUpdates.items[0].name).toEqual('testing2'); - expect(newShippingUpdates.data.shippingUpdates.items[0].status).toEqual('DELIVERED'); - expect(newShippingUpdates.data.shippingUpdates.items[0].name).toEqual('order1Name4'); -}) + await createShippingUpdate('order1', 'product1', 'PENDING', 'order1Name1'); + await createShippingUpdate('order1', 'product2', 'IN_TRANSIT', 'order1Name2'); + await createShippingUpdate('order1', 'product3', 'DELIVERED', 'order1Name3'); + await createShippingUpdate('order1', 'product4', 'DELIVERED', 'order1Name4'); + const newShippingUpdates = await listGSIShippingUpdate('order1', { beginsWith: { itemId: 'product' } }, 'DESC'); + const oldShippingUpdates = await listGSIShippingUpdate('order1', { beginsWith: { itemId: 'product' } }, 'ASC'); + expect(oldShippingUpdates.data.shippingUpdates.items[0].status).toEqual('PENDING'); + expect(oldShippingUpdates.data.shippingUpdates.items[0].name).toEqual('testing2'); + expect(newShippingUpdates.data.shippingUpdates.items[0].status).toEqual('DELIVERED'); + expect(newShippingUpdates.data.shippingUpdates.items[0].name).toEqual('order1Name4'); +}); async function createCustomer(email: string, addresslist: string[], username: string) { - const result = await GRAPHQL_CLIENT.query(`mutation CreateCustomer($input: CreateCustomerInput!) { + const result = await GRAPHQL_CLIENT.query( + `mutation CreateCustomer($input: CreateCustomerInput!) { createCustomer(input: $input) { email addresslist username } - }`, { - input: {email, addresslist, username} - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { email, addresslist, username }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function updateCustomer(email: string, addresslist: string[], username: string) { - const result = await GRAPHQL_CLIENT.query(`mutation UpdateCustomer($input: UpdateCustomerInput!) { + const result = await GRAPHQL_CLIENT.query( + `mutation UpdateCustomer($input: UpdateCustomerInput!) { updateCustomer(input: $input) { email addresslist username } - }`, { - input: {email, addresslist, username} - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { email, addresslist, username }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function getCustomer(email: string) { - const result = await GRAPHQL_CLIENT.query(`query GetCustomer($email: String!) { + const result = await GRAPHQL_CLIENT.query( + `query GetCustomer($email: String!) { getCustomer(email: $email) { email addresslist username } - }`, { - email - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + email, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function createOrder(customerEmail: string, orderId: string, createdAt: string = new Date().toISOString()) { - const result = await GRAPHQL_CLIENT.query(`mutation CreateOrder($input: CreateOrderInput!) { + const result = await GRAPHQL_CLIENT.query( + `mutation CreateOrder($input: CreateOrderInput!) { createOrder(input: $input) { customerEmail orderId createdAt } - }`, { - input: { customerEmail, orderId, createdAt } - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { customerEmail, orderId, createdAt }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function updateOrder(customerEmail: string, createdAt: string, orderId: string) { - const result = await GRAPHQL_CLIENT.query(`mutation UpdateOrder($input: UpdateOrderInput!) { + const result = await GRAPHQL_CLIENT.query( + `mutation UpdateOrder($input: UpdateOrderInput!) { updateOrder(input: $input) { customerEmail orderId createdAt } - }`, { - input: { customerEmail, orderId, createdAt } - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { customerEmail, orderId, createdAt }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function deleteOrder(customerEmail: string, createdAt: string) { - const result = await GRAPHQL_CLIENT.query(`mutation DeleteOrder($input: DeleteOrderInput!) { + const result = await GRAPHQL_CLIENT.query( + `mutation DeleteOrder($input: DeleteOrderInput!) { deleteOrder(input: $input) { customerEmail orderId createdAt } - }`, { - input: { customerEmail, createdAt } - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { customerEmail, createdAt }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function getOrder(customerEmail: string, createdAt: string) { - const result = await GRAPHQL_CLIENT.query(`query GetOrder($customerEmail: String!, $createdAt: String!) { + const result = await GRAPHQL_CLIENT.query( + `query GetOrder($customerEmail: String!, $createdAt: String!) { getOrder(customerEmail: $customerEmail, createdAt: $createdAt) { customerEmail orderId createdAt } - }`, { customerEmail, createdAt }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { customerEmail, createdAt } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } interface ModelStringKeyConditionInput { - eq?: string, - gt?: string, - ge?: string, - lt?: string, - le?: string, - between?: string[], - beginsWith?: string, + eq?: string; + gt?: string; + ge?: string; + lt?: string; + le?: string; + between?: string[]; + beginsWith?: string; } -async function listOrders(customerEmail: string, createdAt: ModelStringKeyConditionInput, sortDirection: string ) { - const input = { customerEmail, createdAt, sortDirection }; - const result = await GRAPHQL_CLIENT.query(`query ListOrders( +async function listOrders(customerEmail: string, createdAt: ModelStringKeyConditionInput, sortDirection: string) { + const input = { customerEmail, createdAt, sortDirection }; + const result = await GRAPHQL_CLIENT.query( + `query ListOrders( $customerEmail: String, $createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection) { listOrders(customerEmail: $customerEmail, createdAt: $createdAt, sortDirection: $sortDirection) { items { @@ -463,100 +509,115 @@ async function listOrders(customerEmail: string, createdAt: ModelStringKeyCondit createdAt } } - }`, input); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + input + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function createItem(orderId: string, status: string, name: string, createdAt: string = new Date().toISOString()) { - const input = { status, orderId, name, createdAt }; - const result = await GRAPHQL_CLIENT.query(`mutation CreateItem($input: CreateItemInput!) { + const input = { status, orderId, name, createdAt }; + const result = await GRAPHQL_CLIENT.query( + `mutation CreateItem($input: CreateItemInput!) { createItem(input: $input) { orderId status createdAt name } - }`, { - input - }); - console.log(`Running create: ${JSON.stringify(input)}`); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input, + } + ); + console.log(`Running create: ${JSON.stringify(input)}`); + console.log(JSON.stringify(result, null, 4)); + return result; } async function updateItem(orderId: string, status: string, createdAt: string, name: string) { - const input = { status, orderId, createdAt, name }; - const result = await GRAPHQL_CLIENT.query(`mutation UpdateItem($input: UpdateItemInput!) { + const input = { status, orderId, createdAt, name }; + const result = await GRAPHQL_CLIENT.query( + `mutation UpdateItem($input: UpdateItemInput!) { updateItem(input: $input) { orderId status createdAt name } - }`, { - input - }); - console.log(`Running create: ${JSON.stringify(input)}`); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input, + } + ); + console.log(`Running create: ${JSON.stringify(input)}`); + console.log(JSON.stringify(result, null, 4)); + return result; } async function deleteItem(orderId: string, status: string, createdAt: string) { - const input = { orderId, status, createdAt }; - const result = await GRAPHQL_CLIENT.query(`mutation DeleteItem($input: DeleteItemInput!) { + const input = { orderId, status, createdAt }; + const result = await GRAPHQL_CLIENT.query( + `mutation DeleteItem($input: DeleteItemInput!) { deleteItem(input: $input) { orderId status createdAt name } - }`, { - input - }); - console.log(`Running delete: ${JSON.stringify(input)}`); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input, + } + ); + console.log(`Running delete: ${JSON.stringify(input)}`); + console.log(JSON.stringify(result, null, 4)); + return result; } async function getItem(orderId: string, status: string, createdAt: string) { - const result = await GRAPHQL_CLIENT.query(`query GetItem($orderId: ID!, $status: Status!, $createdAt: AWSDateTime!) { + const result = await GRAPHQL_CLIENT.query( + `query GetItem($orderId: ID!, $status: Status!, $createdAt: AWSDateTime!) { getItem(orderId: $orderId, status: $status, createdAt: $createdAt) { orderId status createdAt name } - }`, { orderId, status, createdAt }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { orderId, status, createdAt } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } interface StringKeyConditionInput { - eq?: string, - gt?: string, - ge?: string, - lt?: string, - le?: string, - between?: string[], - beginsWith?: string, + eq?: string; + gt?: string; + ge?: string; + lt?: string; + le?: string; + between?: string[]; + beginsWith?: string; } interface ItemCompositeKeyConditionInput { - eq?: ItemCompositeKeyInput, - gt?: ItemCompositeKeyInput, - ge?: ItemCompositeKeyInput, - lt?: ItemCompositeKeyInput, - le?: ItemCompositeKeyInput, - between?: ItemCompositeKeyInput[], - beginsWith?: ItemCompositeKeyInput, + eq?: ItemCompositeKeyInput; + gt?: ItemCompositeKeyInput; + ge?: ItemCompositeKeyInput; + lt?: ItemCompositeKeyInput; + le?: ItemCompositeKeyInput; + between?: ItemCompositeKeyInput[]; + beginsWith?: ItemCompositeKeyInput; } interface ItemCompositeKeyInput { - status?: string, - createdAt?: string + status?: string; + createdAt?: string; } async function listItem(orderId?: string, statusCreatedAt?: ItemCompositeKeyConditionInput, limit?: number, nextToken?: string) { - const result = await GRAPHQL_CLIENT.query(`query ListItems( + const result = await GRAPHQL_CLIENT.query( + `query ListItems( $orderId: ID, $statusCreatedAt: ModelItemPrimaryCompositeKeyConditionInput, $limit: Int, $nextToken: String) { listItems(orderId: $orderId, statusCreatedAt: $statusCreatedAt, limit: $limit, nextToken: $nextToken) { items { @@ -567,13 +628,16 @@ async function listItem(orderId?: string, statusCreatedAt?: ItemCompositeKeyCond } nextToken } - }`, { orderId, statusCreatedAt, limit, nextToken }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { orderId, statusCreatedAt, limit, nextToken } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function itemsByStatus(status: string, createdAt?: StringKeyConditionInput, limit?: number, nextToken?: string) { - const result = await GRAPHQL_CLIENT.query(`query ListByStatus( + const result = await GRAPHQL_CLIENT.query( + `query ListByStatus( $status: Status!, $createdAt: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) { itemsByStatus(status: $status, createdAt: $createdAt, limit: $limit, nextToken: $nextToken) { items { @@ -584,13 +648,16 @@ async function itemsByStatus(status: string, createdAt?: StringKeyConditionInput } nextToken } - }`, { status, createdAt, limit, nextToken }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { status, createdAt, limit, nextToken } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function itemsByCreatedAt(createdAt: string, status?: StringKeyConditionInput, limit?: number, nextToken?: string) { - const result = await GRAPHQL_CLIENT.query(`query ListByCreatedAt( + const result = await GRAPHQL_CLIENT.query( + `query ListByCreatedAt( $createdAt: AWSDateTime!, $status: ModelStringKeyConditionInput, $limit: Int, $nextToken: String) { itemsByCreatedAt(createdAt: $createdAt, status: $status, limit: $limit, nextToken: $nextToken) { items { @@ -601,14 +668,17 @@ async function itemsByCreatedAt(createdAt: string, status?: StringKeyConditionIn } nextToken } - }`, { createdAt, status, limit, nextToken }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { createdAt, status, limit, nextToken } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function createShippingUpdate(orderId: string, itemId: string, status: string, name?: string) { - const input = { status, orderId, itemId, name }; - const result = await GRAPHQL_CLIENT.query(`mutation CreateShippingUpdate($input: CreateShippingUpdateInput!) { + const input = { status, orderId, itemId, name }; + const result = await GRAPHQL_CLIENT.query( + `mutation CreateShippingUpdate($input: CreateShippingUpdateInput!) { createShippingUpdate(input: $input) { orderId status @@ -616,17 +686,20 @@ async function createShippingUpdate(orderId: string, itemId: string, status: str name id } - }`, { - input - }); - console.log(`Running create: ${JSON.stringify(input)}`); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input, + } + ); + console.log(`Running create: ${JSON.stringify(input)}`); + console.log(JSON.stringify(result, null, 4)); + return result; } async function listGSIShippingUpdate(orderId: string, itemId: object, sortDirection: string) { - const input = { orderId, itemId, sortDirection } - const result = await GRAPHQL_CLIENT.query(`query queryGSI( + const input = { orderId, itemId, sortDirection }; + const result = await GRAPHQL_CLIENT.query( + `query queryGSI( $orderId: ID, $itemIdStatus: ModelShippingUpdateByOrderItemStatusCompositeKeyConditionInput, $sortDirection: ModelSortDirection) { @@ -640,18 +713,25 @@ async function listGSIShippingUpdate(orderId: string, itemId: object, sortDirect status } } - }`, input); - console.log(`Running create: ${JSON.stringify(input)}`); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + input + ); + console.log(`Running create: ${JSON.stringify(input)}`); + console.log(JSON.stringify(result, null, 4)); + return result; } interface UpdateShippingInput { - id: string, orderId?: string, status?: string, itemId?: string, name?: string + id: string; + orderId?: string; + status?: string; + itemId?: string; + name?: string; } async function updateShippingUpdate(input: UpdateShippingInput) { - // const input = { id, status, orderId, itemId, name }; - const result = await GRAPHQL_CLIENT.query(`mutation UpdateShippingUpdate($input: UpdateShippingUpdateInput!) { + // const input = { id, status, orderId, itemId, name }; + const result = await GRAPHQL_CLIENT.query( + `mutation UpdateShippingUpdate($input: UpdateShippingUpdateInput!) { updateShippingUpdate(input: $input) { orderId status @@ -659,16 +739,19 @@ async function updateShippingUpdate(input: UpdateShippingInput) { name id } - }`, { - input - }); - console.log(`Running update: ${JSON.stringify(input)}`); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input, + } + ); + console.log(`Running update: ${JSON.stringify(input)}`); + console.log(JSON.stringify(result, null, 4)); + return result; } async function getShippingUpdates(orderId: string) { - const result = await GRAPHQL_CLIENT.query(`query GetShippingUpdates($orderId: ID!) { + const result = await GRAPHQL_CLIENT.query( + `query GetShippingUpdates($orderId: ID!) { shippingUpdates(orderId: $orderId) { items { id @@ -679,13 +762,16 @@ async function getShippingUpdates(orderId: string) { } nextToken } - }`, { orderId }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { orderId } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function getShippingUpdatesWithNameFilter(orderId: string, name: string) { - const result = await GRAPHQL_CLIENT.query(`query GetShippingUpdates($orderId: ID!, $name: String) { + const result = await GRAPHQL_CLIENT.query( + `query GetShippingUpdates($orderId: ID!, $name: String) { shippingUpdates(orderId: $orderId, filter: { name: { eq: $name }}) { items { id @@ -696,7 +782,9 @@ async function getShippingUpdatesWithNameFilter(orderId: string, name: string) { } nextToken } - }`, { orderId, name }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { orderId, name } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts index 58102c46e0..c889b7b0a4 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyTransformerLocal.e2e.test.ts @@ -1,214 +1,192 @@ -import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core' +import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core'; import ModelTransformer from 'graphql-dynamodb-transformer'; import KeyTransformer from 'graphql-key-transformer'; -import { parse, FieldDefinitionNode, ObjectTypeDefinitionNode, - Kind, InputObjectTypeDefinitionNode } from 'graphql'; -import { expectArguments, expectNonNullFields, expectNullableFields, - expectNonNullInputValues, expectNullableInputValues, expectInputValueToHandle } from '../testUtil'; +import { parse, FieldDefinitionNode, ObjectTypeDefinitionNode, Kind, InputObjectTypeDefinitionNode } from 'graphql'; +import { + expectArguments, + expectNonNullFields, + expectNullableFields, + expectNonNullInputValues, + expectNullableInputValues, + expectInputValueToHandle, +} from '../testUtil'; test('Test that a primary @key with a single field changes the hash key.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(fields: ["email"]) { email: String! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - expect( - tableResource.Properties.KeySchema[0].AttributeName, - ).toEqual('email'); - expect( - tableResource.Properties.KeySchema[0].KeyType, - ).toEqual('HASH'); - expect( - tableResource.Properties.AttributeDefinitions[0].AttributeType, - ).toEqual('S'); - const schema = parse(out.schema); - const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; - expect(getTestField.arguments).toHaveLength(1); - expectArguments(getTestField, ['email']) -}) + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + expect(tableResource.Properties.KeySchema[0].AttributeName).toEqual('email'); + expect(tableResource.Properties.KeySchema[0].KeyType).toEqual('HASH'); + expect(tableResource.Properties.AttributeDefinitions[0].AttributeType).toEqual('S'); + const schema = parse(out.schema); + const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; + const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; + expect(getTestField.arguments).toHaveLength(1); + expectArguments(getTestField, ['email']); +}); test('Test that a primary @key with 2 fields changes the hash and sort key.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(fields: ["email", "kind"]) { email: String! kind: Int! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - const hashKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'HASH'); - const hashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); - const rangeKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'RANGE'); - const rangeKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'kind'); - expect(tableResource.Properties.AttributeDefinitions).toHaveLength(2); - expect(hashKey.AttributeName).toEqual('email'); - expect(rangeKey.AttributeName).toEqual('kind'); - expect(hashKeyAttr.AttributeType).toEqual('S'); - expect(rangeKeyAttr.AttributeType).toEqual('N'); - - const schema = parse(out.schema); - const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; - expect(getTestField.arguments).toHaveLength(2); - expectArguments(getTestField, ['email', 'kind']) -}) + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + const hashKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'HASH'); + const hashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); + const rangeKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'RANGE'); + const rangeKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'kind'); + expect(tableResource.Properties.AttributeDefinitions).toHaveLength(2); + expect(hashKey.AttributeName).toEqual('email'); + expect(rangeKey.AttributeName).toEqual('kind'); + expect(hashKeyAttr.AttributeType).toEqual('S'); + expect(rangeKeyAttr.AttributeType).toEqual('N'); + + const schema = parse(out.schema); + const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; + const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; + expect(getTestField.arguments).toHaveLength(2); + expectArguments(getTestField, ['email', 'kind']); +}); test('Test that a primary @key with 3 fields changes the hash and sort keys.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(fields: ["email", "kind", "date"]) { email: String! kind: Int! date: AWSDateTime! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - const hashKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'HASH'); - const hashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); - const rangeKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'RANGE'); - const rangeKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'kind#date'); - expect(tableResource.Properties.AttributeDefinitions).toHaveLength(2); - expect(hashKey.AttributeName).toEqual('email'); - expect(rangeKey.AttributeName).toEqual('kind#date'); - expect(hashKeyAttr.AttributeType).toEqual('S'); - // composite keys will always be strings. - expect(rangeKeyAttr.AttributeType).toEqual('S'); - - const schema = parse(out.schema); - const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; - expect(getTestField.arguments).toHaveLength(3); - expectArguments(getTestField, ['email', 'kind', 'date']); - - const listTestField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; - expect(listTestField.arguments).toHaveLength(6); - expectArguments(listTestField, ['email', 'kindDate', 'filter', 'nextToken', 'limit', 'sortDirection']); -}) + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + const hashKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'HASH'); + const hashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); + const rangeKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'RANGE'); + const rangeKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'kind#date'); + expect(tableResource.Properties.AttributeDefinitions).toHaveLength(2); + expect(hashKey.AttributeName).toEqual('email'); + expect(rangeKey.AttributeName).toEqual('kind#date'); + expect(hashKeyAttr.AttributeType).toEqual('S'); + // composite keys will always be strings. + expect(rangeKeyAttr.AttributeType).toEqual('S'); + + const schema = parse(out.schema); + const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; + const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; + expect(getTestField.arguments).toHaveLength(3); + expectArguments(getTestField, ['email', 'kind', 'date']); + + const listTestField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; + expect(listTestField.arguments).toHaveLength(6); + expectArguments(listTestField, ['email', 'kindDate', 'filter', 'nextToken', 'limit', 'sortDirection']); +}); test('Test that a secondary @key with 3 fields changes the hash and sort keys and adds a query fields correctly.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(name: "GSI", fields: ["email", "kind", "date"], queryField: "listByEmailKindDate") { email: String! kind: Int! date: AWSDateTime! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - console.log(out.schema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - const hashKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'HASH'); - const hashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); - expect(tableResource.Properties.AttributeDefinitions).toHaveLength(3); - expect(hashKey.AttributeName).toEqual('id'); - expect(hashKeyAttr.AttributeType).toEqual('S'); - // composite keys will always be strings. - - const gsi = tableResource.Properties.GlobalSecondaryIndexes.find(o => o.IndexName === 'GSI') - const gsiHashKey = gsi.KeySchema.find(o => o.KeyType === 'HASH'); - const gsiHashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); - const gsiRangeKey = gsi.KeySchema.find(o => o.KeyType === 'RANGE'); - const gsiRangeKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'kind#date'); - expect(gsiHashKey.AttributeName).toEqual('email'); - expect(gsiRangeKey.AttributeName).toEqual('kind#date'); - expect(gsiHashKeyAttr.AttributeType).toEqual('S'); - expect(gsiRangeKeyAttr.AttributeType).toEqual('S'); - - const schema = parse(out.schema); - const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; - expect(getTestField.arguments).toHaveLength(1); - expectArguments(getTestField, ['id']); - - const queryField = queryType.fields.find(f => f.name && f.name.value === 'listByEmailKindDate') as FieldDefinitionNode; - expect(queryField.arguments).toHaveLength(6); - expectArguments(queryField, ['email', 'kindDate', 'filter', 'nextToken', 'limit', 'sortDirection']); - - const listTestField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; - expect(listTestField.arguments).toHaveLength(3); - expectArguments(listTestField, ['filter', 'nextToken', 'limit']); -}) + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + console.log(out.schema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + const hashKey = tableResource.Properties.KeySchema.find(o => o.KeyType === 'HASH'); + const hashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); + expect(tableResource.Properties.AttributeDefinitions).toHaveLength(3); + expect(hashKey.AttributeName).toEqual('id'); + expect(hashKeyAttr.AttributeType).toEqual('S'); + // composite keys will always be strings. + + const gsi = tableResource.Properties.GlobalSecondaryIndexes.find(o => o.IndexName === 'GSI'); + const gsiHashKey = gsi.KeySchema.find(o => o.KeyType === 'HASH'); + const gsiHashKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'email'); + const gsiRangeKey = gsi.KeySchema.find(o => o.KeyType === 'RANGE'); + const gsiRangeKeyAttr = tableResource.Properties.AttributeDefinitions.find(o => o.AttributeName === 'kind#date'); + expect(gsiHashKey.AttributeName).toEqual('email'); + expect(gsiRangeKey.AttributeName).toEqual('kind#date'); + expect(gsiHashKeyAttr.AttributeType).toEqual('S'); + expect(gsiRangeKeyAttr.AttributeType).toEqual('S'); + + const schema = parse(out.schema); + const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; + const getTestField = queryType.fields.find(f => f.name && f.name.value === 'getTest') as FieldDefinitionNode; + expect(getTestField.arguments).toHaveLength(1); + expectArguments(getTestField, ['id']); + + const queryField = queryType.fields.find(f => f.name && f.name.value === 'listByEmailKindDate') as FieldDefinitionNode; + expect(queryField.arguments).toHaveLength(6); + expectArguments(queryField, ['email', 'kindDate', 'filter', 'nextToken', 'limit', 'sortDirection']); + + const listTestField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; + expect(listTestField.arguments).toHaveLength(3); + expectArguments(listTestField, ['filter', 'nextToken', 'limit']); +}); test('Test that a secondary @key with a single field adds a GSI.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(name: "GSI_Email", fields: ["email"], queryField: "testsByEmail") { id: ID! email: String! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - expect( - tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].AttributeName, - ).toEqual('email'); - expect( - tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].KeyType, - ).toEqual('HASH'); - expect( - tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'email').AttributeType, - ).toEqual('S'); - const schema = parse(out.schema); - const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const getField = queryType.fields.find(f => f.name.value === 'getTest'); - expect(getField.arguments).toHaveLength(1); - expectArguments(getField, ['id']) - const listTestsField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; - expect(listTestsField.arguments).toHaveLength(3); - expectArguments(listTestsField, ['filter', 'nextToken', 'limit']); - const queryIndexField = queryType.fields.find(f => f.name && f.name.value === 'testsByEmail') as FieldDefinitionNode; - expect(queryIndexField.arguments).toHaveLength(5); - expectArguments(queryIndexField, ['email', 'filter', 'nextToken', 'limit', 'sortDirection']); -}) + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + expect(tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].AttributeName).toEqual('email'); + expect(tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].KeyType).toEqual('HASH'); + expect(tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'email').AttributeType).toEqual('S'); + const schema = parse(out.schema); + const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; + const getField = queryType.fields.find(f => f.name.value === 'getTest'); + expect(getField.arguments).toHaveLength(1); + expectArguments(getField, ['id']); + const listTestsField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; + expect(listTestsField.arguments).toHaveLength(3); + expectArguments(listTestsField, ['filter', 'nextToken', 'limit']); + const queryIndexField = queryType.fields.find(f => f.name && f.name.value === 'testsByEmail') as FieldDefinitionNode; + expect(queryIndexField.arguments).toHaveLength(5); + expectArguments(queryIndexField, ['email', 'filter', 'nextToken', 'limit', 'sortDirection']); +}); test('Test that a secondary @key with a multiple field adds an GSI.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(fields: ["email", "createdAt"]) @key(name: "CategoryGSI", fields: ["category", "createdAt"], queryField: "testsByCategory") { email: String! @@ -216,67 +194,49 @@ test('Test that a secondary @key with a multiple field adds an GSI.', () => { category: String! description: String } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - expect( - tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].AttributeName, - ).toEqual('category'); - expect( - tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].KeyType, - ).toEqual('HASH'); - expect( - tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[1].AttributeName, - ).toEqual('createdAt'); - expect( - tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[1].KeyType, - ).toEqual('RANGE'); - expect( - tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'email').AttributeType, - ).toEqual('S'); - expect( - tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'category').AttributeType, - ).toEqual('S'); - expect( - tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'createdAt').AttributeType, - ).toEqual('S'); - const schema = parse(out.schema); - const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const queryIndexField = queryType.fields.find(f => f.name && f.name.value === 'testsByCategory') as FieldDefinitionNode; - expect(queryIndexField.arguments).toHaveLength(6); - expectArguments(queryIndexField, ['category', 'createdAt', 'filter', 'nextToken', 'limit', 'sortDirection']); - - // When using a complex primary key args are added to the list field. They are optional and if provided, will use a Query instead of a Scan. - const listTestsField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; - expect(listTestsField.arguments).toHaveLength(6); - expectArguments(listTestsField, ['email', 'createdAt', 'filter', 'nextToken', 'limit', 'sortDirection']); - - // Check the create, update, delete inputs. - const createInput = schema.definitions.find((def: any) => def.name && def.name.value === 'CreateTestInput') as ObjectTypeDefinitionNode; - expectNonNullFields(createInput, ['email', 'createdAt', 'category']); - expectNullableFields(createInput, ['description']); - expect(createInput.fields).toHaveLength(4); - const updateInput = schema.definitions.find((def: any) => def.name && def.name.value === 'UpdateTestInput') as ObjectTypeDefinitionNode; - expectNonNullFields(updateInput, ['email', 'createdAt']); - expectNullableFields(updateInput, ['category', 'description']); - expect(updateInput.fields).toHaveLength(4); - const deleteInput = schema.definitions.find((def: any) => def.name && def.name.value === 'DeleteTestInput') as ObjectTypeDefinitionNode; - expectNonNullFields(deleteInput, ['email', 'createdAt']); - expect(deleteInput.fields).toHaveLength(2); -}) + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + expect(tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].AttributeName).toEqual('category'); + expect(tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[0].KeyType).toEqual('HASH'); + expect(tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[1].AttributeName).toEqual('createdAt'); + expect(tableResource.Properties.GlobalSecondaryIndexes[0].KeySchema[1].KeyType).toEqual('RANGE'); + expect(tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'email').AttributeType).toEqual('S'); + expect(tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'category').AttributeType).toEqual('S'); + expect(tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'createdAt').AttributeType).toEqual('S'); + const schema = parse(out.schema); + const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; + const queryIndexField = queryType.fields.find(f => f.name && f.name.value === 'testsByCategory') as FieldDefinitionNode; + expect(queryIndexField.arguments).toHaveLength(6); + expectArguments(queryIndexField, ['category', 'createdAt', 'filter', 'nextToken', 'limit', 'sortDirection']); + + // When using a complex primary key args are added to the list field. They are optional and if provided, will use a Query instead of a Scan. + const listTestsField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; + expect(listTestsField.arguments).toHaveLength(6); + expectArguments(listTestsField, ['email', 'createdAt', 'filter', 'nextToken', 'limit', 'sortDirection']); + + // Check the create, update, delete inputs. + const createInput = schema.definitions.find((def: any) => def.name && def.name.value === 'CreateTestInput') as ObjectTypeDefinitionNode; + expectNonNullFields(createInput, ['email', 'createdAt', 'category']); + expectNullableFields(createInput, ['description']); + expect(createInput.fields).toHaveLength(4); + const updateInput = schema.definitions.find((def: any) => def.name && def.name.value === 'UpdateTestInput') as ObjectTypeDefinitionNode; + expectNonNullFields(updateInput, ['email', 'createdAt']); + expectNullableFields(updateInput, ['category', 'description']); + expect(updateInput.fields).toHaveLength(4); + const deleteInput = schema.definitions.find((def: any) => def.name && def.name.value === 'DeleteTestInput') as ObjectTypeDefinitionNode; + expectNonNullFields(deleteInput, ['email', 'createdAt']); + expect(deleteInput.fields).toHaveLength(2); +}); test('Test that a secondary @key with a multiple field adds an LSI.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(fields: ["email", "createdAt"]) @key(name: "GSI_Email_UpdatedAt", fields: ["email", "updatedAt"], queryField: "testsByEmailByUpdatedAt") { @@ -284,53 +244,36 @@ test('Test that a secondary @key with a multiple field adds an LSI.', () => { createdAt: String! updatedAt: String! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - expect( - tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[0].AttributeName, - ).toEqual('email'); - expect( - tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[0].KeyType, - ).toEqual('HASH'); - expect( - tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[1].AttributeName, - ).toEqual('updatedAt'); - expect( - tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[1].KeyType, - ).toEqual('RANGE'); - expect( - tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'email').AttributeType, - ).toEqual('S'); - expect( - tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'updatedAt').AttributeType, - ).toEqual('S'); - expect( - tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'createdAt').AttributeType, - ).toEqual('S'); - const schema = parse(out.schema); - const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const queryIndexField = queryType.fields.find(f => f.name && f.name.value === 'testsByEmailByUpdatedAt') as FieldDefinitionNode; - expect(queryIndexField.arguments).toHaveLength(6); - expectArguments(queryIndexField, ['email', 'updatedAt', 'filter', 'nextToken', 'limit', 'sortDirection']); - - // When using a complex primary key args are added to the list field. They are optional and if provided, will use a Query instead of a Scan. - const listTestsField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; - expect(listTestsField.arguments).toHaveLength(6); - expectArguments(listTestsField, ['email', 'createdAt', 'filter', 'nextToken', 'limit', 'sortDirection']); -}) + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + expect(tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[0].AttributeName).toEqual('email'); + expect(tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[0].KeyType).toEqual('HASH'); + expect(tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[1].AttributeName).toEqual('updatedAt'); + expect(tableResource.Properties.LocalSecondaryIndexes[0].KeySchema[1].KeyType).toEqual('RANGE'); + expect(tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'email').AttributeType).toEqual('S'); + expect(tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'updatedAt').AttributeType).toEqual('S'); + expect(tableResource.Properties.AttributeDefinitions.find(ad => ad.AttributeName === 'createdAt').AttributeType).toEqual('S'); + const schema = parse(out.schema); + const queryType = schema.definitions.find((def: any) => def.name && def.name.value === 'Query') as ObjectTypeDefinitionNode; + const queryIndexField = queryType.fields.find(f => f.name && f.name.value === 'testsByEmailByUpdatedAt') as FieldDefinitionNode; + expect(queryIndexField.arguments).toHaveLength(6); + expectArguments(queryIndexField, ['email', 'updatedAt', 'filter', 'nextToken', 'limit', 'sortDirection']); + + // When using a complex primary key args are added to the list field. They are optional and if provided, will use a Query instead of a Scan. + const listTestsField = queryType.fields.find(f => f.name && f.name.value === 'listTests') as FieldDefinitionNode; + expect(listTestsField.arguments).toHaveLength(6); + expectArguments(listTestsField, ['email', 'createdAt', 'filter', 'nextToken', 'limit', 'sortDirection']); +}); test('Test that a primary @key with complex fields will update the input objects.', () => { - const validSchema = ` + const validSchema = ` type Test @model @key(fields: ["email"]) { email: String! listInput: [String] @@ -339,55 +282,52 @@ test('Test that a primary @key with complex fields will update the input objects } `; - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new KeyTransformer() - ] - }); - - const out = transformer.transform(validSchema); - let tableResource = out.stacks.Test.Resources.TestTable; - expect(tableResource).toBeDefined() - expect( - tableResource.Properties.KeySchema[0].AttributeName, - ).toEqual('email'); - expect( - tableResource.Properties.KeySchema[0].KeyType, - ).toEqual('HASH'); - expect( - tableResource.Properties.AttributeDefinitions[0].AttributeType, - ).toEqual('S'); - const schema = parse(out.schema); - const createInput = schema.definitions.find((def: any) => def.name && def.name.value === 'CreateTestInput') as InputObjectTypeDefinitionNode; - const updateInput = schema.definitions.find((def: any) => def.name && def.name.value === 'UpdateTestInput') as InputObjectTypeDefinitionNode; - const deleteInput = schema.definitions.find((def: any) => def.name && def.name.value === 'DeleteTestInput') as InputObjectTypeDefinitionNode; - expect(createInput).toBeDefined(); - expectNonNullInputValues(createInput, ['email', 'nonNullListInput', 'nonNullListInputOfNonNullStrings']); - expectNullableInputValues(createInput, ['listInput']); - expectInputValueToHandle(createInput, (f: any) => { - if (f.name.value === 'nonNullListInputOfNonNullStrings') { - return f.type.kind === Kind.NON_NULL_TYPE && f.type.type.kind === Kind.LIST_TYPE && f.type.type.type.kind === Kind.NON_NULL_TYPE; - } else if (f.name.value === 'nonNullListInput') { - return f.type.kind === Kind.NON_NULL_TYPE && f.type.type.kind === Kind.LIST_TYPE; - } else if (f.name.value === 'listInput') { - return f.type.kind === Kind.LIST_TYPE; - } - return true; - }); - - expectNonNullInputValues(updateInput, ['email']); - expectNullableInputValues(updateInput, ['listInput', 'nonNullListInput', 'nonNullListInputOfNonNullStrings']); - expectInputValueToHandle(updateInput, (f: any) => { - if (f.name.value === 'nonNullListInputOfNonNullStrings') { - return f.type.kind === Kind.LIST_TYPE && f.type.type.kind === Kind.NON_NULL_TYPE; - } else if (f.name.value === 'nonNullListInput') { - return f.type.kind === Kind.LIST_TYPE; - } else if (f.name.value === 'listInput') { - return f.type.kind === Kind.LIST_TYPE; - } - return true; - }); - - expectNonNullInputValues(deleteInput, ['email']); -}) \ No newline at end of file + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer(), new KeyTransformer()], + }); + + const out = transformer.transform(validSchema); + let tableResource = out.stacks.Test.Resources.TestTable; + expect(tableResource).toBeDefined(); + expect(tableResource.Properties.KeySchema[0].AttributeName).toEqual('email'); + expect(tableResource.Properties.KeySchema[0].KeyType).toEqual('HASH'); + expect(tableResource.Properties.AttributeDefinitions[0].AttributeType).toEqual('S'); + const schema = parse(out.schema); + const createInput = schema.definitions.find( + (def: any) => def.name && def.name.value === 'CreateTestInput' + ) as InputObjectTypeDefinitionNode; + const updateInput = schema.definitions.find( + (def: any) => def.name && def.name.value === 'UpdateTestInput' + ) as InputObjectTypeDefinitionNode; + const deleteInput = schema.definitions.find( + (def: any) => def.name && def.name.value === 'DeleteTestInput' + ) as InputObjectTypeDefinitionNode; + expect(createInput).toBeDefined(); + expectNonNullInputValues(createInput, ['email', 'nonNullListInput', 'nonNullListInputOfNonNullStrings']); + expectNullableInputValues(createInput, ['listInput']); + expectInputValueToHandle(createInput, (f: any) => { + if (f.name.value === 'nonNullListInputOfNonNullStrings') { + return f.type.kind === Kind.NON_NULL_TYPE && f.type.type.kind === Kind.LIST_TYPE && f.type.type.type.kind === Kind.NON_NULL_TYPE; + } else if (f.name.value === 'nonNullListInput') { + return f.type.kind === Kind.NON_NULL_TYPE && f.type.type.kind === Kind.LIST_TYPE; + } else if (f.name.value === 'listInput') { + return f.type.kind === Kind.LIST_TYPE; + } + return true; + }); + + expectNonNullInputValues(updateInput, ['email']); + expectNullableInputValues(updateInput, ['listInput', 'nonNullListInput', 'nonNullListInputOfNonNullStrings']); + expectInputValueToHandle(updateInput, (f: any) => { + if (f.name.value === 'nonNullListInputOfNonNullStrings') { + return f.type.kind === Kind.LIST_TYPE && f.type.type.kind === Kind.NON_NULL_TYPE; + } else if (f.name.value === 'nonNullListInput') { + return f.type.kind === Kind.LIST_TYPE; + } else if (f.name.value === 'listInput') { + return f.type.kind === Kind.LIST_TYPE; + } + return true; + }); + + expectNonNullInputValues(deleteInput, ['email']); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts index b75e0d85fa..6342e779fe 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/KeyWithAuth.e2e.test.ts @@ -1,37 +1,41 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import KeyTransformer from 'graphql-key-transformer' -import * as fs from 'fs' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' -import * as S3 from 'aws-sdk/clients/s3' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import KeyTransformer from 'graphql-key-transformer'; +import * as fs from 'fs'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; +import * as S3 from 'aws-sdk/clients/s3'; +import { GraphQLClient } from '../GraphQLClient'; import { S3Client } from '../S3Client'; -import * as path from 'path' -import { deploy } from '../deployNestedStacks' +import * as path from 'path'; +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; import { - createUserPool, createUserPoolClient, deleteUserPool, - signupAndAuthenticateUser, createGroup, addUserToGroup, - configureAmplify - } from '../cognitoUtils'; + createUserPool, + createUserPoolClient, + deleteUserPool, + signupAndAuthenticateUser, + createGroup, + addUserToGroup, + configureAmplify, +} from '../cognitoUtils'; // to deal with bug in cognito-identity-js -(global as any).fetch = require("node-fetch"); +(global as any).fetch = require('node-fetch'); jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') +const cf = new CloudFormationClient('us-west-2'); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `KeyWithAuth-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-key-with-auth-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/key_auth_transform_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `KeyWithAuth-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-key-with-auth-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/key_auth_transform_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; let GRAPHQL_ENDPOINT = undefined; @@ -57,44 +61,44 @@ let GRAPHQL_CLIENT_3 = undefined; let USER_POOL_ID = undefined; -const USERNAME1 = 'user1@test.com' -const USERNAME2 = 'user2@test.com' -const USERNAME3 = 'user3@test.com' -const TMP_PASSWORD = 'Password123!' -const REAL_PASSWORD = 'Password1234!' +const USERNAME1 = 'user1@test.com'; +const USERNAME2 = 'user2@test.com'; +const USERNAME3 = 'user3@test.com'; +const TMP_PASSWORD = 'Password123!'; +const REAL_PASSWORD = 'Password1234!'; const ADMIN_GROUP_NAME = 'Admin'; const DEVS_GROUP_NAME = 'Devs'; const PARTICIPANT_GROUP_NAME = 'Participant'; const WATCHER_GROUP_NAME = 'Watcher'; -const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }) -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } function deleteDirectory(directory: string) { - const files = fs.readdirSync(directory) - for (const file of files) { - const contentPath = path.join(directory, file) - if (fs.lstatSync(contentPath).isDirectory()) { - deleteDirectory(contentPath) - fs.rmdirSync(contentPath) - } else { - fs.unlinkSync(contentPath) - } + const files = fs.readdirSync(directory); + for (const file of files) { + const contentPath = path.join(directory, file); + if (fs.lstatSync(contentPath).isDirectory()) { + deleteDirectory(contentPath); + fs.rmdirSync(contentPath); + } else { + fs.unlinkSync(contentPath); } + } } beforeAll(async () => { - // Create a stack for the post model with auth enabled. - const validSchema = ` + // Create a stack for the post model with auth enabled. + const validSchema = ` type Order @model @key(fields: ["customerEmail", "orderId"]) @@ -105,233 +109,253 @@ beforeAll(async () => { createdAt: String orderId: String! } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { - console.error(`Failed to create bucket: ${e}`) - } - const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); - USER_POOL_ID = userPoolResponse.UserPool.Id; - const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); - const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; - try { - // Clean the bucket - const out = transformer.transform(validSchema) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { AuthCognitoUserPoolId: USER_POOL_ID }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - const apiKey = getApiKey(finishedStack.Outputs) - console.log(`API KEY: ${apiKey}`); - expect(apiKey).not.toBeTruthy() - - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy() - expect(USER_POOL_ID).toBeTruthy() - expect(userPoolClientId).toBeTruthy() - - // Configure Amplify, create users, and sign in. - configureAmplify(USER_POOL_ID, userPoolClientId) - - const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - - await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME) - await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME) - await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME) - await createGroup(USER_POOL_ID, DEVS_GROUP_NAME) - await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID) - const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - - const idToken = authResAfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }) - - const accessToken = authResAfterGroup.getAccessToken().getJwtToken() - GRAPHQL_CLIENT_1_ACCESS = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: accessToken }) - - const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }) - - const idToken3 = authRes3.getIdToken().getJwtToken() - GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }) - - // Wait for any propagation to avoid random - // "The security token included in the request is invalid" errors - await new Promise((res) => setTimeout(() => res(), 5000)) - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new KeyTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.error(`Failed to create bucket: ${e}`); + } + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + try { + // Clean the bucket + const out = transformer.transform(validSchema); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { AuthCognitoUserPoolId: USER_POOL_ID }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + const apiKey = getApiKey(finishedStack.Outputs); + console.log(`API KEY: ${apiKey}`); + expect(apiKey).not.toBeTruthy(); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + expect(USER_POOL_ID).toBeTruthy(); + expect(userPoolClientId).toBeTruthy(); + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId); + + const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + + await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME); + await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME); + await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME); + await createGroup(USER_POOL_ID, DEVS_GROUP_NAME); + await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID); + const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + + const idToken = authResAfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }); + + const accessToken = authResAfterGroup.getAccessToken().getJwtToken(); + GRAPHQL_CLIENT_1_ACCESS = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: accessToken }); + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }); + + const idToken3 = authRes3.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }); + + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); - afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await deleteUserPool(cognitoClient, USER_POOL_ID) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await deleteUserPool(cognitoClient, USER_POOL_ID); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) - } -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Test queries below */ test('Test createOrder mutation as admin', async () => { - const response = await createOrder(GRAPHQL_CLIENT_1, USERNAME2, "order1"); - expect(response.data.createOrder.customerEmail).toBeDefined() - expect(response.data.createOrder.orderId).toEqual('order1') - expect(response.data.createOrder.createdAt).toBeDefined() -}) + const response = await createOrder(GRAPHQL_CLIENT_1, USERNAME2, 'order1'); + expect(response.data.createOrder.customerEmail).toBeDefined(); + expect(response.data.createOrder.orderId).toEqual('order1'); + expect(response.data.createOrder.createdAt).toBeDefined(); +}); test('Test createOrder mutation as owner', async () => { - const response = await createOrder(GRAPHQL_CLIENT_2, USERNAME2, "order2"); - expect(response.data.createOrder.customerEmail).toBeDefined() - expect(response.data.createOrder.orderId).toEqual('order2') - expect(response.data.createOrder.createdAt).toBeDefined() -}) + const response = await createOrder(GRAPHQL_CLIENT_2, USERNAME2, 'order2'); + expect(response.data.createOrder.customerEmail).toBeDefined(); + expect(response.data.createOrder.orderId).toEqual('order2'); + expect(response.data.createOrder.createdAt).toBeDefined(); +}); test('Test createOrder mutation as owner', async () => { - const response = await createOrder(GRAPHQL_CLIENT_3, USERNAME2, "order3"); - expect(response.data.createOrder).toBeNull(); - expect(response.errors).toHaveLength(1); -}) + const response = await createOrder(GRAPHQL_CLIENT_3, USERNAME2, 'order3'); + expect(response.data.createOrder).toBeNull(); + expect(response.errors).toHaveLength(1); +}); test('Test list orders as owner', async () => { - await createOrder(GRAPHQL_CLIENT_3, USERNAME3, "owned1") - await createOrder(GRAPHQL_CLIENT_3, USERNAME3, "owned2") - const listResponse = await listOrders(GRAPHQL_CLIENT_3, USERNAME3, { beginsWith: "owned" }) - expect(listResponse.data.listOrders.items).toHaveLength(2); -}) + await createOrder(GRAPHQL_CLIENT_3, USERNAME3, 'owned1'); + await createOrder(GRAPHQL_CLIENT_3, USERNAME3, 'owned2'); + const listResponse = await listOrders(GRAPHQL_CLIENT_3, USERNAME3, { beginsWith: 'owned' }); + expect(listResponse.data.listOrders.items).toHaveLength(2); +}); test('Test list orders as non owner', async () => { - await createOrder(GRAPHQL_CLIENT_3, USERNAME3, "unowned1") - await createOrder(GRAPHQL_CLIENT_3, USERNAME3, "unowned2") - const listResponse = await listOrders(GRAPHQL_CLIENT_2, USERNAME3, { beginsWith: "unowned" }) - expect(listResponse.data.listOrders.items).toHaveLength(0); -}) + await createOrder(GRAPHQL_CLIENT_3, USERNAME3, 'unowned1'); + await createOrder(GRAPHQL_CLIENT_3, USERNAME3, 'unowned2'); + const listResponse = await listOrders(GRAPHQL_CLIENT_2, USERNAME3, { beginsWith: 'unowned' }); + expect(listResponse.data.listOrders.items).toHaveLength(0); +}); test('Test get orders as owner', async () => { - await createOrder(GRAPHQL_CLIENT_2, USERNAME2, "myobj") - const getResponse = await getOrder(GRAPHQL_CLIENT_2, USERNAME2, "myobj") - expect(getResponse.data.getOrder.orderId).toEqual("myobj"); -}) + await createOrder(GRAPHQL_CLIENT_2, USERNAME2, 'myobj'); + const getResponse = await getOrder(GRAPHQL_CLIENT_2, USERNAME2, 'myobj'); + expect(getResponse.data.getOrder.orderId).toEqual('myobj'); +}); test('Test get orders as non-owner', async () => { - await createOrder(GRAPHQL_CLIENT_2, USERNAME2, "notmyobj") - const getResponse = await getOrder(GRAPHQL_CLIENT_3, USERNAME2, "notmyobj") - expect(getResponse.data.getOrder).toBeNull(); - expect(getResponse.errors).toHaveLength(1); -}) + await createOrder(GRAPHQL_CLIENT_2, USERNAME2, 'notmyobj'); + const getResponse = await getOrder(GRAPHQL_CLIENT_3, USERNAME2, 'notmyobj'); + expect(getResponse.data.getOrder).toBeNull(); + expect(getResponse.errors).toHaveLength(1); +}); test('Test query orders as owner', async () => { - await createOrder(GRAPHQL_CLIENT_3, USERNAME3, "ownedby3a") - const listResponse = await ordersByOrderId(GRAPHQL_CLIENT_3, "ownedby3a") - expect(listResponse.data.ordersByOrderId.items).toHaveLength(1); -}) + await createOrder(GRAPHQL_CLIENT_3, USERNAME3, 'ownedby3a'); + const listResponse = await ordersByOrderId(GRAPHQL_CLIENT_3, 'ownedby3a'); + expect(listResponse.data.ordersByOrderId.items).toHaveLength(1); +}); test('Test query orders as non owner', async () => { - await createOrder(GRAPHQL_CLIENT_3, USERNAME3, "notownedby2a") - const listResponse = await ordersByOrderId(GRAPHQL_CLIENT_2, "notownedby2a") - expect(listResponse.data.ordersByOrderId.items).toHaveLength(0); -}) + await createOrder(GRAPHQL_CLIENT_3, USERNAME3, 'notownedby2a'); + const listResponse = await ordersByOrderId(GRAPHQL_CLIENT_2, 'notownedby2a'); + expect(listResponse.data.ordersByOrderId.items).toHaveLength(0); +}); async function createOrder(client: GraphQLClient, customerEmail: string, orderId: string) { - const result = await client.query(`mutation CreateOrder($input: CreateOrderInput!) { + const result = await client.query( + `mutation CreateOrder($input: CreateOrderInput!) { createOrder(input: $input) { customerEmail orderId createdAt } - }`, { - input: { customerEmail, orderId } - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { customerEmail, orderId }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function updateOrder(client: GraphQLClient, customerEmail: string, orderId: string) { - const result = await client.query(`mutation UpdateOrder($input: UpdateOrderInput!) { + const result = await client.query( + `mutation UpdateOrder($input: UpdateOrderInput!) { updateOrder(input: $input) { customerEmail orderId createdAt } - }`, { - input: { customerEmail, orderId } - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { customerEmail, orderId }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function deleteOrder(client: GraphQLClient, customerEmail: string, orderId: string) { - const result = await client.query(`mutation DeleteOrder($input: DeleteOrderInput!) { + const result = await client.query( + `mutation DeleteOrder($input: DeleteOrderInput!) { deleteOrder(input: $input) { customerEmail orderId createdAt } - }`, { - input: { customerEmail, orderId } - }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { + input: { customerEmail, orderId }, + } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function getOrder(client: GraphQLClient, customerEmail: string, orderId: string) { - const result = await client.query(`query GetOrder($customerEmail: String!, $orderId: String!) { + const result = await client.query( + `query GetOrder($customerEmail: String!, $orderId: String!) { getOrder(customerEmail: $customerEmail, orderId: $orderId) { customerEmail orderId createdAt } - }`, { customerEmail, orderId }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { customerEmail, orderId } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function listOrders(client: GraphQLClient, customerEmail: string, orderId: { beginsWith: string }) { - const result = await client.query(`query ListOrder($customerEmail: String, $orderId: ModelStringKeyConditionInput) { + const result = await client.query( + `query ListOrder($customerEmail: String, $orderId: ModelStringKeyConditionInput) { listOrders(customerEmail: $customerEmail, orderId: $orderId) { items { customerEmail @@ -340,13 +364,16 @@ async function listOrders(client: GraphQLClient, customerEmail: string, orderId: } nextToken } - }`, { customerEmail, orderId }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { customerEmail, orderId } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } async function ordersByOrderId(client: GraphQLClient, orderId: string) { - const result = await client.query(`query OrdersByOrderId($orderId: String!) { + const result = await client.query( + `query OrdersByOrderId($orderId: String!) { ordersByOrderId(orderId: $orderId) { items { customerEmail @@ -355,7 +382,9 @@ async function ordersByOrderId(client: GraphQLClient, orderId: string) { } nextToken } - }`, { orderId }); - console.log(JSON.stringify(result, null, 4)); - return result; + }`, + { orderId } + ); + console.log(JSON.stringify(result, null, 4)); + return result; } diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts index 123aaebdb6..4df941190c 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelAuthTransformer.e2e.test.ts @@ -1,36 +1,40 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import * as fs from 'fs' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' -import * as S3 from 'aws-sdk/clients/s3' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import * as fs from 'fs'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; +import * as S3 from 'aws-sdk/clients/s3'; +import { GraphQLClient } from '../GraphQLClient'; import { S3Client } from '../S3Client'; -import * as path from 'path' -import { deploy } from '../deployNestedStacks' +import * as path from 'path'; +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; import { - createUserPool, createUserPoolClient, deleteUserPool, - signupAndAuthenticateUser, createGroup, addUserToGroup, - configureAmplify - } from '../cognitoUtils'; + createUserPool, + createUserPoolClient, + deleteUserPool, + signupAndAuthenticateUser, + createGroup, + addUserToGroup, + configureAmplify, +} from '../cognitoUtils'; // to deal with bug in cognito-identity-js -(global as any).fetch = require("node-fetch"); +(global as any).fetch = require('node-fetch'); jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') +const cf = new CloudFormationClient('us-west-2'); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `ModelAuthTransformerTest-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-auth-transformer-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/model_auth_transform_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `ModelAuthTransformerTest-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-auth-transformer-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/model_auth_transform_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; let GRAPHQL_ENDPOINT = undefined; @@ -56,44 +60,44 @@ let GRAPHQL_CLIENT_3 = undefined; let USER_POOL_ID = undefined; -const USERNAME1 = 'user1@test.com' -const USERNAME2 = 'user2@test.com' -const USERNAME3 = 'user3@test.com' -const TMP_PASSWORD = 'Password123!' -const REAL_PASSWORD = 'Password1234!' +const USERNAME1 = 'user1@test.com'; +const USERNAME2 = 'user2@test.com'; +const USERNAME3 = 'user3@test.com'; +const TMP_PASSWORD = 'Password123!'; +const REAL_PASSWORD = 'Password1234!'; const ADMIN_GROUP_NAME = 'Admin'; const DEVS_GROUP_NAME = 'Devs'; const PARTICIPANT_GROUP_NAME = 'Participant'; const WATCHER_GROUP_NAME = 'Watcher'; -const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }) -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } function deleteDirectory(directory: string) { - const files = fs.readdirSync(directory) - for (const file of files) { - const contentPath = path.join(directory, file) - if (fs.lstatSync(contentPath).isDirectory()) { - deleteDirectory(contentPath) - fs.rmdirSync(contentPath) - } else { - fs.unlinkSync(contentPath) - } - } + const files = fs.readdirSync(directory); + for (const file of files) { + const contentPath = path.join(directory, file); + if (fs.lstatSync(contentPath).isDirectory()) { + deleteDirectory(contentPath); + fs.rmdirSync(contentPath); + } else { + fs.unlinkSync(contentPath); + } + } } beforeAll(async () => { - // Create a stack for the post model with auth enabled. - const validSchema = ` + // Create a stack for the post model with auth enabled. + const validSchema = ` type Post @model @auth(rules: [{ allow: owner }]) { id: ID! title: String! @@ -174,119 +178,127 @@ beforeAll(async () => { content: String owner: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { - console.error(`Failed to create bucket: ${e}`) - } - const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); - USER_POOL_ID = userPoolResponse.UserPool.Id; - const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); - const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; - try { - // Clean the bucket - const out = transformer.transform(validSchema) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { AuthCognitoUserPoolId: USER_POOL_ID }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - const apiKey = getApiKey(finishedStack.Outputs) - console.log(`API KEY: ${apiKey}`); - expect(apiKey).not.toBeTruthy() - - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy() - expect(USER_POOL_ID).toBeTruthy() - expect(userPoolClientId).toBeTruthy() - - // Configure Amplify, create users, and sign in. - configureAmplify(USER_POOL_ID, userPoolClientId) - - const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - - await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME) - await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME) - await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME) - await createGroup(USER_POOL_ID, DEVS_GROUP_NAME) - await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID) - const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - - const idToken = authResAfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }) - - const accessToken = authResAfterGroup.getAccessToken().getJwtToken() - GRAPHQL_CLIENT_1_ACCESS = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: accessToken }) - - const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }) - - const idToken3 = authRes3.getIdToken().getJwtToken() - GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }) - - // Wait for any propagation to avoid random - // "The security token included in the request is invalid" errors - await new Promise((res) => setTimeout(() => res(), 5000)) - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.error(`Failed to create bucket: ${e}`); + } + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + try { + // Clean the bucket + const out = transformer.transform(validSchema); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { AuthCognitoUserPoolId: USER_POOL_ID }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + const apiKey = getApiKey(finishedStack.Outputs); + console.log(`API KEY: ${apiKey}`); + expect(apiKey).not.toBeTruthy(); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + expect(USER_POOL_ID).toBeTruthy(); + expect(userPoolClientId).toBeTruthy(); + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId); + + const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + + await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME); + await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME); + await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME); + await createGroup(USER_POOL_ID, DEVS_GROUP_NAME); + await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID); + const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + + const idToken = authResAfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }); + + const accessToken = authResAfterGroup.getAccessToken().getJwtToken(); + GRAPHQL_CLIENT_1_ACCESS = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: accessToken }); + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }); + + const idToken3 = authRes3.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }); + + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); - afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await deleteUserPool(cognitoClient, USER_POOL_ID) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) - } -}) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await deleteUserPool(cognitoClient, USER_POOL_ID); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); + } + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Test queries below */ test('Test createPost mutation', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -294,15 +306,18 @@ test('Test createPost mutation', async () => { updatedAt owner } - }`, {}) - console.log(response); - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.owner).toEqual(USERNAME1) + }`, + {} + ); + console.log(response); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.owner).toEqual(USERNAME1); - const response2 = await GRAPHQL_CLIENT_1_ACCESS.query(`mutation { + const response2 = await GRAPHQL_CLIENT_1_ACCESS.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -310,17 +325,20 @@ test('Test createPost mutation', async () => { updatedAt owner } - }`, {}) - console.log(response2); - expect(response2.data.createPost.id).toBeDefined() - expect(response2.data.createPost.title).toEqual('Hello, World!') - expect(response2.data.createPost.createdAt).toBeDefined() - expect(response2.data.createPost.updatedAt).toBeDefined() - expect(response2.data.createPost.owner).toEqual(USERNAME1) -}) + }`, + {} + ); + console.log(response2); + expect(response2.data.createPost.id).toBeDefined(); + expect(response2.data.createPost.title).toEqual('Hello, World!'); + expect(response2.data.createPost.createdAt).toBeDefined(); + expect(response2.data.createPost.updatedAt).toBeDefined(); + expect(response2.data.createPost.owner).toEqual(USERNAME1); +}); test('Test getPost query when authorized', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -328,13 +346,16 @@ test('Test getPost query when authorized', async () => { updatedAt owner } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.owner).toEqual(USERNAME1) - const getResponse = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.owner).toEqual(USERNAME1); + const getResponse = await GRAPHQL_CLIENT_1.query( + `query { getPost(id: "${response.data.createPost.id}") { id title @@ -342,14 +363,17 @@ test('Test getPost query when authorized', async () => { updatedAt owner } - }`, {}) - expect(getResponse.data.getPost.id).toBeDefined() - expect(getResponse.data.getPost.title).toEqual('Hello, World!') - expect(getResponse.data.getPost.createdAt).toBeDefined() - expect(getResponse.data.getPost.updatedAt).toBeDefined() - expect(getResponse.data.getPost.owner).toEqual(USERNAME1) + }`, + {} + ); + expect(getResponse.data.getPost.id).toBeDefined(); + expect(getResponse.data.getPost.title).toEqual('Hello, World!'); + expect(getResponse.data.getPost.createdAt).toBeDefined(); + expect(getResponse.data.getPost.updatedAt).toBeDefined(); + expect(getResponse.data.getPost.owner).toEqual(USERNAME1); - const getResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query(`query { + const getResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query( + `query { getPost(id: "${response.data.createPost.id}") { id title @@ -357,16 +381,19 @@ test('Test getPost query when authorized', async () => { updatedAt owner } - }`, {}) - expect(getResponseAccess.data.getPost.id).toBeDefined() - expect(getResponseAccess.data.getPost.title).toEqual('Hello, World!') - expect(getResponseAccess.data.getPost.createdAt).toBeDefined() - expect(getResponseAccess.data.getPost.updatedAt).toBeDefined() - expect(getResponseAccess.data.getPost.owner).toEqual(USERNAME1) -}) + }`, + {} + ); + expect(getResponseAccess.data.getPost.id).toBeDefined(); + expect(getResponseAccess.data.getPost.title).toEqual('Hello, World!'); + expect(getResponseAccess.data.getPost.createdAt).toBeDefined(); + expect(getResponseAccess.data.getPost.updatedAt).toBeDefined(); + expect(getResponseAccess.data.getPost.owner).toEqual(USERNAME1); +}); test('Test getPost query when not authorized', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -374,13 +401,16 @@ test('Test getPost query when not authorized', async () => { updatedAt owner } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.owner).toBeDefined() - const getResponse = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.owner).toBeDefined(); + const getResponse = await GRAPHQL_CLIENT_2.query( + `query { getPost(id: "${response.data.createPost.id}") { id title @@ -388,14 +418,17 @@ test('Test getPost query when not authorized', async () => { updatedAt owner } - }`, {}) - expect(getResponse.data.getPost).toEqual(null) - expect(getResponse.errors.length).toEqual(1) - expect((getResponse.errors[0] as any).errorType).toEqual('Unauthorized') -}) + }`, + {} + ); + expect(getResponse.data.getPost).toEqual(null); + expect(getResponse.errors.length).toEqual(1); + expect((getResponse.errors[0] as any).errorType).toEqual('Unauthorized'); +}); test('Test updatePost mutation when authorized', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -403,13 +436,16 @@ test('Test updatePost mutation when authorized', async () => { updatedAt owner } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.owner).toEqual(USERNAME1) - const updateResponse = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.owner).toEqual(USERNAME1); + const updateResponse = await GRAPHQL_CLIENT_1.query( + `mutation { updatePost(input: { id: "${response.data.createPost.id}", title: "Bye, World!" }) { id title @@ -417,12 +453,15 @@ test('Test updatePost mutation when authorized', async () => { updatedAt owner } - }`, {}) - expect(updateResponse.data.updatePost.id).toEqual(response.data.createPost.id) - expect(updateResponse.data.updatePost.title).toEqual('Bye, World!') - expect(updateResponse.data.updatePost.updatedAt > response.data.createPost.updatedAt).toEqual(true) + }`, + {} + ); + expect(updateResponse.data.updatePost.id).toEqual(response.data.createPost.id); + expect(updateResponse.data.updatePost.title).toEqual('Bye, World!'); + expect(updateResponse.data.updatePost.updatedAt > response.data.createPost.updatedAt).toEqual(true); - const updateResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query(`mutation { + const updateResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query( + `mutation { updatePost(input: { id: "${response.data.createPost.id}", title: "Bye, World!" }) { id title @@ -430,14 +469,17 @@ test('Test updatePost mutation when authorized', async () => { updatedAt owner } - }`, {}) - expect(updateResponseAccess.data.updatePost.id).toEqual(response.data.createPost.id) - expect(updateResponseAccess.data.updatePost.title).toEqual('Bye, World!') - expect(updateResponseAccess.data.updatePost.updatedAt > response.data.createPost.updatedAt).toEqual(true) -}) + }`, + {} + ); + expect(updateResponseAccess.data.updatePost.id).toEqual(response.data.createPost.id); + expect(updateResponseAccess.data.updatePost.title).toEqual('Bye, World!'); + expect(updateResponseAccess.data.updatePost.updatedAt > response.data.createPost.updatedAt).toEqual(true); +}); test('Test updatePost mutation when not authorized', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -445,13 +487,16 @@ test('Test updatePost mutation when not authorized', async () => { updatedAt owner } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.owner).toBeDefined() - const updateResponse = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.owner).toBeDefined(); + const updateResponse = await GRAPHQL_CLIENT_2.query( + `mutation { updatePost(input: { id: "${response.data.createPost.id}", title: "Bye, World!" }) { id title @@ -459,14 +504,17 @@ test('Test updatePost mutation when not authorized', async () => { updatedAt owner } - }`, {}) - expect(updateResponse.data.updatePost).toEqual(null) - expect(updateResponse.errors.length).toEqual(1) - expect((updateResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException') -}) + }`, + {} + ); + expect(updateResponse.data.updatePost).toEqual(null); + expect(updateResponse.errors.length).toEqual(1); + expect((updateResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); +}); test('Test deletePost mutation when authorized', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -474,20 +522,26 @@ test('Test deletePost mutation when authorized', async () => { updatedAt owner } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.owner).toEqual(USERNAME1) - const deleteResponse = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.owner).toEqual(USERNAME1); + const deleteResponse = await GRAPHQL_CLIENT_1.query( + `mutation { deletePost(input: { id: "${response.data.createPost.id}" }) { id } - }`, {}) - expect(deleteResponse.data.deletePost.id).toEqual(response.data.createPost.id) + }`, + {} + ); + expect(deleteResponse.data.deletePost.id).toEqual(response.data.createPost.id); - const responseAccess = await GRAPHQL_CLIENT_1_ACCESS.query(`mutation { + const responseAccess = await GRAPHQL_CLIENT_1_ACCESS.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -495,22 +549,28 @@ test('Test deletePost mutation when authorized', async () => { updatedAt owner } - }`, {}) - expect(responseAccess.data.createPost.id).toBeDefined() - expect(responseAccess.data.createPost.title).toEqual('Hello, World!') - expect(responseAccess.data.createPost.createdAt).toBeDefined() - expect(responseAccess.data.createPost.updatedAt).toBeDefined() - expect(responseAccess.data.createPost.owner).toEqual(USERNAME1) - const deleteResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query(`mutation { + }`, + {} + ); + expect(responseAccess.data.createPost.id).toBeDefined(); + expect(responseAccess.data.createPost.title).toEqual('Hello, World!'); + expect(responseAccess.data.createPost.createdAt).toBeDefined(); + expect(responseAccess.data.createPost.updatedAt).toBeDefined(); + expect(responseAccess.data.createPost.owner).toEqual(USERNAME1); + const deleteResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query( + `mutation { deletePost(input: { id: "${responseAccess.data.createPost.id}" }) { id } - }`, {}) - expect(deleteResponseAccess.data.deletePost.id).toEqual(responseAccess.data.createPost.id) -}) + }`, + {} + ); + expect(deleteResponseAccess.data.deletePost.id).toEqual(responseAccess.data.createPost.id); +}); test('Test deletePost mutation when not authorized', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -518,24 +578,30 @@ test('Test deletePost mutation when not authorized', async () => { updatedAt owner } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.owner).toEqual(USERNAME1) - const deleteResponse = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.owner).toEqual(USERNAME1); + const deleteResponse = await GRAPHQL_CLIENT_2.query( + `mutation { deletePost(input: { id: "${response.data.createPost.id}" }) { id } - }`, {}) - expect(deleteResponse.data.deletePost).toEqual(null) - expect(deleteResponse.errors.length).toEqual(1) - expect((deleteResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException') -}) + }`, + {} + ); + expect(deleteResponse.data.deletePost).toEqual(null); + expect(deleteResponse.errors.length).toEqual(1); + expect((deleteResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); +}); test('Test listPosts query when authorized', async () => { - const firstPost = await GRAPHQL_CLIENT_1.query(`mutation { + const firstPost = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "testing list" }) { id title @@ -543,13 +609,16 @@ test('Test listPosts query when authorized', async () => { updatedAt owner } - }`, {}) - expect(firstPost.data.createPost.id).toBeDefined() - expect(firstPost.data.createPost.title).toEqual('testing list') - expect(firstPost.data.createPost.createdAt).toBeDefined() - expect(firstPost.data.createPost.updatedAt).toBeDefined() - expect(firstPost.data.createPost.owner).toEqual(USERNAME1) - const secondPost = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + expect(firstPost.data.createPost.id).toBeDefined(); + expect(firstPost.data.createPost.title).toEqual('testing list'); + expect(firstPost.data.createPost.createdAt).toBeDefined(); + expect(firstPost.data.createPost.updatedAt).toBeDefined(); + expect(firstPost.data.createPost.owner).toEqual(USERNAME1); + const secondPost = await GRAPHQL_CLIENT_2.query( + `mutation { createPost(input: { title: "testing list" }) { id title @@ -557,254 +626,262 @@ test('Test listPosts query when authorized', async () => { updatedAt owner } - }`, {}) - // There are two posts but only 1 created by me. - const listResponse = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + // There are two posts but only 1 created by me. + const listResponse = await GRAPHQL_CLIENT_1.query( + `query { listPosts(filter: { title: { eq: "testing list" } }, limit: 25) { items { id } } - }`, {}) - console.log(JSON.stringify(listResponse, null, 4)) - expect(listResponse.data.listPosts.items.length).toEqual(1) + }`, + {} + ); + console.log(JSON.stringify(listResponse, null, 4)); + expect(listResponse.data.listPosts.items.length).toEqual(1); - const listResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query(`query { + const listResponseAccess = await GRAPHQL_CLIENT_1_ACCESS.query( + `query { listPosts(filter: { title: { eq: "testing list" } }, limit: 25) { items { id } } - }`, {}) - console.log(JSON.stringify(listResponseAccess, null, 4)) - expect(listResponseAccess.data.listPosts.items.length).toEqual(1) -}) + }`, + {} + ); + console.log(JSON.stringify(listResponseAccess, null, 4)); + expect(listResponseAccess.data.listPosts.items.length).toEqual(1); +}); /** * Static Group Auth */ test(`Test createSalary w/ Admin group protection authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 10 }) { id wage } } - `) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(10) -}) + `); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(10); +}); test(`Test update my own salary without admin permission`, async () => { - const req = await GRAPHQL_CLIENT_2.query(` + const req = await GRAPHQL_CLIENT_2.query(` mutation { createSalary(input: { wage: 10 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.wage).toEqual(10) - const req2 = await GRAPHQL_CLIENT_2.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.wage).toEqual(10); + const req2 = await GRAPHQL_CLIENT_2.query(` mutation { updateSalary(input: { id: "${req.data.createSalary.id}", wage: 14 }) { id wage } } - `) - console.log(JSON.stringify(req2, null, 4)) - expect(req2.data.updateSalary.wage).toEqual(14) -}) + `); + console.log(JSON.stringify(req2, null, 4)); + expect(req2.data.updateSalary.wage).toEqual(14); +}); test(`Test updating someone else's salary as an admin`, async () => { - const req = await GRAPHQL_CLIENT_2.query(` + const req = await GRAPHQL_CLIENT_2.query(` mutation { createSalary(input: { wage: 11 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(11) - const req2 = await GRAPHQL_CLIENT_1.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(11); + const req2 = await GRAPHQL_CLIENT_1.query(` mutation { updateSalary(input: { id: "${req.data.createSalary.id}", wage: 12 }) { id wage } } - `) - console.log(JSON.stringify(req2, null, 4)) - expect(req2.data.updateSalary.id).toEqual(req.data.createSalary.id) - expect(req2.data.updateSalary.wage).toEqual(12) -}) + `); + console.log(JSON.stringify(req2, null, 4)); + expect(req2.data.updateSalary.id).toEqual(req.data.createSalary.id); + expect(req2.data.updateSalary.wage).toEqual(12); +}); test(`Test updating someone else's salary when I am not admin.`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 13 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(13) - const req2 = await GRAPHQL_CLIENT_2.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(13); + const req2 = await GRAPHQL_CLIENT_2.query(` mutation { updateSalary(input: { id: "${req.data.createSalary.id}", wage: 14 }) { id wage } } - `) - expect(req2.data.updateSalary).toEqual(null) - expect(req2.errors.length).toEqual(1) - expect((req2.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException') -}) + `); + expect(req2.data.updateSalary).toEqual(null); + expect(req2.errors.length).toEqual(1); + expect((req2.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); +}); test(`Test deleteSalary w/ Admin group protection authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 15 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(15) - const req2 = await GRAPHQL_CLIENT_1.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(15); + const req2 = await GRAPHQL_CLIENT_1.query(` mutation { deleteSalary(input: { id: "${req.data.createSalary.id}" }) { id wage } } - `) - console.log(JSON.stringify(req2, null, 4)) - expect(req2.data.deleteSalary.id).toEqual(req.data.createSalary.id) - expect(req2.data.deleteSalary.wage).toEqual(15) -}) + `); + console.log(JSON.stringify(req2, null, 4)); + expect(req2.data.deleteSalary.id).toEqual(req.data.createSalary.id); + expect(req2.data.deleteSalary.wage).toEqual(15); +}); test(`Test deleteSalary w/ Admin group protection not authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 16 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(16) - const req2 = await GRAPHQL_CLIENT_2.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(16); + const req2 = await GRAPHQL_CLIENT_2.query(` mutation { deleteSalary(input: { id: "${req.data.createSalary.id}" }) { id wage } } - `) - expect(req2.data.deleteSalary).toEqual(null) - expect(req2.errors.length).toEqual(1) - expect((req2.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException') -}) + `); + expect(req2.data.deleteSalary).toEqual(null); + expect(req2.errors.length).toEqual(1); + expect((req2.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); +}); test(`Test and Admin can get a salary created by any user`, async () => { - const req = await GRAPHQL_CLIENT_2.query(` + const req = await GRAPHQL_CLIENT_2.query(` mutation { createSalary(input: { wage: 15 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(15) - const req2 = await GRAPHQL_CLIENT_1.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(15); + const req2 = await GRAPHQL_CLIENT_1.query(` query { getSalary(id: "${req.data.createSalary.id}") { id wage } } - `) - expect(req2.data.getSalary.id).toEqual(req.data.createSalary.id) - expect(req2.data.getSalary.wage).toEqual(15) -}) + `); + expect(req2.data.getSalary.id).toEqual(req.data.createSalary.id); + expect(req2.data.getSalary.wage).toEqual(15); +}); test(`Test owner can create and get a salary when not admin`, async () => { - const req = await GRAPHQL_CLIENT_2.query(` + const req = await GRAPHQL_CLIENT_2.query(` mutation { createSalary(input: { wage: 15 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(15) - const req2 = await GRAPHQL_CLIENT_2.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(15); + const req2 = await GRAPHQL_CLIENT_2.query(` query { getSalary(id: "${req.data.createSalary.id}") { id wage } } - `) - expect(req2.data.getSalary.id).toEqual(req.data.createSalary.id) - expect(req2.data.getSalary.wage).toEqual(15) -}) + `); + expect(req2.data.getSalary.id).toEqual(req.data.createSalary.id); + expect(req2.data.getSalary.wage).toEqual(15); +}); test(`Test getSalary w/ Admin group protection not authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 16 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(16) - const req2 = await GRAPHQL_CLIENT_2.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(16); + const req2 = await GRAPHQL_CLIENT_2.query(` query { getSalary(id: "${req.data.createSalary.id}") { id wage } } - `) - expect(req2.data.getSalary).toEqual(null) - expect(req2.errors.length).toEqual(1) - expect((req2.errors[0] as any).errorType).toEqual('Unauthorized') -}) + `); + expect(req2.data.getSalary).toEqual(null); + expect(req2.errors.length).toEqual(1); + expect((req2.errors[0] as any).errorType).toEqual('Unauthorized'); +}); test(`Test listSalarys w/ Admin group protection authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 101 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(101) - const req2 = await GRAPHQL_CLIENT_1.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(101); + const req2 = await GRAPHQL_CLIENT_1.query(` query { listSalarys(filter: { wage: { eq: 101 }}) { items { @@ -813,25 +890,25 @@ test(`Test listSalarys w/ Admin group protection authorized`, async () => { } } } - `) - expect(req2.data.listSalarys.items.length).toEqual(1) - expect(req2.data.listSalarys.items[0].id).toEqual(req.data.createSalary.id) - expect(req2.data.listSalarys.items[0].wage).toEqual(101) -}) + `); + expect(req2.data.listSalarys.items.length).toEqual(1); + expect(req2.data.listSalarys.items[0].id).toEqual(req.data.createSalary.id); + expect(req2.data.listSalarys.items[0].wage).toEqual(101); +}); test(`Test listSalarys w/ Admin group protection not authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSalary(input: { wage: 102 }) { id wage } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSalary.id).toBeDefined() - expect(req.data.createSalary.wage).toEqual(102) - const req2 = await GRAPHQL_CLIENT_2.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSalary.id).toBeDefined(); + expect(req.data.createSalary.wage).toEqual(102); + const req2 = await GRAPHQL_CLIENT_2.query(` query { listSalarys(filter: { wage: { eq: 102 }}) { items { @@ -840,15 +917,15 @@ test(`Test listSalarys w/ Admin group protection not authorized`, async () => { } } } - `) - expect(req2.data.listSalarys.items).toEqual([]) -}) + `); + expect(req2.data.listSalarys.items).toEqual([]); +}); /** * Dynamic Group Auth */ test(`Test createManyGroupProtected w/ dynamic group protection authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createManyGroupProtected(input: { value: 10, groups: ["Admin"] }) { id @@ -856,15 +933,15 @@ test(`Test createManyGroupProtected w/ dynamic group protection authorized`, asy groups } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createManyGroupProtected.id).toBeDefined() - expect(req.data.createManyGroupProtected.value).toEqual(10) - expect(req.data.createManyGroupProtected.groups).toEqual(["Admin"]) -}) + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createManyGroupProtected.id).toBeDefined(); + expect(req.data.createManyGroupProtected.value).toEqual(10); + expect(req.data.createManyGroupProtected.groups).toEqual(['Admin']); +}); test(`Test createManyGroupProtected w/ dynamic group protection when not authorized`, async () => { - const req = await GRAPHQL_CLIENT_2.query(` + const req = await GRAPHQL_CLIENT_2.query(` mutation { createManyGroupProtected(input: { value: 10, groups: ["Admin"] }) { id @@ -872,15 +949,15 @@ test(`Test createManyGroupProtected w/ dynamic group protection when not authori groups } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createManyGroupProtected).toEqual(null) - expect(req.errors.length).toEqual(1) - expect((req.errors[0] as any).errorType).toEqual('Unauthorized') -}) + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createManyGroupProtected).toEqual(null); + expect(req.errors.length).toEqual(1); + expect((req.errors[0] as any).errorType).toEqual('Unauthorized'); +}); test(`Test createSingleGroupProtected w/ dynamic group protection authorized`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createSingleGroupProtected(input: { value: 10, group: "Admin" }) { id @@ -888,15 +965,15 @@ test(`Test createSingleGroupProtected w/ dynamic group protection authorized`, a group } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSingleGroupProtected.id).toBeDefined() - expect(req.data.createSingleGroupProtected.value).toEqual(10) - expect(req.data.createSingleGroupProtected.group).toEqual("Admin") -}) + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSingleGroupProtected.id).toBeDefined(); + expect(req.data.createSingleGroupProtected.value).toEqual(10); + expect(req.data.createSingleGroupProtected.group).toEqual('Admin'); +}); test(`Test createSingleGroupProtected w/ dynamic group protection when not authorized`, async () => { - const req = await GRAPHQL_CLIENT_2.query(` + const req = await GRAPHQL_CLIENT_2.query(` mutation { createSingleGroupProtected(input: { value: 10, group: "Admin" }) { id @@ -904,15 +981,15 @@ test(`Test createSingleGroupProtected w/ dynamic group protection when not autho group } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createSingleGroupProtected).toEqual(null) - expect(req.errors.length).toEqual(1) - expect((req.errors[0] as any).errorType).toEqual('Unauthorized') -}) + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createSingleGroupProtected).toEqual(null); + expect(req.errors.length).toEqual(1); + expect((req.errors[0] as any).errorType).toEqual('Unauthorized'); +}); test(`Test listPWProtecteds when the user is authorized.`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createPWProtected(input: { content: "Foobie", participants: "${PARTICIPANT_GROUP_NAME}", watchers: "${WATCHER_GROUP_NAME}" }) { id @@ -921,11 +998,11 @@ test(`Test listPWProtecteds when the user is authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createPWProtected).toBeTruthy() + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createPWProtected).toBeTruthy(); - const uReq = await GRAPHQL_CLIENT_1.query(` + const uReq = await GRAPHQL_CLIENT_1.query(` mutation { updatePWProtected(input: { id: "${req.data.createPWProtected.id}", content: "Foobie2" }) { id @@ -934,11 +1011,11 @@ test(`Test listPWProtecteds when the user is authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(uReq, null, 4)) - expect(uReq.data.updatePWProtected).toBeTruthy() + `); + console.log(JSON.stringify(uReq, null, 4)); + expect(uReq.data.updatePWProtected).toBeTruthy(); - const req2 = await GRAPHQL_CLIENT_1.query(` + const req2 = await GRAPHQL_CLIENT_1.query(` query { listPWProtecteds { items { @@ -950,13 +1027,12 @@ test(`Test listPWProtecteds when the user is authorized.`, async () => { nextToken } } - `) - expect(req2.data.listPWProtecteds.items.length).toEqual(1) - expect(req2.data.listPWProtecteds.items[0].id).toEqual(req.data.createPWProtected.id) - expect(req2.data.listPWProtecteds.items[0].content).toEqual("Foobie2") - + `); + expect(req2.data.listPWProtecteds.items.length).toEqual(1); + expect(req2.data.listPWProtecteds.items[0].id).toEqual(req.data.createPWProtected.id); + expect(req2.data.listPWProtecteds.items[0].content).toEqual('Foobie2'); - const req3 = await GRAPHQL_CLIENT_1.query(` + const req3 = await GRAPHQL_CLIENT_1.query(` query { getPWProtected(id: "${req.data.createPWProtected.id}") { id @@ -965,11 +1041,11 @@ test(`Test listPWProtecteds when the user is authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(req3, null, 4)) - expect(req3.data.getPWProtected).toBeTruthy() + `); + console.log(JSON.stringify(req3, null, 4)); + expect(req3.data.getPWProtected).toBeTruthy(); - const dReq = await GRAPHQL_CLIENT_1.query(` + const dReq = await GRAPHQL_CLIENT_1.query(` mutation { deletePWProtected(input: { id: "${req.data.createPWProtected.id}" }) { id @@ -978,13 +1054,13 @@ test(`Test listPWProtecteds when the user is authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(dReq, null, 4)) - expect(dReq.data.deletePWProtected).toBeTruthy() -}) + `); + console.log(JSON.stringify(dReq, null, 4)); + expect(dReq.data.deletePWProtected).toBeTruthy(); +}); test(`Test listPWProtecteds when groups is null in dynamodb.`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createPWProtected(input: { content: "Foobie" }) { id @@ -993,11 +1069,11 @@ test(`Test listPWProtecteds when groups is null in dynamodb.`, async () => { watchers } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createPWProtected).toBeTruthy() + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createPWProtected).toBeTruthy(); - const req2 = await GRAPHQL_CLIENT_1.query(` + const req2 = await GRAPHQL_CLIENT_1.query(` query { listPWProtecteds { items { @@ -1009,10 +1085,10 @@ test(`Test listPWProtecteds when groups is null in dynamodb.`, async () => { nextToken } } - `) - expect(req2.data.listPWProtecteds.items.length).toEqual(0) + `); + expect(req2.data.listPWProtecteds.items.length).toEqual(0); - const req3 = await GRAPHQL_CLIENT_1.query(` + const req3 = await GRAPHQL_CLIENT_1.query(` query { getPWProtected(id: "${req.data.createPWProtected.id}") { id @@ -1021,15 +1097,15 @@ test(`Test listPWProtecteds when groups is null in dynamodb.`, async () => { watchers } } - `) - console.log(JSON.stringify(req3, null, 4)) - expect(req3.data.getPWProtected).toEqual(null) - expect(req3.errors.length).toEqual(1) - expect((req3.errors[0] as any).errorType).toEqual('Unauthorized') -}) + `); + console.log(JSON.stringify(req3, null, 4)); + expect(req3.data.getPWProtected).toEqual(null); + expect(req3.errors.length).toEqual(1); + expect((req3.errors[0] as any).errorType).toEqual('Unauthorized'); +}); test(`Test Protecteds when the user is not authorized.`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createPWProtected(input: { content: "Barbie", participants: "${PARTICIPANT_GROUP_NAME}", watchers: "${WATCHER_GROUP_NAME}" }) { id @@ -1038,11 +1114,11 @@ test(`Test Protecteds when the user is not authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createPWProtected).toBeTruthy() + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createPWProtected).toBeTruthy(); - const req2 = await GRAPHQL_CLIENT_2.query(` + const req2 = await GRAPHQL_CLIENT_2.query(` query { listPWProtecteds { items { @@ -1054,12 +1130,12 @@ test(`Test Protecteds when the user is not authorized.`, async () => { nextToken } } - `) - console.log(JSON.stringify(req2, null, 4)) - expect(req2.data.listPWProtecteds.items.length).toEqual(0) - expect(req2.data.listPWProtecteds.nextToken).toBeNull() + `); + console.log(JSON.stringify(req2, null, 4)); + expect(req2.data.listPWProtecteds.items.length).toEqual(0); + expect(req2.data.listPWProtecteds.nextToken).toBeNull(); - const uReq = await GRAPHQL_CLIENT_2.query(` + const uReq = await GRAPHQL_CLIENT_2.query(` mutation { updatePWProtected(input: { id: "${req.data.createPWProtected.id}", content: "Foobie2" }) { id @@ -1068,11 +1144,11 @@ test(`Test Protecteds when the user is not authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(uReq, null, 4)) - expect(uReq.data.updatePWProtected).toBeNull() + `); + console.log(JSON.stringify(uReq, null, 4)); + expect(uReq.data.updatePWProtected).toBeNull(); - const req3 = await GRAPHQL_CLIENT_2.query(` + const req3 = await GRAPHQL_CLIENT_2.query(` query { getPWProtected(id: "${req.data.createPWProtected.id}") { id @@ -1081,11 +1157,11 @@ test(`Test Protecteds when the user is not authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(req3, null, 4)) - expect(req3.data.getPWProtected).toBeNull() + `); + console.log(JSON.stringify(req3, null, 4)); + expect(req3.data.getPWProtected).toBeNull(); - const dReq = await GRAPHQL_CLIENT_2.query(` + const dReq = await GRAPHQL_CLIENT_2.query(` mutation { deletePWProtected(input: { id: "${req.data.createPWProtected.id}" }) { id @@ -1094,12 +1170,12 @@ test(`Test Protecteds when the user is not authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(dReq, null, 4)) - expect(dReq.data.deletePWProtected).toBeNull() + `); + console.log(JSON.stringify(dReq, null, 4)); + expect(dReq.data.deletePWProtected).toBeNull(); - // The record should still exist after delete. - const getReq = await GRAPHQL_CLIENT_1.query(` + // The record should still exist after delete. + const getReq = await GRAPHQL_CLIENT_1.query(` query { getPWProtected(id: "${req.data.createPWProtected.id}") { id @@ -1108,104 +1184,103 @@ test(`Test Protecteds when the user is not authorized.`, async () => { watchers } } - `) - console.log(JSON.stringify(getReq, null, 4)) - expect(getReq.data.getPWProtected).toBeTruthy() -}) + `); + console.log(JSON.stringify(getReq, null, 4)); + expect(getReq.data.getPWProtected).toBeTruthy(); +}); test(`Test creating, updating, and deleting an admin note as an admin`, async () => { - const req = await GRAPHQL_CLIENT_1.query(` + const req = await GRAPHQL_CLIENT_1.query(` mutation { createAdminNote(input: { content: "Hello" }) { id content } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.data.createAdminNote.id).toBeDefined() - expect(req.data.createAdminNote.content).toEqual("Hello") - const req2 = await GRAPHQL_CLIENT_1.query(` + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.data.createAdminNote.id).toBeDefined(); + expect(req.data.createAdminNote.content).toEqual('Hello'); + const req2 = await GRAPHQL_CLIENT_1.query(` mutation { updateAdminNote(input: { id: "${req.data.createAdminNote.id}", content: "Hello 2" }) { id content } } - `) - console.log(JSON.stringify(req2, null, 4)) - expect(req2.data.updateAdminNote.id).toEqual(req.data.createAdminNote.id) - expect(req2.data.updateAdminNote.content).toEqual("Hello 2") - const req3 = await GRAPHQL_CLIENT_1.query(` + `); + console.log(JSON.stringify(req2, null, 4)); + expect(req2.data.updateAdminNote.id).toEqual(req.data.createAdminNote.id); + expect(req2.data.updateAdminNote.content).toEqual('Hello 2'); + const req3 = await GRAPHQL_CLIENT_1.query(` mutation { deleteAdminNote(input: { id: "${req.data.createAdminNote.id}" }) { id content } } - `) - console.log(JSON.stringify(req3, null, 4)) - expect(req3.data.deleteAdminNote.id).toEqual(req.data.createAdminNote.id) - expect(req3.data.deleteAdminNote.content).toEqual("Hello 2") -}) + `); + console.log(JSON.stringify(req3, null, 4)); + expect(req3.data.deleteAdminNote.id).toEqual(req.data.createAdminNote.id); + expect(req3.data.deleteAdminNote.content).toEqual('Hello 2'); +}); test(`Test creating, updating, and deleting an admin note as a non admin`, async () => { - const adminReq = await GRAPHQL_CLIENT_1.query(` + const adminReq = await GRAPHQL_CLIENT_1.query(` mutation { createAdminNote(input: { content: "Hello" }) { id content } } - `) - console.log(JSON.stringify(adminReq, null, 4)) - expect(adminReq.data.createAdminNote.id).toBeDefined() - expect(adminReq.data.createAdminNote.content).toEqual("Hello") - + `); + console.log(JSON.stringify(adminReq, null, 4)); + expect(adminReq.data.createAdminNote.id).toBeDefined(); + expect(adminReq.data.createAdminNote.content).toEqual('Hello'); - const req = await GRAPHQL_CLIENT_2.query(` + const req = await GRAPHQL_CLIENT_2.query(` mutation { createAdminNote(input: { content: "Hello" }) { id content } } - `) - console.log(JSON.stringify(req, null, 4)) - expect(req.errors.length).toEqual(1) - expect((req.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(req, null, 4)); + expect(req.errors.length).toEqual(1); + expect((req.errors[0] as any).errorType).toEqual('Unauthorized'); - const req2 = await GRAPHQL_CLIENT_2.query(` + const req2 = await GRAPHQL_CLIENT_2.query(` mutation { updateAdminNote(input: { id: "${adminReq.data.createAdminNote.id}", content: "Hello 2" }) { id content } } - `) - console.log(JSON.stringify(req2, null, 4)) - expect(req2.errors.length).toEqual(1) - expect((req2.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(req2, null, 4)); + expect(req2.errors.length).toEqual(1); + expect((req2.errors[0] as any).errorType).toEqual('Unauthorized'); - const req3 = await GRAPHQL_CLIENT_2.query(` + const req3 = await GRAPHQL_CLIENT_2.query(` mutation { deleteAdminNote(input: { id: "${adminReq.data.createAdminNote.id}" }) { id content } } - `) - console.log(JSON.stringify(req3, null, 4)) - expect(req3.errors.length).toEqual(1) - expect((req3.errors[0] as any).errorType).toEqual('Unauthorized') -}) + `); + console.log(JSON.stringify(req3, null, 4)); + expect(req3.errors.length).toEqual(1); + expect((req3.errors[0] as any).errorType).toEqual('Unauthorized'); +}); /** * Get Query Tests */ test(`Test getAllThree as admin.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: "user2@test.com" @@ -1217,11 +1292,11 @@ test(`Test getAllThree as admin.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); - const fetchOwnedBy2AsAdmin = await GRAPHQL_CLIENT_1.query(` + const fetchOwnedBy2AsAdmin = await GRAPHQL_CLIENT_1.query(` query { getAllThree(id: "${ownedBy2.data.createAllThree.id}") { id @@ -1231,23 +1306,23 @@ test(`Test getAllThree as admin.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(fetchOwnedBy2AsAdmin, null, 4)) - expect(fetchOwnedBy2AsAdmin.data.getAllThree).toBeTruthy() + `); + console.log(JSON.stringify(fetchOwnedBy2AsAdmin, null, 4)); + expect(fetchOwnedBy2AsAdmin.data.getAllThree).toBeTruthy(); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test getAllThree as owner.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: "user2@test.com" @@ -1259,11 +1334,11 @@ test(`Test getAllThree as owner.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); - const fetchOwnedBy2AsOwner = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedBy2AsOwner = await GRAPHQL_CLIENT_2.query(` query { getAllThree(id: "${ownedBy2.data.createAllThree.id}") { id @@ -1273,23 +1348,23 @@ test(`Test getAllThree as owner.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(fetchOwnedBy2AsOwner, null, 4)) - expect(fetchOwnedBy2AsOwner.data.getAllThree).toBeTruthy() + `); + console.log(JSON.stringify(fetchOwnedBy2AsOwner, null, 4)); + expect(fetchOwnedBy2AsOwner.data.getAllThree).toBeTruthy(); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test getAllThree as one of a set of editors.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { editors: ["user2@test.com"] @@ -1301,11 +1376,11 @@ test(`Test getAllThree as one of a set of editors.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); - const fetchOwnedBy2AsEditor = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedBy2AsEditor = await GRAPHQL_CLIENT_2.query(` query { getAllThree(id: "${ownedBy2.data.createAllThree.id}") { id @@ -1315,23 +1390,23 @@ test(`Test getAllThree as one of a set of editors.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(fetchOwnedBy2AsEditor, null, 4)) - expect(fetchOwnedBy2AsEditor.data.getAllThree).toBeTruthy() + `); + console.log(JSON.stringify(fetchOwnedBy2AsEditor, null, 4)); + expect(fetchOwnedBy2AsEditor.data.getAllThree).toBeTruthy(); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test getAllThree as a member of a dynamic group.`, async () => { - const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` + const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { groups: ["Devs"] @@ -1343,11 +1418,11 @@ test(`Test getAllThree as a member of a dynamic group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdmins, null, 4)) - expect(ownedByAdmins.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedByAdmins, null, 4)); + expect(ownedByAdmins.data.createAllThree).toBeTruthy(); - const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` query { getAllThree(id: "${ownedByAdmins.data.createAllThree.id}") { id @@ -1357,11 +1432,11 @@ test(`Test getAllThree as a member of a dynamic group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)) - expect(fetchOwnedByAdminsAsAdmin.data.getAllThree).toBeTruthy() + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)); + expect(fetchOwnedByAdminsAsAdmin.data.getAllThree).toBeTruthy(); - const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` + const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` query { getAllThree(id: "${ownedByAdmins.data.createAllThree.id}") { id @@ -1371,24 +1446,24 @@ test(`Test getAllThree as a member of a dynamic group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)) - expect(fetchOwnedByAdminsAsNonAdmin.errors.length).toEqual(1) - expect((fetchOwnedByAdminsAsNonAdmin.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)); + expect(fetchOwnedByAdminsAsNonAdmin.errors.length).toEqual(1); + expect((fetchOwnedByAdminsAsNonAdmin.errors[0] as any).errorType).toEqual('Unauthorized'); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByAdmins.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id); +}); test(`Test getAllThree as a member of the alternative group.`, async () => { - const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` + const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { alternativeGroup: "Devs" @@ -1400,11 +1475,11 @@ test(`Test getAllThree as a member of the alternative group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdmins, null, 4)) - expect(ownedByAdmins.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedByAdmins, null, 4)); + expect(ownedByAdmins.data.createAllThree).toBeTruthy(); - const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` query { getAllThree(id: "${ownedByAdmins.data.createAllThree.id}") { id @@ -1414,11 +1489,11 @@ test(`Test getAllThree as a member of the alternative group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)) - expect(fetchOwnedByAdminsAsAdmin.data.getAllThree).toBeTruthy() + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)); + expect(fetchOwnedByAdminsAsAdmin.data.getAllThree).toBeTruthy(); - const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` + const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` query { getAllThree(id: "${ownedByAdmins.data.createAllThree.id}") { id @@ -1428,28 +1503,28 @@ test(`Test getAllThree as a member of the alternative group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)) - expect(fetchOwnedByAdminsAsNonAdmin.errors.length).toEqual(1) - expect((fetchOwnedByAdminsAsNonAdmin.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)); + expect(fetchOwnedByAdminsAsNonAdmin.errors.length).toEqual(1); + expect((fetchOwnedByAdminsAsNonAdmin.errors[0] as any).errorType).toEqual('Unauthorized'); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByAdmins.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id); +}); /** * List Query Tests */ test(`Test listAllThrees as admin.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: "user2@test.com" @@ -1461,11 +1536,11 @@ test(`Test listAllThrees as admin.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); - const fetchOwnedBy2AsAdmin = await GRAPHQL_CLIENT_1.query(` + const fetchOwnedBy2AsAdmin = await GRAPHQL_CLIENT_1.query(` query { listAllThrees { items { @@ -1477,24 +1552,24 @@ test(`Test listAllThrees as admin.`, async () => { } } } - `) - console.log(JSON.stringify(fetchOwnedBy2AsAdmin, null, 4)) - expect(fetchOwnedBy2AsAdmin.data.listAllThrees.items).toHaveLength(1) - expect(fetchOwnedBy2AsAdmin.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id) + `); + console.log(JSON.stringify(fetchOwnedBy2AsAdmin, null, 4)); + expect(fetchOwnedBy2AsAdmin.data.listAllThrees.items).toHaveLength(1); + expect(fetchOwnedBy2AsAdmin.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test listAllThrees as owner.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: "user2@test.com" @@ -1506,11 +1581,11 @@ test(`Test listAllThrees as owner.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); - const fetchOwnedBy2AsOwner = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedBy2AsOwner = await GRAPHQL_CLIENT_2.query(` query { listAllThrees { items { @@ -1522,24 +1597,24 @@ test(`Test listAllThrees as owner.`, async () => { } } } - `) - console.log(JSON.stringify(fetchOwnedBy2AsOwner, null, 4)) - expect(fetchOwnedBy2AsOwner.data.listAllThrees.items).toHaveLength(1) - expect(fetchOwnedBy2AsOwner.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id) + `); + console.log(JSON.stringify(fetchOwnedBy2AsOwner, null, 4)); + expect(fetchOwnedBy2AsOwner.data.listAllThrees.items).toHaveLength(1); + expect(fetchOwnedBy2AsOwner.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test listAllThrees as one of a set of editors.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { editors: ["user2@test.com"] @@ -1551,11 +1626,11 @@ test(`Test listAllThrees as one of a set of editors.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); - const fetchOwnedBy2AsEditor = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedBy2AsEditor = await GRAPHQL_CLIENT_2.query(` query { listAllThrees { items { @@ -1567,24 +1642,24 @@ test(`Test listAllThrees as one of a set of editors.`, async () => { } } } - `) - console.log(JSON.stringify(fetchOwnedBy2AsEditor, null, 4)) - expect(fetchOwnedBy2AsEditor.data.listAllThrees.items).toHaveLength(1) - expect(fetchOwnedBy2AsEditor.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id) + `); + console.log(JSON.stringify(fetchOwnedBy2AsEditor, null, 4)); + expect(fetchOwnedBy2AsEditor.data.listAllThrees.items).toHaveLength(1); + expect(fetchOwnedBy2AsEditor.data.listAllThrees.items[0].id).toEqual(ownedBy2.data.createAllThree.id); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test listAllThrees as a member of a dynamic group.`, async () => { - const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` + const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { groups: ["Devs"] @@ -1596,11 +1671,11 @@ test(`Test listAllThrees as a member of a dynamic group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdmins, null, 4)) - expect(ownedByAdmins.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedByAdmins, null, 4)); + expect(ownedByAdmins.data.createAllThree).toBeTruthy(); - const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` query { listAllThrees { items { @@ -1612,12 +1687,12 @@ test(`Test listAllThrees as a member of a dynamic group.`, async () => { } } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)) - expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items).toHaveLength(1) - expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual(ownedByAdmins.data.createAllThree.id) + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)); + expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items).toHaveLength(1); + expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual(ownedByAdmins.data.createAllThree.id); - const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` + const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` query { listAllThrees { items { @@ -1629,23 +1704,23 @@ test(`Test listAllThrees as a member of a dynamic group.`, async () => { } } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)) - expect(fetchOwnedByAdminsAsNonAdmin.data.listAllThrees.items).toHaveLength(0) + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)); + expect(fetchOwnedByAdminsAsNonAdmin.data.listAllThrees.items).toHaveLength(0); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByAdmins.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id); +}); test(`Test getAllThree as a member of the alternative group.`, async () => { - const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` + const ownedByAdmins = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { alternativeGroup: "Devs" @@ -1657,11 +1732,11 @@ test(`Test getAllThree as a member of the alternative group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdmins, null, 4)) - expect(ownedByAdmins.data.createAllThree).toBeTruthy() + `); + console.log(JSON.stringify(ownedByAdmins, null, 4)); + expect(ownedByAdmins.data.createAllThree).toBeTruthy(); - const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` + const fetchOwnedByAdminsAsAdmin = await GRAPHQL_CLIENT_2.query(` query { listAllThrees { items { @@ -1673,12 +1748,12 @@ test(`Test getAllThree as a member of the alternative group.`, async () => { } } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)) - expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items).toHaveLength(1) - expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual(ownedByAdmins.data.createAllThree.id) + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsAdmin, null, 4)); + expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items).toHaveLength(1); + expect(fetchOwnedByAdminsAsAdmin.data.listAllThrees.items[0].id).toEqual(ownedByAdmins.data.createAllThree.id); - const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` + const fetchOwnedByAdminsAsNonAdmin = await GRAPHQL_CLIENT_3.query(` query { listAllThrees { items { @@ -1690,27 +1765,27 @@ test(`Test getAllThree as a member of the alternative group.`, async () => { } } } - `) - console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)) - expect(fetchOwnedByAdminsAsNonAdmin.data.listAllThrees.items).toHaveLength(0) + `); + console.log(JSON.stringify(fetchOwnedByAdminsAsNonAdmin, null, 4)); + expect(fetchOwnedByAdminsAsNonAdmin.data.listAllThrees.items).toHaveLength(0); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByAdmins.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id); +}); /** * Create Mutation Tests */ test(`Test createAllThree as admin.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: "user2@test.com" @@ -1722,17 +1797,17 @@ test(`Test createAllThree as admin.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() - // set by input - expect(ownedBy2.data.createAllThree.owner).toEqual("user2@test.com") - // auto filled as logged in user. - expect(ownedBy2.data.createAllThree.editors[0]).toEqual("user1@test.com") - expect(ownedBy2.data.createAllThree.groups).toBeNull() - expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); + // set by input + expect(ownedBy2.data.createAllThree.owner).toEqual('user2@test.com'); + // auto filled as logged in user. + expect(ownedBy2.data.createAllThree.editors[0]).toEqual('user1@test.com'); + expect(ownedBy2.data.createAllThree.groups).toBeNull(); + expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull(); - const ownedBy2NoEditors = await GRAPHQL_CLIENT_1.query(` + const ownedBy2NoEditors = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: "user2@test.com", @@ -1745,39 +1820,39 @@ test(`Test createAllThree as admin.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2NoEditors, null, 4)) - expect(ownedBy2NoEditors.data.createAllThree).toBeTruthy() - // set by input - expect(ownedBy2NoEditors.data.createAllThree.owner).toEqual("user2@test.com") - // set by input - expect(ownedBy2NoEditors.data.createAllThree.editors).toHaveLength(0) - expect(ownedBy2NoEditors.data.createAllThree.groups).toBeNull() - expect(ownedBy2NoEditors.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2NoEditors, null, 4)); + expect(ownedBy2NoEditors.data.createAllThree).toBeTruthy(); + // set by input + expect(ownedBy2NoEditors.data.createAllThree.owner).toEqual('user2@test.com'); + // set by input + expect(ownedBy2NoEditors.data.createAllThree.editors).toHaveLength(0); + expect(ownedBy2NoEditors.data.createAllThree.groups).toBeNull(); + expect(ownedBy2NoEditors.data.createAllThree.alternativeGroup).toBeNull(); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); - const deleteReq2 = await GRAPHQL_CLIENT_1.query(` + const deleteReq2 = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2NoEditors.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq2, null, 4)) - expect(deleteReq2.data.deleteAllThree.id).toEqual(ownedBy2NoEditors.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq2, null, 4)); + expect(deleteReq2.data.deleteAllThree.id).toEqual(ownedBy2NoEditors.data.createAllThree.id); +}); test(`Test createAllThree as owner.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_2.query(` + const ownedBy2 = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: "user2@test.com", @@ -1790,15 +1865,15 @@ test(`Test createAllThree as owner.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() - expect(ownedBy2.data.createAllThree.owner).toEqual("user2@test.com") - expect(ownedBy2.data.createAllThree.editors).toHaveLength(0) - expect(ownedBy2.data.createAllThree.groups).toBeNull() - expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); + expect(ownedBy2.data.createAllThree.owner).toEqual('user2@test.com'); + expect(ownedBy2.data.createAllThree.editors).toHaveLength(0); + expect(ownedBy2.data.createAllThree.groups).toBeNull(); + expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull(); - const ownedBy1 = await GRAPHQL_CLIENT_2.query(` + const ownedBy1 = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: "user1@test.com", @@ -1811,25 +1886,24 @@ test(`Test createAllThree as owner.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy1, null, 4)) - expect(ownedBy1.errors.length).toEqual(1) - expect((ownedBy1.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(ownedBy1, null, 4)); + expect(ownedBy1.errors.length).toEqual(1); + expect((ownedBy1.errors[0] as any).errorType).toEqual('Unauthorized'); - - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test createAllThree as one of a set of editors.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_2.query(` + const ownedBy2 = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: null, @@ -1842,15 +1916,15 @@ test(`Test createAllThree as one of a set of editors.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() - expect(ownedBy2.data.createAllThree.owner).toBeNull() - expect(ownedBy2.data.createAllThree.editors[0]).toEqual("user2@test.com") - expect(ownedBy2.data.createAllThree.groups).toBeNull() - expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); + expect(ownedBy2.data.createAllThree.owner).toBeNull(); + expect(ownedBy2.data.createAllThree.editors[0]).toEqual('user2@test.com'); + expect(ownedBy2.data.createAllThree.groups).toBeNull(); + expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull(); - const ownedBy2WithDefaultOwner = await GRAPHQL_CLIENT_2.query(` + const ownedBy2WithDefaultOwner = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { editors: ["user2@test.com"] @@ -1862,15 +1936,15 @@ test(`Test createAllThree as one of a set of editors.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2WithDefaultOwner, null, 4)) - expect(ownedBy2WithDefaultOwner.data.createAllThree).toBeTruthy() - expect(ownedBy2WithDefaultOwner.data.createAllThree.owner).toEqual("user2@test.com") - expect(ownedBy2WithDefaultOwner.data.createAllThree.editors[0]).toEqual("user2@test.com") - expect(ownedBy2WithDefaultOwner.data.createAllThree.groups).toBeNull() - expect(ownedBy2WithDefaultOwner.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2WithDefaultOwner, null, 4)); + expect(ownedBy2WithDefaultOwner.data.createAllThree).toBeTruthy(); + expect(ownedBy2WithDefaultOwner.data.createAllThree.owner).toEqual('user2@test.com'); + expect(ownedBy2WithDefaultOwner.data.createAllThree.editors[0]).toEqual('user2@test.com'); + expect(ownedBy2WithDefaultOwner.data.createAllThree.groups).toBeNull(); + expect(ownedBy2WithDefaultOwner.data.createAllThree.alternativeGroup).toBeNull(); - const ownedByEditorsUnauthed = await GRAPHQL_CLIENT_2.query(` + const ownedByEditorsUnauthed = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: null, @@ -1883,35 +1957,34 @@ test(`Test createAllThree as one of a set of editors.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByEditorsUnauthed, null, 4)) - expect(ownedByEditorsUnauthed.errors.length).toEqual(1) - expect((ownedByEditorsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(ownedByEditorsUnauthed, null, 4)); + expect(ownedByEditorsUnauthed.errors.length).toEqual(1); + expect((ownedByEditorsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized'); - - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); - const deleteReq2 = await GRAPHQL_CLIENT_1.query(` + const deleteReq2 = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2WithDefaultOwner.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq2, null, 4)) - expect(deleteReq2.data.deleteAllThree.id).toEqual(ownedBy2WithDefaultOwner.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq2, null, 4)); + expect(deleteReq2.data.deleteAllThree.id).toEqual(ownedBy2WithDefaultOwner.data.createAllThree.id); +}); test(`Test createAllThree as a member of a dynamic group.`, async () => { - const ownedByDevs = await GRAPHQL_CLIENT_2.query(` + const ownedByDevs = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: null, @@ -1925,15 +1998,15 @@ test(`Test createAllThree as a member of a dynamic group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByDevs, null, 4)) - expect(ownedByDevs.data.createAllThree).toBeTruthy() - expect(ownedByDevs.data.createAllThree.owner).toBeNull() - expect(ownedByDevs.data.createAllThree.editors).toHaveLength(0) - expect(ownedByDevs.data.createAllThree.groups[0]).toEqual("Devs") - expect(ownedByDevs.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedByDevs, null, 4)); + expect(ownedByDevs.data.createAllThree).toBeTruthy(); + expect(ownedByDevs.data.createAllThree.owner).toBeNull(); + expect(ownedByDevs.data.createAllThree.editors).toHaveLength(0); + expect(ownedByDevs.data.createAllThree.groups[0]).toEqual('Devs'); + expect(ownedByDevs.data.createAllThree.alternativeGroup).toBeNull(); - const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_3.query(` + const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_3.query(` mutation { createAllThree(input: { owner: null, @@ -1947,24 +2020,24 @@ test(`Test createAllThree as a member of a dynamic group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)) - expect(ownedByAdminsUnauthed.errors.length).toEqual(1) - expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)); + expect(ownedByAdminsUnauthed.errors.length).toEqual(1); + expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized'); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByDevs.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByDevs.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByDevs.data.createAllThree.id); +}); test(`Test createAllThree as a member of the alternative group.`, async () => { - const ownedByAdmins = await GRAPHQL_CLIENT_2.query(` + const ownedByAdmins = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: null, @@ -1978,14 +2051,14 @@ test(`Test createAllThree as a member of the alternative group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdmins, null, 4)) - expect(ownedByAdmins.data.createAllThree).toBeTruthy() - expect(ownedByAdmins.data.createAllThree.owner).toBeNull() - expect(ownedByAdmins.data.createAllThree.editors).toHaveLength(0) - expect(ownedByAdmins.data.createAllThree.alternativeGroup).toEqual("Devs") + `); + console.log(JSON.stringify(ownedByAdmins, null, 4)); + expect(ownedByAdmins.data.createAllThree).toBeTruthy(); + expect(ownedByAdmins.data.createAllThree.owner).toBeNull(); + expect(ownedByAdmins.data.createAllThree.editors).toHaveLength(0); + expect(ownedByAdmins.data.createAllThree.alternativeGroup).toEqual('Devs'); - const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_3.query(` + const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_3.query(` mutation { createAllThree(input: { owner: null, @@ -1999,29 +2072,28 @@ test(`Test createAllThree as a member of the alternative group.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)) - expect(ownedByAdminsUnauthed.errors.length).toEqual(1) - expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)); + expect(ownedByAdminsUnauthed.errors.length).toEqual(1); + expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized'); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByAdmins.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByAdmins.data.createAllThree.id); +}); /** * Update Mutation Tests */ - test(`Test updateAllThree and deleteAllThree as admin.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { editors: [] @@ -2034,17 +2106,17 @@ test(`Test updateAllThree and deleteAllThree as admin.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() - // set by input - expect(ownedBy2.data.createAllThree.owner).toEqual("user2@test.com") - // auto filled as logged in user. - expect(ownedBy2.data.createAllThree.editors).toHaveLength(0) - expect(ownedBy2.data.createAllThree.groups).toBeNull() - expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); + // set by input + expect(ownedBy2.data.createAllThree.owner).toEqual('user2@test.com'); + // auto filled as logged in user. + expect(ownedBy2.data.createAllThree.editors).toHaveLength(0); + expect(ownedBy2.data.createAllThree.groups).toBeNull(); + expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull(); - const ownedByTwoUpdate = await GRAPHQL_CLIENT_1.query(` + const ownedByTwoUpdate = await GRAPHQL_CLIENT_1.query(` mutation { updateAllThree(input: { id: "${ownedBy2.data.createAllThree.id}", @@ -2057,29 +2129,29 @@ test(`Test updateAllThree and deleteAllThree as admin.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedByTwoUpdate, null, 4)) - expect(ownedByTwoUpdate.data.updateAllThree).toBeTruthy() - // set by input - expect(ownedByTwoUpdate.data.updateAllThree.owner).toEqual("user2@test.com") - // set by input - expect(ownedByTwoUpdate.data.updateAllThree.editors).toHaveLength(0) - expect(ownedByTwoUpdate.data.updateAllThree.groups).toBeNull() - expect(ownedByTwoUpdate.data.updateAllThree.alternativeGroup).toEqual("Devs") + `); + console.log(JSON.stringify(ownedByTwoUpdate, null, 4)); + expect(ownedByTwoUpdate.data.updateAllThree).toBeTruthy(); + // set by input + expect(ownedByTwoUpdate.data.updateAllThree.owner).toEqual('user2@test.com'); + // set by input + expect(ownedByTwoUpdate.data.updateAllThree.editors).toHaveLength(0); + expect(ownedByTwoUpdate.data.updateAllThree.groups).toBeNull(); + expect(ownedByTwoUpdate.data.updateAllThree.alternativeGroup).toEqual('Devs'); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test updateAllThree and deleteAllThree as owner.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_2.query(` + const ownedBy2 = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: "user2@test.com", @@ -2092,15 +2164,15 @@ test(`Test updateAllThree and deleteAllThree as owner.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() - expect(ownedBy2.data.createAllThree.owner).toEqual("user2@test.com") - expect(ownedBy2.data.createAllThree.editors).toHaveLength(0) - expect(ownedBy2.data.createAllThree.groups).toBeNull() - expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); + expect(ownedBy2.data.createAllThree.owner).toEqual('user2@test.com'); + expect(ownedBy2.data.createAllThree.editors).toHaveLength(0); + expect(ownedBy2.data.createAllThree.groups).toBeNull(); + expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull(); - const ownedBy2Update = await GRAPHQL_CLIENT_2.query(` + const ownedBy2Update = await GRAPHQL_CLIENT_2.query(` mutation { updateAllThree(input: { id: "${ownedBy2.data.createAllThree.id}", @@ -2113,30 +2185,29 @@ test(`Test updateAllThree and deleteAllThree as owner.`, async () => { alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2Update, null, 4)) - expect(ownedBy2Update.data.updateAllThree).toBeTruthy() - // set by input - expect(ownedBy2Update.data.updateAllThree.owner).toEqual("user2@test.com") - // set by input - expect(ownedBy2Update.data.updateAllThree.editors).toHaveLength(0) - expect(ownedBy2Update.data.updateAllThree.groups).toBeNull() - expect(ownedBy2Update.data.updateAllThree.alternativeGroup).toEqual("Devs") + `); + console.log(JSON.stringify(ownedBy2Update, null, 4)); + expect(ownedBy2Update.data.updateAllThree).toBeTruthy(); + // set by input + expect(ownedBy2Update.data.updateAllThree.owner).toEqual('user2@test.com'); + // set by input + expect(ownedBy2Update.data.updateAllThree.editors).toHaveLength(0); + expect(ownedBy2Update.data.updateAllThree.groups).toBeNull(); + expect(ownedBy2Update.data.updateAllThree.alternativeGroup).toEqual('Devs'); - - const deleteReq = await GRAPHQL_CLIENT_2.query(` + const deleteReq = await GRAPHQL_CLIENT_2.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test updateAllThree and deleteAllThree as one of a set of editors.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_2.query(` + const ownedBy2 = await GRAPHQL_CLIENT_2.query(` mutation { createAllThree(input: { owner: null, @@ -2149,15 +2220,15 @@ test(`Test updateAllThree and deleteAllThree as one of a set of editors.`, async alternativeGroup } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createAllThree).toBeTruthy() - expect(ownedBy2.data.createAllThree.owner).toBeNull() - expect(ownedBy2.data.createAllThree.editors[0]).toEqual("user2@test.com") - expect(ownedBy2.data.createAllThree.groups).toBeNull() - expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createAllThree).toBeTruthy(); + expect(ownedBy2.data.createAllThree.owner).toBeNull(); + expect(ownedBy2.data.createAllThree.editors[0]).toEqual('user2@test.com'); + expect(ownedBy2.data.createAllThree.groups).toBeNull(); + expect(ownedBy2.data.createAllThree.alternativeGroup).toBeNull(); - const ownedByUpdate = await GRAPHQL_CLIENT_2.query(` + const ownedByUpdate = await GRAPHQL_CLIENT_2.query(` mutation { updateAllThree(input: { id: "${ownedBy2.data.createAllThree.id}", @@ -2170,29 +2241,29 @@ test(`Test updateAllThree and deleteAllThree as one of a set of editors.`, async alternativeGroup } } - `) - console.log(JSON.stringify(ownedByUpdate, null, 4)) - expect(ownedByUpdate.data.updateAllThree).toBeTruthy() - // set by input - expect(ownedByUpdate.data.updateAllThree.owner).toBeNull() - // set by input - expect(ownedByUpdate.data.updateAllThree.editors[0]).toEqual("user2@test.com") - expect(ownedByUpdate.data.updateAllThree.groups).toBeNull() - expect(ownedByUpdate.data.updateAllThree.alternativeGroup).toEqual("Devs") + `); + console.log(JSON.stringify(ownedByUpdate, null, 4)); + expect(ownedByUpdate.data.updateAllThree).toBeTruthy(); + // set by input + expect(ownedByUpdate.data.updateAllThree.owner).toBeNull(); + // set by input + expect(ownedByUpdate.data.updateAllThree.editors[0]).toEqual('user2@test.com'); + expect(ownedByUpdate.data.updateAllThree.groups).toBeNull(); + expect(ownedByUpdate.data.updateAllThree.alternativeGroup).toEqual('Devs'); - const deleteReq = await GRAPHQL_CLIENT_2.query(` + const deleteReq = await GRAPHQL_CLIENT_2.query(` mutation { deleteAllThree(input: { id: "${ownedBy2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedBy2.data.createAllThree.id); +}); test(`Test updateAllThree and deleteAllThree as a member of a dynamic group.`, async () => { - const ownedByDevs = await GRAPHQL_CLIENT_1.query(` + const ownedByDevs = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: null, @@ -2206,15 +2277,15 @@ test(`Test updateAllThree and deleteAllThree as a member of a dynamic group.`, a alternativeGroup } } - `) - console.log(JSON.stringify(ownedByDevs, null, 4)) - expect(ownedByDevs.data.createAllThree).toBeTruthy() - expect(ownedByDevs.data.createAllThree.owner).toBeNull() - expect(ownedByDevs.data.createAllThree.editors).toHaveLength(0) - expect(ownedByDevs.data.createAllThree.groups[0]).toEqual("Devs") - expect(ownedByDevs.data.createAllThree.alternativeGroup).toBeNull() + `); + console.log(JSON.stringify(ownedByDevs, null, 4)); + expect(ownedByDevs.data.createAllThree).toBeTruthy(); + expect(ownedByDevs.data.createAllThree.owner).toBeNull(); + expect(ownedByDevs.data.createAllThree.editors).toHaveLength(0); + expect(ownedByDevs.data.createAllThree.groups[0]).toEqual('Devs'); + expect(ownedByDevs.data.createAllThree.alternativeGroup).toBeNull(); - const ownedByUpdate = await GRAPHQL_CLIENT_2.query(` + const ownedByUpdate = await GRAPHQL_CLIENT_2.query(` mutation { updateAllThree(input: { id: "${ownedByDevs.data.createAllThree.id}", @@ -2227,17 +2298,17 @@ test(`Test updateAllThree and deleteAllThree as a member of a dynamic group.`, a alternativeGroup } } - `) - console.log(JSON.stringify(ownedByUpdate, null, 4)) - expect(ownedByUpdate.data.updateAllThree).toBeTruthy() - // set by input - expect(ownedByUpdate.data.updateAllThree.owner).toBeNull() - // set by input - expect(ownedByUpdate.data.updateAllThree.editors).toHaveLength(0) - expect(ownedByUpdate.data.updateAllThree.groups[0]).toEqual("Devs") - expect(ownedByUpdate.data.updateAllThree.alternativeGroup).toEqual("Devs") + `); + console.log(JSON.stringify(ownedByUpdate, null, 4)); + expect(ownedByUpdate.data.updateAllThree).toBeTruthy(); + // set by input + expect(ownedByUpdate.data.updateAllThree.owner).toBeNull(); + // set by input + expect(ownedByUpdate.data.updateAllThree.editors).toHaveLength(0); + expect(ownedByUpdate.data.updateAllThree.groups[0]).toEqual('Devs'); + expect(ownedByUpdate.data.updateAllThree.alternativeGroup).toEqual('Devs'); - const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_3.query(` + const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_3.query(` mutation { createAllThree(input: { owner: null, @@ -2251,24 +2322,24 @@ test(`Test updateAllThree and deleteAllThree as a member of a dynamic group.`, a alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)) - expect(ownedByAdminsUnauthed.errors.length).toEqual(1) - expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized') + `); + console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)); + expect(ownedByAdminsUnauthed.errors.length).toEqual(1); + expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('Unauthorized'); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByDevs.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByDevs.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByDevs.data.createAllThree.id); +}); test(`Test updateAllThree and deleteAllThree as a member of the alternative group.`, async () => { - const ownedByDevs = await GRAPHQL_CLIENT_1.query(` + const ownedByDevs = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: null, @@ -2282,14 +2353,14 @@ test(`Test updateAllThree and deleteAllThree as a member of the alternative grou alternativeGroup } } - `) - console.log(JSON.stringify(ownedByDevs, null, 4)) - expect(ownedByDevs.data.createAllThree).toBeTruthy() - expect(ownedByDevs.data.createAllThree.owner).toBeNull() - expect(ownedByDevs.data.createAllThree.editors).toHaveLength(0) - expect(ownedByDevs.data.createAllThree.alternativeGroup).toEqual("Devs") + `); + console.log(JSON.stringify(ownedByDevs, null, 4)); + expect(ownedByDevs.data.createAllThree).toBeTruthy(); + expect(ownedByDevs.data.createAllThree.owner).toBeNull(); + expect(ownedByDevs.data.createAllThree.editors).toHaveLength(0); + expect(ownedByDevs.data.createAllThree.alternativeGroup).toEqual('Devs'); - const ownedByUpdate = await GRAPHQL_CLIENT_2.query(` + const ownedByUpdate = await GRAPHQL_CLIENT_2.query(` mutation { updateAllThree(input: { id: "${ownedByDevs.data.createAllThree.id}", @@ -2302,17 +2373,17 @@ test(`Test updateAllThree and deleteAllThree as a member of the alternative grou alternativeGroup } } - `) - console.log(JSON.stringify(ownedByUpdate, null, 4)) - expect(ownedByUpdate.data.updateAllThree).toBeTruthy() - // set by input - expect(ownedByUpdate.data.updateAllThree.owner).toBeNull() - // set by input - expect(ownedByUpdate.data.updateAllThree.editors).toHaveLength(0) - expect(ownedByUpdate.data.updateAllThree.groups).toBeNull() - expect(ownedByUpdate.data.updateAllThree.alternativeGroup).toEqual("Admin") + `); + console.log(JSON.stringify(ownedByUpdate, null, 4)); + expect(ownedByUpdate.data.updateAllThree).toBeTruthy(); + // set by input + expect(ownedByUpdate.data.updateAllThree.owner).toBeNull(); + // set by input + expect(ownedByUpdate.data.updateAllThree.editors).toHaveLength(0); + expect(ownedByUpdate.data.updateAllThree.groups).toBeNull(); + expect(ownedByUpdate.data.updateAllThree.alternativeGroup).toEqual('Admin'); - const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_2.query(` + const ownedByAdminsUnauthed = await GRAPHQL_CLIENT_2.query(` mutation { updateAllThree(input: { id: "${ownedByDevs.data.createAllThree.id}", @@ -2325,12 +2396,12 @@ test(`Test updateAllThree and deleteAllThree as a member of the alternative grou alternativeGroup } } - `) - console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)) - expect(ownedByAdminsUnauthed.errors.length).toEqual(1) - expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException') + `); + console.log(JSON.stringify(ownedByAdminsUnauthed, null, 4)); + expect(ownedByAdminsUnauthed.errors.length).toEqual(1); + expect((ownedByAdminsUnauthed.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); - const ownedByDevs2 = await GRAPHQL_CLIENT_1.query(` + const ownedByDevs2 = await GRAPHQL_CLIENT_1.query(` mutation { createAllThree(input: { owner: null, @@ -2344,36 +2415,36 @@ test(`Test updateAllThree and deleteAllThree as a member of the alternative grou alternativeGroup } } - `) - console.log(JSON.stringify(ownedByDevs2, null, 4)) - expect(ownedByDevs2.data.createAllThree).toBeTruthy() - expect(ownedByDevs2.data.createAllThree.owner).toBeNull() - expect(ownedByDevs2.data.createAllThree.editors).toHaveLength(0) - expect(ownedByDevs2.data.createAllThree.alternativeGroup).toEqual("Devs") + `); + console.log(JSON.stringify(ownedByDevs2, null, 4)); + expect(ownedByDevs2.data.createAllThree).toBeTruthy(); + expect(ownedByDevs2.data.createAllThree.owner).toBeNull(); + expect(ownedByDevs2.data.createAllThree.editors).toHaveLength(0); + expect(ownedByDevs2.data.createAllThree.alternativeGroup).toEqual('Devs'); - const deleteReq2 = await GRAPHQL_CLIENT_2.query(` + const deleteReq2 = await GRAPHQL_CLIENT_2.query(` mutation { deleteAllThree(input: { id: "${ownedByDevs2.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq2, null, 4)) - expect(deleteReq2.data.deleteAllThree.id).toEqual(ownedByDevs2.data.createAllThree.id) + `); + console.log(JSON.stringify(deleteReq2, null, 4)); + expect(deleteReq2.data.deleteAllThree.id).toEqual(ownedByDevs2.data.createAllThree.id); - const deleteReq = await GRAPHQL_CLIENT_1.query(` + const deleteReq = await GRAPHQL_CLIENT_1.query(` mutation { deleteAllThree(input: { id: "${ownedByDevs.data.createAllThree.id}" }) { id } } - `) - console.log(JSON.stringify(deleteReq, null, 4)) - expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByDevs.data.createAllThree.id) -}) + `); + console.log(JSON.stringify(deleteReq, null, 4)); + expect(deleteReq.data.deleteAllThree.id).toEqual(ownedByDevs.data.createAllThree.id); +}); test(`Test createTestIdentity as admin.`, async () => { - const ownedBy2 = await GRAPHQL_CLIENT_1.query(` + const ownedBy2 = await GRAPHQL_CLIENT_1.query(` mutation { createTestIdentity(input: { title: "Test title" @@ -2383,14 +2454,14 @@ test(`Test createTestIdentity as admin.`, async () => { owner } } - `) - console.log(JSON.stringify(ownedBy2, null, 4)) - expect(ownedBy2.data.createTestIdentity).toBeTruthy() - expect(ownedBy2.data.createTestIdentity.title).toEqual("Test title") - expect(ownedBy2.data.createTestIdentity.owner.slice(0, 19)).toEqual("https://cognito-idp") + `); + console.log(JSON.stringify(ownedBy2, null, 4)); + expect(ownedBy2.data.createTestIdentity).toBeTruthy(); + expect(ownedBy2.data.createTestIdentity.title).toEqual('Test title'); + expect(ownedBy2.data.createTestIdentity.owner.slice(0, 19)).toEqual('https://cognito-idp'); - // user 2 should be able to update because they share the same issuer. - const update = await GRAPHQL_CLIENT_3.query(` + // user 2 should be able to update because they share the same issuer. + const update = await GRAPHQL_CLIENT_3.query(` mutation { updateTestIdentity(input: { id: "${ownedBy2.data.createTestIdentity.id}", @@ -2401,14 +2472,14 @@ test(`Test createTestIdentity as admin.`, async () => { owner } } - `) - console.log(JSON.stringify(update, null, 4)) - expect(update.data.updateTestIdentity).toBeTruthy() - expect(update.data.updateTestIdentity.title).toEqual("Test title update") - expect(update.data.updateTestIdentity.owner.slice(0, 19)).toEqual("https://cognito-idp") + `); + console.log(JSON.stringify(update, null, 4)); + expect(update.data.updateTestIdentity).toBeTruthy(); + expect(update.data.updateTestIdentity.title).toEqual('Test title update'); + expect(update.data.updateTestIdentity.owner.slice(0, 19)).toEqual('https://cognito-idp'); - // user 2 should be able to get because they share the same issuer. - const getReq = await GRAPHQL_CLIENT_3.query(` + // user 2 should be able to get because they share the same issuer. + const getReq = await GRAPHQL_CLIENT_3.query(` query { getTestIdentity(id: "${ownedBy2.data.createTestIdentity.id}") { id @@ -2416,13 +2487,14 @@ test(`Test createTestIdentity as admin.`, async () => { owner } } - `) - console.log(JSON.stringify(getReq, null, 4)) - expect(getReq.data.getTestIdentity).toBeTruthy() - expect(getReq.data.getTestIdentity.title).toEqual("Test title update") - expect(getReq.data.getTestIdentity.owner.slice(0, 19)).toEqual("https://cognito-idp") + `); + console.log(JSON.stringify(getReq, null, 4)); + expect(getReq.data.getTestIdentity).toBeTruthy(); + expect(getReq.data.getTestIdentity.title).toEqual('Test title update'); + expect(getReq.data.getTestIdentity.owner.slice(0, 19)).toEqual('https://cognito-idp'); - const listResponse = await GRAPHQL_CLIENT_3.query(`query { + const listResponse = await GRAPHQL_CLIENT_3.query( + `query { listTestIdentitys(filter: { title: { eq: "Test title update" } }, limit: 100) { items { id @@ -2430,15 +2502,17 @@ test(`Test createTestIdentity as admin.`, async () => { owner } } - }`, {}) - const relevantPost = listResponse.data.listTestIdentitys.items.find(p => p.id === getReq.data.getTestIdentity.id) - console.log(JSON.stringify(listResponse, null, 4)) - expect(relevantPost).toBeTruthy() - expect(relevantPost.title).toEqual("Test title update") - expect(relevantPost.owner.slice(0, 19)).toEqual("https://cognito-idp") + }`, + {} + ); + const relevantPost = listResponse.data.listTestIdentitys.items.find(p => p.id === getReq.data.getTestIdentity.id); + console.log(JSON.stringify(listResponse, null, 4)); + expect(relevantPost).toBeTruthy(); + expect(relevantPost.title).toEqual('Test title update'); + expect(relevantPost.owner.slice(0, 19)).toEqual('https://cognito-idp'); - // user 2 should be able to delete because they share the same issuer. - const delReq = await GRAPHQL_CLIENT_3.query(` + // user 2 should be able to delete because they share the same issuer. + const delReq = await GRAPHQL_CLIENT_3.query(` mutation { deleteTestIdentity(input: { id: "${ownedBy2.data.createTestIdentity.id}" @@ -2448,108 +2522,133 @@ test(`Test createTestIdentity as admin.`, async () => { owner } } - `) - console.log(JSON.stringify(delReq, null, 4)) - expect(delReq.data.deleteTestIdentity).toBeTruthy() - expect(delReq.data.deleteTestIdentity.title).toEqual("Test title update") - expect(delReq.data.deleteTestIdentity.owner.slice(0, 19)).toEqual("https://cognito-idp") -}) + `); + console.log(JSON.stringify(delReq, null, 4)); + expect(delReq.data.deleteTestIdentity).toBeTruthy(); + expect(delReq.data.deleteTestIdentity.title).toEqual('Test title update'); + expect(delReq.data.deleteTestIdentity.owner.slice(0, 19)).toEqual('https://cognito-idp'); +}); /** * Test 'operations' argument */ -test('Test get and list with \'read\' operation set', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { +test("Test get and list with 'read' operation set", async () => { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createOwnerReadProtected(input: { content: "Hello, World!", owner: "${USERNAME1}" }) { id content owner } - }`, {}) - console.log(response); - expect(response.data.createOwnerReadProtected.id).toBeDefined() - expect(response.data.createOwnerReadProtected.content).toEqual('Hello, World!') - expect(response.data.createOwnerReadProtected.owner).toEqual(USERNAME1) + }`, + {} + ); + console.log(response); + expect(response.data.createOwnerReadProtected.id).toBeDefined(); + expect(response.data.createOwnerReadProtected.content).toEqual('Hello, World!'); + expect(response.data.createOwnerReadProtected.owner).toEqual(USERNAME1); - const response2 = await GRAPHQL_CLIENT_2.query(`query { + const response2 = await GRAPHQL_CLIENT_2.query( + `query { getOwnerReadProtected(id: "${response.data.createOwnerReadProtected.id}") { id content owner } - }`, {}) - console.log(response2); - expect(response2.data.getOwnerReadProtected).toBeNull() - expect(response2.errors).toHaveLength(1) + }`, + {} + ); + console.log(response2); + expect(response2.data.getOwnerReadProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); - const response3 = await GRAPHQL_CLIENT_1.query(`query { + const response3 = await GRAPHQL_CLIENT_1.query( + `query { getOwnerReadProtected(id: "${response.data.createOwnerReadProtected.id}") { id content owner } - }`, {}) - console.log(response3); - expect(response3.data.getOwnerReadProtected.id).toBeDefined() - expect(response3.data.getOwnerReadProtected.content).toEqual('Hello, World!') - expect(response3.data.getOwnerReadProtected.owner).toEqual(USERNAME1) + }`, + {} + ); + console.log(response3); + expect(response3.data.getOwnerReadProtected.id).toBeDefined(); + expect(response3.data.getOwnerReadProtected.content).toEqual('Hello, World!'); + expect(response3.data.getOwnerReadProtected.owner).toEqual(USERNAME1); - const response4 = await GRAPHQL_CLIENT_1.query(`query { + const response4 = await GRAPHQL_CLIENT_1.query( + `query { listOwnerReadProtecteds { items { id content owner } } - }`, {}) - console.log(response4); - expect(response4.data.listOwnerReadProtecteds.items.length).toBeGreaterThanOrEqual(1); + }`, + {} + ); + console.log(response4); + expect(response4.data.listOwnerReadProtecteds.items.length).toBeGreaterThanOrEqual(1); - const response5 = await GRAPHQL_CLIENT_2.query(`query { + const response5 = await GRAPHQL_CLIENT_2.query( + `query { listOwnerReadProtecteds { items { id content owner } } - }`, {}) - console.log(response5); - expect(response5.data.listOwnerReadProtecteds.items).toHaveLength(0); -}) + }`, + {} + ); + console.log(response5); + expect(response5.data.listOwnerReadProtecteds.items).toHaveLength(0); +}); -test('Test createOwnerCreateUpdateDeleteProtected with \'create\' operation set', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { +test("Test createOwnerCreateUpdateDeleteProtected with 'create' operation set", async () => { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createOwnerCreateUpdateDeleteProtected(input: { content: "Hello, World!", owner: "${USERNAME1}" }) { id content owner } - }`, {}) - console.log(response); - expect(response.data.createOwnerCreateUpdateDeleteProtected.id).toBeDefined() - expect(response.data.createOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!') - expect(response.data.createOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1) + }`, + {} + ); + console.log(response); + expect(response.data.createOwnerCreateUpdateDeleteProtected.id).toBeDefined(); + expect(response.data.createOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!'); + expect(response.data.createOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1); - const response2 = await GRAPHQL_CLIENT_1.query(`mutation { + const response2 = await GRAPHQL_CLIENT_1.query( + `mutation { createOwnerCreateUpdateDeleteProtected(input: { content: "Hello, World!", owner: "${USERNAME2}" }) { id content owner } - }`, {}) - console.log(response2); - expect(response2.data.createOwnerCreateUpdateDeleteProtected).toBeNull() - expect(response2.errors).toHaveLength(1) -}) + }`, + {} + ); + console.log(response2); + expect(response2.data.createOwnerCreateUpdateDeleteProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); +}); -test('Test updateOwnerCreateUpdateDeleteProtected with \'update\' operation set', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { +test("Test updateOwnerCreateUpdateDeleteProtected with 'update' operation set", async () => { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createOwnerCreateUpdateDeleteProtected(input: { content: "Hello, World!", owner: "${USERNAME1}" }) { id content owner } - }`, {}) - console.log(response); - expect(response.data.createOwnerCreateUpdateDeleteProtected.id).toBeDefined() - expect(response.data.createOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!') - expect(response.data.createOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1) + }`, + {} + ); + console.log(response); + expect(response.data.createOwnerCreateUpdateDeleteProtected.id).toBeDefined(); + expect(response.data.createOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!'); + expect(response.data.createOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1); - const response2 = await GRAPHQL_CLIENT_2.query(`mutation { + const response2 = await GRAPHQL_CLIENT_2.query( + `mutation { updateOwnerCreateUpdateDeleteProtected( input: { id: "${response.data.createOwnerCreateUpdateDeleteProtected.id}", @@ -2560,12 +2659,15 @@ test('Test updateOwnerCreateUpdateDeleteProtected with \'update\' operation set' content owner } - }`, {}) - console.log(response2); - expect(response2.data.updateOwnerCreateUpdateDeleteProtected).toBeNull() - expect(response2.errors).toHaveLength(1) + }`, + {} + ); + console.log(response2); + expect(response2.data.updateOwnerCreateUpdateDeleteProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); - const response3 = await GRAPHQL_CLIENT_1.query(`mutation { + const response3 = await GRAPHQL_CLIENT_1.query( + `mutation { updateOwnerCreateUpdateDeleteProtected( input: { id: "${response.data.createOwnerCreateUpdateDeleteProtected.id}", @@ -2576,27 +2678,33 @@ test('Test updateOwnerCreateUpdateDeleteProtected with \'update\' operation set' content owner } - }`, {}) - console.log(response3); - expect(response3.data.updateOwnerCreateUpdateDeleteProtected.id).toBeDefined() - expect(response3.data.updateOwnerCreateUpdateDeleteProtected.content).toEqual('Bye, World!') - expect(response3.data.updateOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1) -}) + }`, + {} + ); + console.log(response3); + expect(response3.data.updateOwnerCreateUpdateDeleteProtected.id).toBeDefined(); + expect(response3.data.updateOwnerCreateUpdateDeleteProtected.content).toEqual('Bye, World!'); + expect(response3.data.updateOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1); +}); -test('Test deleteOwnerCreateUpdateDeleteProtected with \'update\' operation set', async () => { - const response = await GRAPHQL_CLIENT_1.query(`mutation { +test("Test deleteOwnerCreateUpdateDeleteProtected with 'update' operation set", async () => { + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createOwnerCreateUpdateDeleteProtected(input: { content: "Hello, World!", owner: "${USERNAME1}" }) { id content owner } - }`, {}) - console.log(response); - expect(response.data.createOwnerCreateUpdateDeleteProtected.id).toBeDefined() - expect(response.data.createOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!') - expect(response.data.createOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1) + }`, + {} + ); + console.log(response); + expect(response.data.createOwnerCreateUpdateDeleteProtected.id).toBeDefined(); + expect(response.data.createOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!'); + expect(response.data.createOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1); - const response2 = await GRAPHQL_CLIENT_2.query(`mutation { + const response2 = await GRAPHQL_CLIENT_2.query( + `mutation { deleteOwnerCreateUpdateDeleteProtected( input: { id: "${response.data.createOwnerCreateUpdateDeleteProtected.id}" @@ -2606,12 +2714,15 @@ test('Test deleteOwnerCreateUpdateDeleteProtected with \'update\' operation set' content owner } - }`, {}) - console.log(response2); - expect(response2.data.deleteOwnerCreateUpdateDeleteProtected).toBeNull() - expect(response2.errors).toHaveLength(1) + }`, + {} + ); + console.log(response2); + expect(response2.data.deleteOwnerCreateUpdateDeleteProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); - const response3 = await GRAPHQL_CLIENT_1.query(`mutation { + const response3 = await GRAPHQL_CLIENT_1.query( + `mutation { deleteOwnerCreateUpdateDeleteProtected( input: { id: "${response.data.createOwnerCreateUpdateDeleteProtected.id}" @@ -2621,9 +2732,11 @@ test('Test deleteOwnerCreateUpdateDeleteProtected with \'update\' operation set' content owner } - }`, {}) - console.log(response3); - expect(response3.data.deleteOwnerCreateUpdateDeleteProtected.id).toBeDefined() - expect(response3.data.deleteOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!') - expect(response3.data.deleteOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1) -}) + }`, + {} + ); + console.log(response3); + expect(response3.data.deleteOwnerCreateUpdateDeleteProtected.id).toBeDefined(); + expect(response3.data.deleteOwnerCreateUpdateDeleteProtected.content).toEqual('Hello, World!'); + expect(response3.data.deleteOwnerCreateUpdateDeleteProtected.owner).toEqual(USERNAME1); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts index 131c416577..9fd5280aeb 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionTransformer.e2e.test.ts @@ -67,34 +67,44 @@ beforeAll(async () => { album: Album @connection (name: "AlbumPhotos", keyField: "albumId") } `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}), - ] - }) - const out = transformer.transform(validSchema); - - try { - await awsS3Client.createBucket({ - Bucket: BUCKET_NAME, - }).promise() - } catch (e) { - console.error(`Failed to create S3 bucket: ${e}`) - } - try { - console.log('Creating Stack ' + STACK_NAME) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelConnectionTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + + try { + await awsS3Client + .createBucket({ + Bucket: BUCKET_NAME, + }) + .promise(); + } catch (e) { + console.error(`Failed to create S3 bucket: ${e}`); + } + try { + console.log('Creating Stack ' + STACK_NAME); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); // Arbitrary wait to make sure everything is ready. await cf.wait(5, () => Promise.resolve()); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts index dac5b70311..b1c0486c8b 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/ModelConnectionWithKeyTransformer.e2e.test.ts @@ -1,41 +1,41 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelConnectionTransformer from 'graphql-connection-transformer' -import ModelKeyTransformer from 'graphql-key-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' -import { deploy } from '../deployNestedStacks' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelConnectionTransformer from 'graphql-connection-transformer'; +import ModelKeyTransformer from 'graphql-key-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; +import { deploy } from '../deployNestedStacks'; import emptyBucket from '../emptyBucket'; import { S3Client } from '../S3Client'; -import * as S3 from 'aws-sdk/clients/s3' +import * as S3 from 'aws-sdk/clients/s3'; import * as moment from 'moment'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cf = new CloudFormationClient('us-west-2'); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `ModelConnectionKeyTransformerTest-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-connection-key-transformer-test-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/model_connection_key_transform_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `ModelConnectionKeyTransformerTest-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-connection-key-transformer-test-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/model_connection_key_transform_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; let GRAPHQL_CLIENT = undefined; function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type AProject @model(subscriptions: null) @key(fields: ["projectId"]) @@ -118,103 +118,120 @@ beforeAll(async () => { connection: Model1 @connection(sortField: "modelOneSort") modelOneSort: Int! } - ` - - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new ModelKeyTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}), - ] - }) - const out = transformer.transform(validSchema); - - try { - await awsS3Client.createBucket({ - Bucket: BUCKET_NAME, - }).promise() - } catch (e) { - console.error(`Failed to create S3 bucket: ${e}`) - } - try { - console.log('Creating Stack ' + STACK_NAME) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - - // Arbitrary wait to make sure everything is ready. - await cf.wait(5, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - expect(finishedStack).toBeDefined() - console.log(JSON.stringify(finishedStack, null, 4)) - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + `; + + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelConnectionTransformer(), + new ModelKeyTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + + try { + await awsS3Client + .createBucket({ + Bucket: BUCKET_NAME, + }) + .promise(); + } catch (e) { + console.error(`Failed to create S3 bucket: ${e}`); + } + try { + console.log('Creating Stack ' + STACK_NAME); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + + // Arbitrary wait to make sure everything is ready. + await cf.wait(5, () => Promise.resolve()); + console.log('Successfully created stack ' + STACK_NAME); + expect(finishedStack).toBeDefined(); + console.log(JSON.stringify(finishedStack, null, 4)); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) - } -}) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); + } + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Test queries below */ test('Unnamed connection 1 way navigation, with primary @key directive 1:1', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateATeam { createATeam(input: {teamId: "T1", name: "Team 1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateAProject { createAProject(input: {projectId: "P1", name: "P1", aProjectTeamId: "T1"}) { projectId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListAProjects { listAProjects { items { @@ -227,44 +244,56 @@ test('Unnamed connection 1 way navigation, with primary @key directive 1:1', asy } } } - `, {}) - expect(queryResponse.data.listAProjects).toBeDefined() - const items = queryResponse.data.listAProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].team).toBeDefined() - expect(items[0].team.teamId).toEqual('T1') -}) + `, + {} + ); + expect(queryResponse.data.listAProjects).toBeDefined(); + const items = queryResponse.data.listAProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].team).toBeDefined(); + expect(items[0].team.teamId).toEqual('T1'); +}); test('Unnamed connection 1 way navigation, with primary @key directive 1:M', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateBProject { createBProject(input: {projectId: "P1", name: "P1"}) { projectId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateBTeam { createBTeam(input: {teamId: "T1", name: "Team 1", bProjectTeamsId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateBTeam { createBTeam(input: {teamId: "T2", name: "Team 2", bProjectTeamsId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListBProjects { listBProjects { items { @@ -279,38 +308,47 @@ test('Unnamed connection 1 way navigation, with primary @key directive 1:M', asy } } } - `, {}) - expect(queryResponse.data.listBProjects).toBeDefined() - const items = queryResponse.data.listBProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].teams).toBeDefined() - expect(items[0].teams.items).toBeDefined() - expect(items[0].teams.items.length).toEqual(2) - expect(items[0].teams.items[0].teamId).toEqual('T1') - expect(items[0].teams.items[1].teamId).toEqual('T2') -}) + `, + {} + ); + expect(queryResponse.data.listBProjects).toBeDefined(); + const items = queryResponse.data.listBProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].teams).toBeDefined(); + expect(items[0].teams.items).toBeDefined(); + expect(items[0].teams.items.length).toEqual(2); + expect(items[0].teams.items[0].teamId).toEqual('T1'); + expect(items[0].teams.items[1].teamId).toEqual('T2'); +}); test('Named connection 2 way navigation, with with custom @key fields 1:1', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateCTeam { createCTeam(input: {teamId: "T1", name: "Team 1", cTeamProjectId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateCProject { createCProject(input: {projectId: "P1", name: "P1", cProjectTeamId: "T1"}) { projectId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListCProjects { listCProjects { items { @@ -327,46 +365,58 @@ test('Named connection 2 way navigation, with with custom @key fields 1:1', asyn } } } - `, {}) - expect(queryResponse.data.listCProjects).toBeDefined() - const items = queryResponse.data.listCProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].team).toBeDefined() - expect(items[0].team.teamId).toEqual('T1') - expect(items[0].team.project).toBeDefined() - expect(items[0].team.project.projectId).toEqual('P1') -}) + `, + {} + ); + expect(queryResponse.data.listCProjects).toBeDefined(); + const items = queryResponse.data.listCProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].team).toBeDefined(); + expect(items[0].team.teamId).toEqual('T1'); + expect(items[0].team.project).toBeDefined(); + expect(items[0].team.project.projectId).toEqual('P1'); +}); test('Named connection 2 way navigation, with with custom @key fields 1:M', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateDProject { createDProject(input: {projectId: "P1", name: "P1"}) { projectId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateDTeam { createDTeam(input: {teamId: "T1", name: "Team 1", dTeamProjectId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation CreateDTeam { createDTeam(input: {teamId: "T2", name: "Team 2", dTeamProjectId: "P1"}) { teamId name } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query ListDProjects { listDProjects { items { @@ -385,24 +435,27 @@ test('Named connection 2 way navigation, with with custom @key fields 1:M', asyn } } } - `, {}) - expect(queryResponse.data.listDProjects).toBeDefined() - const items = queryResponse.data.listDProjects.items - expect(items.length).toEqual(1) - expect(items[0].projectId).toEqual('P1') - expect(items[0].teams).toBeDefined() - expect(items[0].teams.items).toBeDefined() - expect(items[0].teams.items.length).toEqual(2) - expect(items[0].teams.items[0].teamId).toEqual('T1') - expect(items[0].teams.items[0].project).toBeDefined() - expect(items[0].teams.items[0].project.projectId).toEqual('P1') - expect(items[0].teams.items[1].teamId).toEqual('T2') - expect(items[0].teams.items[1].project).toBeDefined() - expect(items[0].teams.items[1].project.projectId).toEqual('P1') -}) + `, + {} + ); + expect(queryResponse.data.listDProjects).toBeDefined(); + const items = queryResponse.data.listDProjects.items; + expect(items.length).toEqual(1); + expect(items[0].projectId).toEqual('P1'); + expect(items[0].teams).toBeDefined(); + expect(items[0].teams.items).toBeDefined(); + expect(items[0].teams.items.length).toEqual(2); + expect(items[0].teams.items[0].teamId).toEqual('T1'); + expect(items[0].teams.items[0].project).toBeDefined(); + expect(items[0].teams.items[0].project.projectId).toEqual('P1'); + expect(items[0].teams.items[1].teamId).toEqual('T2'); + expect(items[0].teams.items[1].project).toBeDefined(); + expect(items[0].teams.items[1].project.projectId).toEqual('P1'); +}); test('Unnamed connection with sortField parameter only #2100', async () => { - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation M11 { createModel1(input: {id: "M11", sort: 10, name: "M1-1"}) { id @@ -410,9 +463,12 @@ test('Unnamed connection with sortField parameter only #2100', async () => { sort } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation M12 { createModel1(input: {id: "M12", sort: 10, name: "M1-2"}) { id @@ -420,18 +476,24 @@ test('Unnamed connection with sortField parameter only #2100', async () => { sort } } - `, {}); + `, + {} + ); - await GRAPHQL_CLIENT.query(` + await GRAPHQL_CLIENT.query( + ` mutation M21 { createModel2(input: {id: "M21", modelOneSort: 10, model2ConnectionId: "M11"}) { id modelOneSort } } - `, {}); + `, + {} + ); - const queryResponse = await GRAPHQL_CLIENT.query(` + const queryResponse = await GRAPHQL_CLIENT.query( + ` query Query { getModel2(id: "M21") { id @@ -442,11 +504,13 @@ test('Unnamed connection with sortField parameter only #2100', async () => { } } } - `, {}) - expect(queryResponse.data.getModel2).toBeDefined() - const item = queryResponse.data.getModel2 - expect(item.id).toEqual('M21') - expect(item.connection).toBeDefined() - expect(item.connection.id).toEqual('M11') - expect(item.connection.sort).toEqual(10) -}) + `, + {} + ); + expect(queryResponse.data.getModel2).toBeDefined(); + const item = queryResponse.data.getModel2; + expect(item.id).toEqual('M21'); + expect(item.connection).toBeDefined(); + expect(item.connection.id).toEqual('M11'); + expect(item.connection.sort).toEqual(10); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts index 9c91a8659e..5044f8d960 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/MultiAuthModelAuthTransformer.e2e.test.ts @@ -5,8 +5,8 @@ import { ResourceConstants } from 'graphql-transformer-common'; import GraphQLTransform from 'graphql-transformer-core'; import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; import ModelAuthTransformer from 'graphql-auth-transformer'; -import KeyTransformer from 'graphql-key-transformer' -import ModelConnectionTransformer from 'graphql-connection-transformer' +import KeyTransformer from 'graphql-key-transformer'; +import ModelConnectionTransformer from 'graphql-connection-transformer'; import * as fs from 'fs'; import { CloudFormationClient } from '../CloudFormationClient'; import { Output } from 'aws-sdk/clients/cloudformation'; @@ -14,13 +14,10 @@ import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; import * as S3 from 'aws-sdk/clients/s3'; import { S3Client } from '../S3Client'; import * as path from 'path'; -import { deploy } from '../deployNestedStacks' +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; -import { - createUserPool, createUserPoolClient, deleteUserPool, - signupAndAuthenticateUser, configureAmplify -} from '../cognitoUtils'; +import { createUserPool, createUserPoolClient, deleteUserPool, signupAndAuthenticateUser, configureAmplify } from '../cognitoUtils'; import Role from 'cloudform-types/types/iam/role'; import UserPoolClient from 'cloudform-types/types/cognito/userPoolClient'; import IdentityPool from 'cloudform-types/types/cognito/identityPool'; @@ -28,13 +25,13 @@ import IdentityPoolRoleAttachment from 'cloudform-types/types/cognito/identityPo import AWS = require('aws-sdk'); // to deal with bug in cognito-identity-js -(global as any).fetch = require("node-fetch"); +(global as any).fetch = require('node-fetch'); // To overcome of the way of how AmplifyJS picks up currentUserCredentials -const anyAWS = (AWS as any); +const anyAWS = AWS as any; if (anyAWS && anyAWS.config && anyAWS.config.credentials) { - delete anyAWS.config.credentials; + delete anyAWS.config.credentials; } jest.setTimeout(2000000); @@ -71,28 +68,28 @@ const customS3Client = new S3Client(REGION); const awsS3Client = new S3({ region: REGION }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key); - return output ? output.OutputValue : null; - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } function deleteDirectory(directory: string) { - const files = fs.readdirSync(directory); - for (const file of files) { - const contentPath = path.join(directory, file); - if (fs.lstatSync(contentPath).isDirectory()) { - deleteDirectory(contentPath); - fs.rmdirSync(contentPath); - } else { - fs.unlinkSync(contentPath); - } + const files = fs.readdirSync(directory); + for (const file of files) { + const contentPath = path.join(directory, file); + if (fs.lstatSync(contentPath).isDirectory()) { + deleteDirectory(contentPath); + fs.rmdirSync(contentPath); + } else { + fs.unlinkSync(contentPath); } + } } beforeAll(async () => { - // Create a stack for the post model with auth enabled. - const validSchema = ` + // Create a stack for the post model with auth enabled. + const validSchema = ` # Allow anyone to access. This is translated into API_KEY. type PostPublic @model @auth(rules: [{ allow: public }]) { id: ID! @@ -184,620 +181,627 @@ beforeAll(async () => { } `; - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new KeyTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: 'AMAZON_COGNITO_USER_POOLS' - }, - additionalAuthenticationProviders: [ - { - authenticationType: 'API_KEY', - apiKeyConfig: { - description: 'E2E Test API Key', - apiKeyExpirationDays: 300 - } - }, + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelConnectionTransformer(), + new KeyTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [ + { + authenticationType: 'API_KEY', + apiKeyConfig: { + description: 'E2E Test API Key', + apiKeyExpirationDays: 300, + }, + }, + { + authenticationType: 'AWS_IAM', + }, + ], + }, + }), + ], + }); + + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.error(`Failed to create bucket: ${e}`); + } + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + + try { + // Clean the bucket + const out = transformer.transform(validSchema); + + const authRole = new Role({ + RoleName: AUTH_ROLE_NAME, + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Sid: '', + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + }, + ], + }, + }); + + const unauthRole = new Role({ + RoleName: UNAUTH_ROLE_NAME, + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Sid: '', + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'unauthenticated', + }, + }, + }, + ], + }, + Policies: [ + new Role.Policy({ + PolicyName: 'appsync-unauthrole-policy', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['appsync:GraphQL'], + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', { - authenticationType: 'AWS_IAM' + 'Fn::GetAtt': ['GraphQLAPI', 'ApiId'], }, + '/*', + ], ], - } - }) - ] + }, + ], + }, + ], + }, + }), + ], }); - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise(); - } catch (e) { - console.error(`Failed to create bucket: ${e}`); + const identityPool = new IdentityPool({ + IdentityPoolName: IDENTITY_POOL_NAME, + CognitoIdentityProviders: [ + { + ClientId: { + Ref: 'UserPoolClient', + }, + ProviderName: { + 'Fn::Sub': [ + 'cognito-idp.${region}.amazonaws.com/${client}', + { + region: { + Ref: 'AWS::Region', + }, + client: USER_POOL_ID, + }, + ], + }, + } as unknown, + { + ClientId: { + Ref: 'UserPoolClientWeb', + }, + ProviderName: { + 'Fn::Sub': [ + 'cognito-idp.${region}.amazonaws.com/${client}', + { + region: { + Ref: 'AWS::Region', + }, + client: USER_POOL_ID, + }, + ], + }, + } as unknown, + ], + AllowUnauthenticatedIdentities: true, + }); + + const identityPoolRoleMap = new IdentityPoolRoleAttachment({ + IdentityPoolId: ({ Ref: 'IdentityPool' } as unknown) as string, + Roles: { + unauthenticated: { 'Fn::GetAtt': ['UnauthRole', 'Arn'] }, + authenticated: { 'Fn::GetAtt': ['AuthRole', 'Arn'] }, + }, + }); + + const userPoolClientWeb = new UserPoolClient({ + ClientName: USER_POOL_CLIENTWEB_NAME, + RefreshTokenValidity: 30, + UserPoolId: USER_POOL_ID, + }); + + const userPoolClient = new UserPoolClient({ + ClientName: USER_POOL_CLIENT_NAME, + GenerateSecret: true, + RefreshTokenValidity: 30, + UserPoolId: USER_POOL_ID, + }); + + out.rootStack.Resources.IdentityPool = identityPool; + out.rootStack.Resources.IdentityPoolRoleMap = identityPoolRoleMap; + out.rootStack.Resources.UserPoolClientWeb = userPoolClientWeb; + out.rootStack.Resources.UserPoolClient = userPoolClient; + out.rootStack.Outputs.IdentityPoolId = { Value: { Ref: 'IdentityPool' } }; + out.rootStack.Outputs.IdentityPoolName = { Value: { 'Fn::GetAtt': ['IdentityPool', 'Name'] } }; + + out.rootStack.Resources.AuthRole = authRole; + out.rootStack.Outputs.AuthRoleArn = { Value: { 'Fn::GetAtt': ['AuthRole', 'Arn'] } }; + out.rootStack.Resources.UnauthRole = unauthRole; + out.rootStack.Outputs.UnauthRoleArn = { Value: { 'Fn::GetAtt': ['UnauthRole', 'Arn'] } }; + + // Since we're doing the policy here we've to remove the transformer generated artifacts from + // the generated stack. + delete out.rootStack.Resources[ResourceConstants.RESOURCES.UnauthRolePolicy]; + delete out.rootStack.Parameters.unauthRoleName; + delete out.rootStack.Resources[ResourceConstants.RESOURCES.AuthRolePolicy]; + delete out.rootStack.Parameters.authRoleName; + + for (const key of Object.keys(out.rootStack.Resources)) { + if ( + out.rootStack.Resources[key].Properties && + out.rootStack.Resources[key].Properties.Parameters && + out.rootStack.Resources[key].Properties.Parameters.unauthRoleName + ) { + delete out.rootStack.Resources[key].Properties.Parameters.unauthRoleName; + } + + if ( + out.rootStack.Resources[key].Properties && + out.rootStack.Resources[key].Properties.Parameters && + out.rootStack.Resources[key].Properties.Parameters.authRoleName + ) { + delete out.rootStack.Resources[key].Properties.Parameters.authRoleName; + } } - const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); - USER_POOL_ID = userPoolResponse.UserPool.Id; - const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); - const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; - try { - // Clean the bucket - const out = transformer.transform(validSchema); - - const authRole = new Role({ - RoleName: AUTH_ROLE_NAME, - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Sid: '', - Effect: 'Allow', - Principal: { - Federated: 'cognito-identity.amazonaws.com' - }, - Action: 'sts:AssumeRoleWithWebIdentity', - Condition: { - 'ForAnyValue:StringLike': { - 'cognito-identity.amazonaws.com:amr': 'authenticated' - } - } - } - ] - } - }); - - const unauthRole = new Role({ - RoleName: UNAUTH_ROLE_NAME, - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Sid: '', - Effect: 'Allow', - Principal: { - Federated: 'cognito-identity.amazonaws.com' - }, - Action: 'sts:AssumeRoleWithWebIdentity', - Condition: { - 'ForAnyValue:StringLike': { - 'cognito-identity.amazonaws.com:amr': 'unauthenticated' - } - } - } - ] - }, - Policies: [ - new Role.Policy({ - PolicyName: 'appsync-unauthrole-policy', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'appsync:GraphQL' - ], - Resource: [{ - 'Fn::Join': [ - '', - [ - 'arn:aws:appsync:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':apis/', - { - 'Fn::GetAtt': [ - 'GraphQLAPI', - 'ApiId' - ] - }, - '/*', - ], - ], - }], - }], - }, - }), - ] - }); - - const identityPool = new IdentityPool({ - IdentityPoolName: IDENTITY_POOL_NAME, - CognitoIdentityProviders: [ - { - ClientId: { - Ref: 'UserPoolClient' - }, - ProviderName: { - 'Fn::Sub': [ - 'cognito-idp.${region}.amazonaws.com/${client}', - { - 'region': { - Ref: 'AWS::Region' - }, - 'client': USER_POOL_ID - } - ] - } - } as unknown, - { - ClientId: { - Ref: 'UserPoolClientWeb' - }, - ProviderName: { - 'Fn::Sub': [ - 'cognito-idp.${region}.amazonaws.com/${client}', - { - 'region': { - Ref: 'AWS::Region' - }, - 'client': USER_POOL_ID - } - ] - } - } as unknown, - ], - AllowUnauthenticatedIdentities: true - }); - - const identityPoolRoleMap = new IdentityPoolRoleAttachment({ - IdentityPoolId: { Ref: 'IdentityPool' } as unknown as string, - Roles: { - 'unauthenticated': { 'Fn::GetAtt': [ 'UnauthRole', 'Arn' ] }, - 'authenticated': { 'Fn::GetAtt': [ 'AuthRole', 'Arn' ] } - } - }); - - const userPoolClientWeb = new UserPoolClient({ - ClientName: USER_POOL_CLIENTWEB_NAME, - RefreshTokenValidity: 30, - UserPoolId: USER_POOL_ID - }); - - const userPoolClient = new UserPoolClient({ - ClientName: USER_POOL_CLIENT_NAME, - GenerateSecret: true, - RefreshTokenValidity: 30, - UserPoolId: USER_POOL_ID - }); - - out.rootStack.Resources.IdentityPool = identityPool; - out.rootStack.Resources.IdentityPoolRoleMap = identityPoolRoleMap; - out.rootStack.Resources.UserPoolClientWeb = userPoolClientWeb; - out.rootStack.Resources.UserPoolClient = userPoolClient; - out.rootStack.Outputs.IdentityPoolId = { Value: { Ref: 'IdentityPool' } }; - out.rootStack.Outputs.IdentityPoolName = { Value: { 'Fn::GetAtt': [ 'IdentityPool', 'Name' ] } }; - - out.rootStack.Resources.AuthRole = authRole; - out.rootStack.Outputs.AuthRoleArn = { Value: { 'Fn::GetAtt': [ 'AuthRole', 'Arn' ] } }; - out.rootStack.Resources.UnauthRole = unauthRole; - out.rootStack.Outputs.UnauthRoleArn = { Value: { 'Fn::GetAtt': [ 'UnauthRole', 'Arn' ] } }; - - // Since we're doing the policy here we've to remove the transformer generated artifacts from - // the generated stack. - delete out.rootStack.Resources[ResourceConstants.RESOURCES.UnauthRolePolicy]; - delete out.rootStack.Parameters.unauthRoleName; - delete out.rootStack.Resources[ResourceConstants.RESOURCES.AuthRolePolicy]; - delete out.rootStack.Parameters.authRoleName; - - for (const key of Object.keys(out.rootStack.Resources)) { - if (out.rootStack.Resources[key].Properties && - out.rootStack.Resources[key].Properties.Parameters && - out.rootStack.Resources[key].Properties.Parameters.unauthRoleName) { - delete out.rootStack.Resources[key].Properties.Parameters.unauthRoleName; - } + for (const stackKey of Object.keys(out.stacks)) { + const stack = out.stacks[stackKey]; - if (out.rootStack.Resources[key].Properties && - out.rootStack.Resources[key].Properties.Parameters && - out.rootStack.Resources[key].Properties.Parameters.authRoleName) { - delete out.rootStack.Resources[key].Properties.Parameters.authRoleName; - } + for (const key of Object.keys(stack.Resources)) { + if (stack.Parameters && stack.Parameters.unauthRoleName) { + delete stack.Parameters.unauthRoleName; + } + if (stack.Parameters && stack.Parameters.authRoleName) { + delete stack.Parameters.authRoleName; + } + if ( + stack.Resources[key].Properties && + stack.Resources[key].Properties.Parameters && + stack.Resources[key].Properties.Parameters.unauthRoleName + ) { + delete stack.Resources[key].Properties.Parameters.unauthRoleName; } + if ( + stack.Resources[key].Properties && + stack.Resources[key].Properties.Parameters && + stack.Resources[key].Properties.Parameters.authRoleName + ) { + delete stack.Resources[key].Properties.Parameters.authRoleName; + } + } + } - for (const stackKey of Object.keys(out.stacks)) { - const stack = out.stacks[stackKey]; + const params = { + CreateAPIKey: '1', + AuthCognitoUserPoolId: USER_POOL_ID, + }; + + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + params, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + const apiKey = getApiKey(finishedStack.Outputs); + console.log(`API KEY: ${apiKey}`); + expect(apiKey).toBeTruthy(); + + const getIdentityPoolId = outputValueSelector('IdentityPoolId'); + const identityPoolId = getIdentityPoolId(finishedStack.Outputs); + expect(identityPoolId).toBeTruthy(); + console.log(`Identity Pool Id: ${identityPoolId}`); + + console.log(`User pool Id: ${USER_POOL_ID}`); + console.log(`User pool ClientId: ${userPoolClientId}`); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + expect(USER_POOL_ID).toBeTruthy(); + expect(userPoolClientId).toBeTruthy(); + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId, identityPoolId); + + const unauthCredentials = await Auth.currentCredentials(); + + IAM_UNAUTHCLIENT = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: REGION, + auth: { + type: AUTH_TYPE.AWS_IAM, + credentials: { + accessKeyId: unauthCredentials.accessKeyId, + secretAccessKey: unauthCredentials.secretAccessKey, + }, + }, + offlineConfig: { + keyPrefix: 'iam', + }, + disableOffline: true, + }); - for (const key of Object.keys(stack.Resources)) { - if (stack.Parameters && - stack.Parameters.unauthRoleName) { - delete stack.Parameters.unauthRoleName; - } - if (stack.Parameters && - stack.Parameters.authRoleName) { - delete stack.Parameters.authRoleName; - } - if (stack.Resources[key].Properties && - stack.Resources[key].Properties.Parameters && - stack.Resources[key].Properties.Parameters.unauthRoleName) { - delete stack.Resources[key].Properties.Parameters.unauthRoleName; - } - if (stack.Resources[key].Properties && - stack.Resources[key].Properties.Parameters && - stack.Resources[key].Properties.Parameters.authRoleName) { - delete stack.Resources[key].Properties.Parameters.authRoleName; - } - } - } + const authRes = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + const idToken = authRes.getIdToken().getJwtToken(); + + USER_POOL_AUTH_CLIENT = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: REGION, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: () => idToken, + }, + offlineConfig: { + keyPrefix: 'userPools', + }, + disableOffline: true, + }); - const params = { - CreateAPIKey: '1', - AuthCognitoUserPoolId: USER_POOL_ID - }; - - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, params, - LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, BUILD_TIMESTAMP - ); - expect(finishedStack).toBeDefined(); - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - const apiKey = getApiKey(finishedStack.Outputs); - console.log(`API KEY: ${apiKey}`); - expect(apiKey).toBeTruthy(); - - const getIdentityPoolId = outputValueSelector('IdentityPoolId') - const identityPoolId = getIdentityPoolId(finishedStack.Outputs); - expect(identityPoolId).toBeTruthy(); - console.log(`Identity Pool Id: ${identityPoolId}`); - - console.log(`User pool Id: ${USER_POOL_ID}`); - console.log(`User pool ClientId: ${userPoolClientId}`); - - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy(); - expect(USER_POOL_ID).toBeTruthy(); - expect(userPoolClientId).toBeTruthy(); - - // Configure Amplify, create users, and sign in. - configureAmplify(USER_POOL_ID, userPoolClientId, identityPoolId); - - const unauthCredentials = await Auth.currentCredentials(); - - IAM_UNAUTHCLIENT = new AWSAppSyncClient({ - url: GRAPHQL_ENDPOINT, - region: REGION, - auth: { - type: AUTH_TYPE.AWS_IAM, - credentials: { - accessKeyId: unauthCredentials.accessKeyId, - secretAccessKey: unauthCredentials.secretAccessKey - } - }, - offlineConfig: { - keyPrefix: 'iam' - }, - disableOffline: true, - }); - - const authRes = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); - const idToken = authRes.getIdToken().getJwtToken(); - - USER_POOL_AUTH_CLIENT = new AWSAppSyncClient({ - url: GRAPHQL_ENDPOINT, - region: REGION, - auth: { - type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, - jwtToken: () => idToken - }, - offlineConfig: { - keyPrefix: 'userPools' - }, - disableOffline: true, - }); - - APIKEY_GRAPHQL_CLIENT = new AWSAppSyncClient({ - url: GRAPHQL_ENDPOINT, - region: REGION, - auth: { - type: AUTH_TYPE.API_KEY, - apiKey: apiKey - }, - offlineConfig: { - keyPrefix: 'apikey' - }, - disableOffline: true, - }); + APIKEY_GRAPHQL_CLIENT = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: REGION, + auth: { + type: AUTH_TYPE.API_KEY, + apiKey: apiKey, + }, + offlineConfig: { + keyPrefix: 'apikey', + }, + disableOffline: true, + }); - // Wait for any propagation to avoid random - // "The security token included in the request is invalid" errors - await new Promise((res) => setTimeout(() => res(), 5000)); - } catch (e) { - console.error(e); - expect(true).toEqual(false); - } + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME); - await cf.deleteStack(STACK_NAME); - await deleteUserPool(cognitoClient, USER_POOL_ID); - await cf.waitForStack(STACK_NAME); - console.log('Successfully deleted stack ' + STACK_NAME); - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true); - console.log('Successfully deleted stack ' + STACK_NAME); - } else { - console.error(e); - expect(true).toEqual(false); - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`); + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await deleteUserPool(cognitoClient, USER_POOL_ID); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } }); /** * Test queries below */ test(`Test 'public' authStrategy`, async () => { - try { - const createMutation = gql(`mutation { + try { + const createMutation = gql(`mutation { createPostPublic(input: { title: "Hello, World!" }) { id title } }`); - const getQuery = gql(`query ($id: ID!) { + const getQuery = gql(`query ($id: ID!) { getPostPublic(id: $id) { id title } }`); - const response = await APIKEY_GRAPHQL_CLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); - expect(response.data.createPostPublic.id).toBeDefined(); - expect(response.data.createPostPublic.title).toEqual('Hello, World!'); - - const postId = response.data.createPostPublic.id; - - // Authenticate User Pools user must fail - try { - await USER_POOL_AUTH_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); - - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublic on type Query'); - } + const response = await APIKEY_GRAPHQL_CLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); + expect(response.data.createPostPublic.id).toBeDefined(); + expect(response.data.createPostPublic.title).toEqual('Hello, World!'); - // IAM with unauth role must fail - try { - await IAM_UNAUTHCLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); + const postId = response.data.createPostPublic.id; - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublic on type Query'); - } + // Authenticate User Pools user must fail + try { + await USER_POOL_AUTH_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); + } catch (e) { + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublic on type Query'); + } + // IAM with unauth role must fail + try { + await IAM_UNAUTHCLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); } catch (e) { - console.error(e); - expect(true).toEqual(false); + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublic on type Query'); } + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); test(`Test 'public' provider: 'iam' authStrategy`, async () => { - try { - const createMutation = gql(`mutation { + try { + const createMutation = gql(`mutation { createPostPublicIAM(input: { title: "Hello, World!" }) { id title } }`); - const getQuery = gql(`query ($id: ID!) { + const getQuery = gql(`query ($id: ID!) { getPostPublicIAM(id: $id) { id title } }`); - const response = await IAM_UNAUTHCLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); - expect(response.data.createPostPublicIAM.id).toBeDefined(); - expect(response.data.createPostPublicIAM.title).toEqual('Hello, World!'); - - const postId = response.data.createPostPublicIAM.id; - - // Authenticate User Pools user must fail - try { - await USER_POOL_AUTH_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); + const response = await IAM_UNAUTHCLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); + expect(response.data.createPostPublicIAM.id).toBeDefined(); + expect(response.data.createPostPublicIAM.title).toEqual('Hello, World!'); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublicIAM on type Query'); - } + const postId = response.data.createPostPublicIAM.id; - // API Key must fail - try { - await APIKEY_GRAPHQL_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); - - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublicIAM on type Query'); - } + // Authenticate User Pools user must fail + try { + await USER_POOL_AUTH_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); + } catch (e) { + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublicIAM on type Query'); + } + // API Key must fail + try { + await APIKEY_GRAPHQL_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); } catch (e) { - console.error(e); - expect(true).toEqual(false); + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPublicIAM on type Query'); } + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); test(`Test 'private' authStrategy`, async () => { - try { - const createMutation = gql(`mutation { + try { + const createMutation = gql(`mutation { createPostPrivate(input: { title: "Hello, World!" }) { id title } }`); - const getQuery = gql(`query ($id: ID!) { + const getQuery = gql(`query ($id: ID!) { getPostPrivate(id: $id) { id title } }`); - const response = await USER_POOL_AUTH_CLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); - expect(response.data.createPostPrivate.id).toBeDefined(); - expect(response.data.createPostPrivate.title).toEqual('Hello, World!'); - - const postId = response.data.createPostPrivate.id; - - // Authenticate API Key fail - try { - await APIKEY_GRAPHQL_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); - - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivate on type Query'); - } + const response = await USER_POOL_AUTH_CLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); + expect(response.data.createPostPrivate.id).toBeDefined(); + expect(response.data.createPostPrivate.title).toEqual('Hello, World!'); - // IAM with unauth role must fail - try { - await IAM_UNAUTHCLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); + const postId = response.data.createPostPrivate.id; - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivate on type Query'); - } + // Authenticate API Key fail + try { + await APIKEY_GRAPHQL_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); + } catch (e) { + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivate on type Query'); + } + // IAM with unauth role must fail + try { + await IAM_UNAUTHCLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); } catch (e) { - console.error(e); - expect(true).toEqual(false); + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivate on type Query'); } + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); test(`Test 'private' provider: 'iam' authStrategy`, async () => { - // This test reuses the unauth role, but any IAM credentials would work - // in real world scenarios, we've to see if provider override works. - try { - const createMutation = gql(`mutation { + // This test reuses the unauth role, but any IAM credentials would work + // in real world scenarios, we've to see if provider override works. + try { + const createMutation = gql(`mutation { createPostPrivateIAM(input: { title: "Hello, World!" }) { id title } }`); - const getQuery = gql(`query ($id: ID!) { + const getQuery = gql(`query ($id: ID!) { getPostPrivateIAM(id: $id) { id title } }`); - const response = await IAM_UNAUTHCLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); - expect(response.data.createPostPrivateIAM.id).toBeDefined(); - expect(response.data.createPostPrivateIAM.title).toEqual('Hello, World!'); - - const postId = response.data.createPostPrivateIAM.id; - - // Authenticate User Pools user must fail - try { - await USER_POOL_AUTH_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); + const response = await IAM_UNAUTHCLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); + expect(response.data.createPostPrivateIAM.id).toBeDefined(); + expect(response.data.createPostPrivateIAM.title).toEqual('Hello, World!'); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivateIAM on type Query'); - } + const postId = response.data.createPostPrivateIAM.id; - // API Key must fail - try { - await APIKEY_GRAPHQL_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postId - } - }); - - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivateIAM on type Query'); - } + // Authenticate User Pools user must fail + try { + await USER_POOL_AUTH_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); + } catch (e) { + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivateIAM on type Query'); + } + // API Key must fail + try { + await APIKEY_GRAPHQL_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postId, + }, + }); + + expect(true).toBe(false); } catch (e) { - console.error(e); - expect(true).toEqual(false); + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostPrivateIAM on type Query'); } + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); test(`Test 'private' provider: 'iam' authStrategy`, async () => { - // This test reuses the unauth role, but any IAM credentials would work - // in real world scenarios, we've to see if provider override works. + // This test reuses the unauth role, but any IAM credentials would work + // in real world scenarios, we've to see if provider override works. - // - Create UserPool - Verify owner - // - Create IAM - Verify owner (blank) - // - Get UserPool owner - Verify success - // - Get UserPool non-owner - Verify deny - // - Get IAM - Verify deny - // - Get API Key - Verify deny + // - Create UserPool - Verify owner + // - Create IAM - Verify owner (blank) + // - Get UserPool owner - Verify success + // - Get UserPool non-owner - Verify deny + // - Get IAM - Verify deny + // - Get API Key - Verify deny - try { - const createMutation = gql(`mutation { + try { + const createMutation = gql(`mutation { createPostOwnerIAM(input: { title: "Hello, World!" }) { id title @@ -805,7 +809,7 @@ test(`Test 'private' provider: 'iam' authStrategy`, async () => { } }`); - const getQuery = gql(`query ($id: ID!) { + const getQuery = gql(`query ($id: ID!) { getPostOwnerIAM(id: $id) { id title @@ -813,99 +817,98 @@ test(`Test 'private' provider: 'iam' authStrategy`, async () => { } }`); - const response = await USER_POOL_AUTH_CLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); - expect(response.data.createPostOwnerIAM.id).toBeDefined(); - expect(response.data.createPostOwnerIAM.title).toEqual('Hello, World!'); - expect(response.data.createPostOwnerIAM.owner).toEqual(USERNAME1); - - const postIdOwner = response.data.createPostOwnerIAM.id; - - const responseIAM = await IAM_UNAUTHCLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); - expect(responseIAM.data.createPostOwnerIAM.id).toBeDefined(); - expect(responseIAM.data.createPostOwnerIAM.title).toEqual('Hello, World!'); - expect(responseIAM.data.createPostOwnerIAM.owner).toBeNull(); - - const postIdIAM = responseIAM.data.createPostOwnerIAM.id; - - const responseGetUserPool = await USER_POOL_AUTH_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postIdOwner - } - }); - - expect(responseGetUserPool.data.getPostOwnerIAM.id).toBeDefined(); - expect(responseGetUserPool.data.getPostOwnerIAM.title).toEqual('Hello, World!'); - expect(responseGetUserPool.data.getPostOwnerIAM.owner).toEqual(USERNAME1); - - try { - await USER_POOL_AUTH_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postIdIAM - } - }); + const response = await USER_POOL_AUTH_CLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); + expect(response.data.createPostOwnerIAM.id).toBeDefined(); + expect(response.data.createPostOwnerIAM.title).toEqual('Hello, World!'); + expect(response.data.createPostOwnerIAM.owner).toEqual(USERNAME1); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostOwnerIAM on type Query'); - } + const postIdOwner = response.data.createPostOwnerIAM.id; - // IAM user must fail - try { - await IAM_UNAUTHCLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postIdOwner - } - }); + const responseIAM = await IAM_UNAUTHCLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); + expect(responseIAM.data.createPostOwnerIAM.id).toBeDefined(); + expect(responseIAM.data.createPostOwnerIAM.title).toEqual('Hello, World!'); + expect(responseIAM.data.createPostOwnerIAM.owner).toBeNull(); + + const postIdIAM = responseIAM.data.createPostOwnerIAM.id; + + const responseGetUserPool = await USER_POOL_AUTH_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postIdOwner, + }, + }); - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostOwnerIAM on type Query'); - } + expect(responseGetUserPool.data.getPostOwnerIAM.id).toBeDefined(); + expect(responseGetUserPool.data.getPostOwnerIAM.title).toEqual('Hello, World!'); + expect(responseGetUserPool.data.getPostOwnerIAM.owner).toEqual(USERNAME1); - // API Key must fail - try { - await APIKEY_GRAPHQL_CLIENT.query({ - query: getQuery, - variables: { - id: postIdOwner - } - }); + try { + await USER_POOL_AUTH_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postIdIAM, + }, + }); + + expect(true).toBe(false); + } catch (e) { + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostOwnerIAM on type Query'); + } - expect(true).toBe(false); - } catch (e) { - expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostOwnerIAM on type Query'); - } + // IAM user must fail + try { + await IAM_UNAUTHCLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postIdOwner, + }, + }); + + expect(true).toBe(false); + } catch (e) { + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostOwnerIAM on type Query'); + } + // API Key must fail + try { + await APIKEY_GRAPHQL_CLIENT.query({ + query: getQuery, + variables: { + id: postIdOwner, + }, + }); + + expect(true).toBe(false); } catch (e) { - console.error(e); - expect(true).toEqual(false); + expect(e.message).toMatch('GraphQL error: Not Authorized to access getPostOwnerIAM on type Query'); } + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); describe(`Test IAM protected field operations`, () => { - // This test reuses the unauth role, but any IAM credentials would work - // in real world scenarios, we've to see if provider override works. + // This test reuses the unauth role, but any IAM credentials would work + // in real world scenarios, we've to see if provider override works. - const createMutation = gql(`mutation { + const createMutation = gql(`mutation { createPostSecretFieldIAM(input: { title: "Hello, World!" }) { id title } }`); - const createMutationWithSecret = gql(`mutation { + const createMutationWithSecret = gql(`mutation { createPostSecretFieldIAM(input: { title: "Hello, World!", secret: "42" }) { id title @@ -913,14 +916,14 @@ describe(`Test IAM protected field operations`, () => { } }`); - const getQuery = gql(`query ($id: ID!) { + const getQuery = gql(`query ($id: ID!) { getPostSecretFieldIAM(id: $id) { id title } }`); - const getQueryWithSecret = gql(`query ($id: ID!) { + const getQueryWithSecret = gql(`query ($id: ID!) { getPostSecretFieldIAM(id: $id) { id title @@ -928,83 +931,87 @@ describe(`Test IAM protected field operations`, () => { } }`); - let postIdNoSecret = ''; - let postIdSecret = ''; - - beforeAll(async () => { - try { - // - Create UserPool - no secret - Success - const response = await USER_POOL_AUTH_CLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); - - postIdNoSecret = response.data.createPostSecretFieldIAM.id; - - // - Create IAM - with secret - Success - const responseIAMSecret = await IAM_UNAUTHCLIENT.mutate({ - mutation: createMutationWithSecret, - fetchPolicy: 'no-cache', - }); - - postIdSecret = responseIAMSecret.data.createPostSecretFieldIAM.id; - } catch (e) { - console.error(e); - expect(true).toEqual(false); - } - }); + let postIdNoSecret = ''; + let postIdSecret = ''; - it ('Get UserPool - Succeed', async () => { - const responseGetUserPool = await USER_POOL_AUTH_CLIENT.query({ - query: getQuery, - fetchPolicy: 'no-cache', - variables: { - id: postIdNoSecret - } - }); - expect(responseGetUserPool.data.getPostSecretFieldIAM.id).toBeDefined(); - expect(responseGetUserPool.data.getPostSecretFieldIAM.title).toEqual('Hello, World!'); - }); + beforeAll(async () => { + try { + // - Create UserPool - no secret - Success + const response = await USER_POOL_AUTH_CLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); - it ('Get UserPool with secret - Fail', async () => { - expect.assertions(1); - await expect (USER_POOL_AUTH_CLIENT.query({ - query: getQueryWithSecret, - fetchPolicy: 'no-cache', - variables: { - id: postIdSecret - } - })).rejects.toThrow('GraphQL error: Not Authorized to access secret on type PostSecretFieldIAM'); - }); + postIdNoSecret = response.data.createPostSecretFieldIAM.id; - it ('Get IAM with secret - Fail (only create and update)', async () => { - expect.assertions(1); - await expect (IAM_UNAUTHCLIENT.query({ - query: getQueryWithSecret, - fetchPolicy: 'no-cache', - variables: { - id: postIdSecret - } - })).rejects.toThrow('GraphQL error: Not Authorized to access getPostSecretFieldIAM on type Query'); + // - Create IAM - with secret - Success + const responseIAMSecret = await IAM_UNAUTHCLIENT.mutate({ + mutation: createMutationWithSecret, + fetchPolicy: 'no-cache', + }); + + postIdSecret = responseIAMSecret.data.createPostSecretFieldIAM.id; + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } + }); + + it('Get UserPool - Succeed', async () => { + const responseGetUserPool = await USER_POOL_AUTH_CLIENT.query({ + query: getQuery, + fetchPolicy: 'no-cache', + variables: { + id: postIdNoSecret, + }, }); + expect(responseGetUserPool.data.getPostSecretFieldIAM.id).toBeDefined(); + expect(responseGetUserPool.data.getPostSecretFieldIAM.title).toEqual('Hello, World!'); + }); + + it('Get UserPool with secret - Fail', async () => { + expect.assertions(1); + await expect( + USER_POOL_AUTH_CLIENT.query({ + query: getQueryWithSecret, + fetchPolicy: 'no-cache', + variables: { + id: postIdSecret, + }, + }) + ).rejects.toThrow('GraphQL error: Not Authorized to access secret on type PostSecretFieldIAM'); + }); + + it('Get IAM with secret - Fail (only create and update)', async () => { + expect.assertions(1); + await expect( + IAM_UNAUTHCLIENT.query({ + query: getQueryWithSecret, + fetchPolicy: 'no-cache', + variables: { + id: postIdSecret, + }, + }) + ).rejects.toThrow('GraphQL error: Not Authorized to access getPostSecretFieldIAM on type Query'); + }); }); describe(`Connection tests with @auth on type`, () => { - const createPostMutation = gql(`mutation { + const createPostMutation = gql(`mutation { createPostConnection(input: { title: "Hello, World!" }) { id title } }`); - const createCommentMutation = gql(`mutation ( $postId: ID! ) { + const createCommentMutation = gql(`mutation ( $postId: ID! ) { createCommentConnection(input: { content: "Comment", commentConnectionPostId: $postId }) { id content } }`); - const getPostQuery = gql(`query ( $postId: ID! ) { + const getPostQuery = gql(`query ( $postId: ID! ) { getPostConnection ( id: $postId ) { id title @@ -1012,7 +1019,7 @@ describe(`Connection tests with @auth on type`, () => { } `); - const getPostQueryWithComments = gql(`query ( $postId: ID! ) { + const getPostQueryWithComments = gql(`query ( $postId: ID! ) { getPostConnection ( id: $postId ) { id title @@ -1026,7 +1033,7 @@ describe(`Connection tests with @auth on type`, () => { } `); - const getCommentQuery = gql(`query ( $commentId: ID! ) { + const getCommentQuery = gql(`query ( $commentId: ID! ) { getCommentConnection ( id: $commentId ) { id content @@ -1034,7 +1041,7 @@ describe(`Connection tests with @auth on type`, () => { } `); - const getCommentWithPostQuery = gql(`query ( $commentId: ID! ) { + const getCommentWithPostQuery = gql(`query ( $commentId: ID! ) { getCommentConnection ( id: $commentId ) { id content @@ -1046,128 +1053,138 @@ describe(`Connection tests with @auth on type`, () => { } `); - let postId = ''; - let commentId = ''; - - beforeAll(async () => { - try { - // Add a comment with ApiKey - Succeed - const response = await APIKEY_GRAPHQL_CLIENT.mutate({ - mutation: createPostMutation, - fetchPolicy: 'no-cache', - }); - - postId = response.data.createPostConnection.id; - - // Add a comment with UserPool - Succeed - const commentResponse = await USER_POOL_AUTH_CLIENT.mutate({ - mutation: createCommentMutation, - fetchPolicy: 'no-cache', - variables: { - postId - } - }); - - commentId = commentResponse.data.createCommentConnection.id; - } catch (e) { - console.error(e); - expect(true).toEqual(false); - } - }); - - it ('Create a Post with UserPool - Fail', async () => { - expect.assertions(1); - await expect (USER_POOL_AUTH_CLIENT.mutate({ - mutation: createPostMutation, - fetchPolicy: 'no-cache' - })).rejects.toThrow('GraphQL error: Not Authorized to access createPostConnection on type Mutation'); - }); - - it ('Add a comment with ApiKey - Fail', async () => { - expect.assertions(1); - await expect (APIKEY_GRAPHQL_CLIENT.mutate({ - mutation: createCommentMutation, - fetchPolicy: 'no-cache', - variables: { - postId - } - })).rejects.toThrow('Not Authorized to access createCommentConnection on type Mutation'); - }); - - it ('Get Post with ApiKey - Succeed', async () => { - const responseGetPost = await APIKEY_GRAPHQL_CLIENT.query({ - query: getPostQuery, - fetchPolicy: 'no-cache', - variables: { - postId - } - }); - expect(responseGetPost.data.getPostConnection.id).toEqual(postId); - expect(responseGetPost.data.getPostConnection.title).toEqual('Hello, World!'); - }); - - it ('Get Post+Comments with ApiKey - Fail', async () => { - expect.assertions(1); - await expect (APIKEY_GRAPHQL_CLIENT.query({ - query: getPostQueryWithComments, - fetchPolicy: 'no-cache', - variables: { - postId - } - })).rejects.toThrow('Not Authorized to access items on type ModelCommentConnectionConnection'); - }); - - it ('Get Post with UserPool - Fail', async () => { - expect.assertions(1); - await expect (USER_POOL_AUTH_CLIENT.query({ - query: getPostQuery, - fetchPolicy: 'no-cache', - variables: { - postId - } - })).rejects.toThrow('Not Authorized to access getPostConnection on type Query'); - }); + let postId = ''; + let commentId = ''; - it ('Get Comment with UserPool - Succeed', async () => { - const responseGetComment = await USER_POOL_AUTH_CLIENT.query({ - query: getCommentQuery, - fetchPolicy: 'no-cache', - variables: { - commentId - } - }); - expect(responseGetComment.data.getCommentConnection.id).toEqual(commentId); - expect(responseGetComment.data.getCommentConnection.content).toEqual('Comment'); + beforeAll(async () => { + try { + // Add a comment with ApiKey - Succeed + const response = await APIKEY_GRAPHQL_CLIENT.mutate({ + mutation: createPostMutation, + fetchPolicy: 'no-cache', + }); + + postId = response.data.createPostConnection.id; + + // Add a comment with UserPool - Succeed + const commentResponse = await USER_POOL_AUTH_CLIENT.mutate({ + mutation: createCommentMutation, + fetchPolicy: 'no-cache', + variables: { + postId, + }, + }); + + commentId = commentResponse.data.createCommentConnection.id; + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } + }); + + it('Create a Post with UserPool - Fail', async () => { + expect.assertions(1); + await expect( + USER_POOL_AUTH_CLIENT.mutate({ + mutation: createPostMutation, + fetchPolicy: 'no-cache', + }) + ).rejects.toThrow('GraphQL error: Not Authorized to access createPostConnection on type Mutation'); + }); + + it('Add a comment with ApiKey - Fail', async () => { + expect.assertions(1); + await expect( + APIKEY_GRAPHQL_CLIENT.mutate({ + mutation: createCommentMutation, + fetchPolicy: 'no-cache', + variables: { + postId, + }, + }) + ).rejects.toThrow('Not Authorized to access createCommentConnection on type Mutation'); + }); + + it('Get Post with ApiKey - Succeed', async () => { + const responseGetPost = await APIKEY_GRAPHQL_CLIENT.query({ + query: getPostQuery, + fetchPolicy: 'no-cache', + variables: { + postId, + }, }); - - it ('Get Comment with ApiKey - Fail', async () => { - expect.assertions(1); - await expect (APIKEY_GRAPHQL_CLIENT.query({ - query: getCommentQuery, - fetchPolicy: 'no-cache', - variables: { - commentId - } - })).rejects.toThrow('Not Authorized to access getCommentConnection on type Query'); + expect(responseGetPost.data.getPostConnection.id).toEqual(postId); + expect(responseGetPost.data.getPostConnection.title).toEqual('Hello, World!'); + }); + + it('Get Post+Comments with ApiKey - Fail', async () => { + expect.assertions(1); + await expect( + APIKEY_GRAPHQL_CLIENT.query({ + query: getPostQueryWithComments, + fetchPolicy: 'no-cache', + variables: { + postId, + }, + }) + ).rejects.toThrow('Not Authorized to access items on type ModelCommentConnectionConnection'); + }); + + it('Get Post with UserPool - Fail', async () => { + expect.assertions(1); + await expect( + USER_POOL_AUTH_CLIENT.query({ + query: getPostQuery, + fetchPolicy: 'no-cache', + variables: { + postId, + }, + }) + ).rejects.toThrow('Not Authorized to access getPostConnection on type Query'); + }); + + it('Get Comment with UserPool - Succeed', async () => { + const responseGetComment = await USER_POOL_AUTH_CLIENT.query({ + query: getCommentQuery, + fetchPolicy: 'no-cache', + variables: { + commentId, + }, }); - - it ('Get Comment with Post with UserPool - Succeed, but null for Post field', async () => { - const responseGetComment = await USER_POOL_AUTH_CLIENT.query({ - query: getCommentWithPostQuery, - errorPolicy: 'all', - fetchPolicy: 'no-cache', - variables: { - commentId - } - }); - expect(responseGetComment.data.getCommentConnection.id).toEqual(commentId); - expect(responseGetComment.data.getCommentConnection.content).toEqual('Comment'); - expect(responseGetComment.data.getCommentConnection.post).toBeNull(); + expect(responseGetComment.data.getCommentConnection.id).toEqual(commentId); + expect(responseGetComment.data.getCommentConnection.content).toEqual('Comment'); + }); + + it('Get Comment with ApiKey - Fail', async () => { + expect.assertions(1); + await expect( + APIKEY_GRAPHQL_CLIENT.query({ + query: getCommentQuery, + fetchPolicy: 'no-cache', + variables: { + commentId, + }, + }) + ).rejects.toThrow('Not Authorized to access getCommentConnection on type Query'); + }); + + it('Get Comment with Post with UserPool - Succeed, but null for Post field', async () => { + const responseGetComment = await USER_POOL_AUTH_CLIENT.query({ + query: getCommentWithPostQuery, + errorPolicy: 'all', + fetchPolicy: 'no-cache', + variables: { + commentId, + }, }); + expect(responseGetComment.data.getCommentConnection.id).toEqual(commentId); + expect(responseGetComment.data.getCommentConnection.content).toEqual('Comment'); + expect(responseGetComment.data.getCommentConnection.post).toBeNull(); + }); }); describe(`IAM Tests`, () => { - const createMutation = gql(`mutation { + const createMutation = gql(`mutation { createPostIAMWithKeys(input: { title: "Hello, World!", type: "Post", date: "2019-01-01T00:00:00Z" }) { id title @@ -1176,7 +1193,7 @@ describe(`IAM Tests`, () => { } }`); - const getPostIAMWithKeysByDate = gql(`query { + const getPostIAMWithKeysByDate = gql(`query { getPostIAMWithKeysByDate(type: "Post") { items { id @@ -1187,34 +1204,34 @@ describe(`IAM Tests`, () => { } }`); - let postId = ''; + let postId = ''; - beforeAll(async () => { - try { - // - Create API Key - Success - const response = await APIKEY_GRAPHQL_CLIENT.mutate({ - mutation: createMutation, - fetchPolicy: 'no-cache', - }); + beforeAll(async () => { + try { + // - Create API Key - Success + const response = await APIKEY_GRAPHQL_CLIENT.mutate({ + mutation: createMutation, + fetchPolicy: 'no-cache', + }); - postId = response.data.createPostIAMWithKeys.id; - } catch (e) { - console.error(e); - expect(true).toEqual(false); - } - }); + postId = response.data.createPostIAMWithKeys.id; + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } + }); - it ('Execute @key query - Succeed', async () => { - const response = await IAM_UNAUTHCLIENT.query({ - query: getPostIAMWithKeysByDate, - fetchPolicy: 'no-cache', - }); - expect(response.data.getPostIAMWithKeysByDate.items).toBeDefined(); - expect(response.data.getPostIAMWithKeysByDate.items.length).toEqual(1); - const post = response.data.getPostIAMWithKeysByDate.items[0]; - expect(post.id).toEqual(postId); - expect(post.title).toEqual('Hello, World!'); - expect(post.type).toEqual('Post'); - expect(post.date).toEqual('2019-01-01T00:00:00Z'); + it('Execute @key query - Succeed', async () => { + const response = await IAM_UNAUTHCLIENT.query({ + query: getPostIAMWithKeysByDate, + fetchPolicy: 'no-cache', }); + expect(response.data.getPostIAMWithKeysByDate.items).toBeDefined(); + expect(response.data.getPostIAMWithKeysByDate.items.length).toEqual(1); + const post = response.data.getPostIAMWithKeysByDate.items[0]; + expect(post.id).toEqual(postId); + expect(post.title).toEqual('Hello, World!'); + expect(post.type).toEqual('Post'); + expect(post.date).toEqual('2019-01-01T00:00:00Z'); + }); }); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts index 41f4b4f0e8..e8f2275330 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NestedStacksTest.e2e.test.ts @@ -1,9 +1,14 @@ import { - ObjectTypeDefinitionNode, parse, FieldDefinitionNode, DocumentNode, - DefinitionNode, Kind, InputObjectTypeDefinitionNode -} from 'graphql' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' + ObjectTypeDefinitionNode, + parse, + FieldDefinitionNode, + DocumentNode, + DefinitionNode, + Kind, + InputObjectTypeDefinitionNode, +} from 'graphql'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; import { ResourceConstants } from 'graphql-transformer-common'; import fs = require('fs'); @@ -12,7 +17,7 @@ import path = require('path'); jest.setTimeout(2000000); test('Test custom root types with additional fields.', () => { - const validSchema = ` + const validSchema = ` type Query { additionalQueryField: String } @@ -26,76 +31,72 @@ test('Test custom root types with additional fields.', () => { id: ID! title: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer() - ] - }) - // GetAttGraphQLAPIId - const out = transformer.transform(validSchema); - // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); - const mainStack = out.rootStack; - const postStack = out.stacks.Post; - expect(mainStack).toBeDefined() - expect(postStack).toBeDefined() - const schema = out.schema - expect(schema).toBeDefined() - const definition = out.schema - expect(definition).toBeDefined() - const parsed = parse(definition); - const queryType = getObjectType(parsed, 'Query'); - expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']) - const mutationType = getObjectType(parsed, 'Mutation'); - expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']) - const subscriptionType = getObjectType(parsed, 'Subscription'); - expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost', 'additionalSubscriptionField']) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer()], + }); + // GetAttGraphQLAPIId + const out = transformer.transform(validSchema); + // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); + const mainStack = out.rootStack; + const postStack = out.stacks.Post; + expect(mainStack).toBeDefined(); + expect(postStack).toBeDefined(); + const schema = out.schema; + expect(schema).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const queryType = getObjectType(parsed, 'Query'); + expectFields(queryType, ['getPost', 'listPosts', 'additionalQueryField']); + const mutationType = getObjectType(parsed, 'Mutation'); + expectFields(mutationType, ['createPost', 'updatePost', 'deletePost', 'additionalMutationField']); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expectFields(subscriptionType, ['onCreatePost', 'onUpdatePost', 'onDeletePost', 'additionalSubscriptionField']); }); function expectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } function doNotExpectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - expect( - type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - ).toBeUndefined() - } + for (const fieldName of fields) { + expect(type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName)).toBeUndefined(); + } } function getObjectType(doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type - ) as ObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; } function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type - ) as InputObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type) as + | InputObjectTypeDefinitionNode + | undefined; } function verifyInputCount(doc: DocumentNode, type: string, count: number): boolean { - return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; + return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; } function cleanUpFiles(directory: string) { - var files = fs.readdirSync(directory) - for (const file of files) { - const dir = path.join(directory, file) - if (!fs.lstatSync(dir).isDirectory()) { - fs.unlinkSync(dir) - } else { - cleanUpFiles(dir) - } + var files = fs.readdirSync(directory); + for (const file of files) { + const dir = path.join(directory, file); + if (!fs.lstatSync(dir).isDirectory()) { + fs.unlinkSync(dir); + } else { + cleanUpFiles(dir); } - fs.rmdirSync(directory) + } + fs.rmdirSync(directory); } function readFile(filePath: string) { - return fs.readFileSync(filePath, "utf8") -} \ No newline at end of file + return fs.readFileSync(filePath, 'utf8'); +} diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts index eb2ad8de28..4b4349821e 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionTransformer.e2e.test.ts @@ -1,41 +1,41 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import KeyTransformer from 'graphql-key-transformer' -import { ModelConnectionTransformer } from 'graphql-connection-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' -import { deploy } from '../deployNestedStacks' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import KeyTransformer from 'graphql-key-transformer'; +import { ModelConnectionTransformer } from 'graphql-connection-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; +import { deploy } from '../deployNestedStacks'; import emptyBucket from '../emptyBucket'; import { S3Client } from '../S3Client'; -import * as S3 from 'aws-sdk/clients/s3' +import * as S3 from 'aws-sdk/clients/s3'; import * as moment from 'moment'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cf = new CloudFormationClient('us-west-2'); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `NewConnectionTransformerTest-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-new-connection-transformer-test-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/new_connection_transform_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `NewConnectionTransformerTest-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-new-connection-transformer-test-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/new_connection_transform_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; let GRAPHQL_CLIENT = undefined; function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Child @model @key(fields: ["id", "name"]) @@ -122,101 +122,118 @@ type PostAuthor post: Post @connection(fields: ["postID"]) } -` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema); - // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); - try { - await awsS3Client.createBucket({ - Bucket: BUCKET_NAME, - }).promise() - } catch (e) { - console.error(`Failed to create S3 bucket: ${e}`) - } - try { - console.log('Creating Stack ' + STACK_NAME) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - // Arbitrary wait to make sure everything is ready. - await cf.wait(5, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - expect(finishedStack).toBeDefined() - console.log(JSON.stringify(finishedStack, null, 4)) - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } +`; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new KeyTransformer(), + new ModelConnectionTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)); + try { + await awsS3Client + .createBucket({ + Bucket: BUCKET_NAME, + }) + .promise(); + } catch (e) { + console.error(`Failed to create S3 bucket: ${e}`); + } + try { + console.log('Creating Stack ' + STACK_NAME); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + // Arbitrary wait to make sure everything is ready. + await cf.wait(5, () => Promise.resolve()); + console.log('Successfully created stack ' + STACK_NAME); + expect(finishedStack).toBeDefined(); + console.log(JSON.stringify(finishedStack, null, 4)); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Test queries below */ test('Test Parent.child getItem', async () => { - const createChild = await GRAPHQL_CLIENT.query(`mutation { + const createChild = await GRAPHQL_CLIENT.query( + `mutation { createChild(input: { id: "1", name: "child1" }) { id name } - }`, {}) - expect(createChild.data.createChild.id).toBeDefined() - expect(createChild.data.createChild.name).toEqual('child1') - const createParent = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createChild.data.createChild.id).toBeDefined(); + expect(createChild.data.createChild.name).toEqual('child1'); + const createParent = await GRAPHQL_CLIENT.query( + `mutation { createParent(input: { childID: "1", childName: "${createChild.data.createChild.name}" }) { id childID childName } - }`, {}) - expect(createParent.data.createParent.id).toBeDefined() - expect(createParent.data.createParent.childID).toEqual(createChild.data.createChild.id) - expect(createParent.data.createParent.childName).toEqual(createChild.data.createChild.name) - const queryParent = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createParent.data.createParent.id).toBeDefined(); + expect(createParent.data.createParent.childID).toEqual(createChild.data.createChild.id); + expect(createParent.data.createParent.childName).toEqual(createChild.data.createChild.name); + const queryParent = await GRAPHQL_CLIENT.query( + `query { getParent(id: "${createParent.data.createParent.id}") { id child { @@ -224,36 +241,44 @@ test('Test Parent.child getItem', async () => { name } } - }`, {}) - expect(queryParent.data.getParent).toBeDefined() - const child = queryParent.data.getParent.child - expect(child.id).toEqual(createParent.data.createParent.childID) - expect(child.name).toEqual(createParent.data.createParent.childName) - -}) + }`, + {} + ); + expect(queryParent.data.getParent).toBeDefined(); + const child = queryParent.data.getParent.child; + expect(child.id).toEqual(createParent.data.createParent.childID); + expect(child.name).toEqual(createParent.data.createParent.childName); +}); test('Test Child.parents query', async () => { - const createChild = await GRAPHQL_CLIENT.query(`mutation { + const createChild = await GRAPHQL_CLIENT.query( + `mutation { createChild(input: { id: "2", name: "child2" }) { id name } - }`, {}) - expect(createChild.data.createChild.id).toBeDefined() - expect(createChild.data.createChild.name).toEqual('child2') - - const createParent1 = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createChild.data.createChild.id).toBeDefined(); + expect(createChild.data.createChild.name).toEqual('child2'); + + const createParent1 = await GRAPHQL_CLIENT.query( + `mutation { createParent(input: { childID: "${createChild.data.createChild.id}", childName: "${createChild.data.createChild.name}" }) { id childID childName } - }`, {}) - expect(createParent1.data.createParent.id).toBeDefined() - expect(createParent1.data.createParent.childID).toEqual(createChild.data.createChild.id) - expect(createParent1.data.createParent.childName).toEqual(createChild.data.createChild.name) - - const queryChild = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createParent1.data.createParent.id).toBeDefined(); + expect(createParent1.data.createParent.childID).toEqual(createChild.data.createChild.id); + expect(createParent1.data.createParent.childName).toEqual(createChild.data.createChild.name); + + const queryChild = await GRAPHQL_CLIENT.query( + `query { getChild(id: "${createChild.data.createChild.id}", name: "${createChild.data.createChild.name}") { id parents { @@ -264,28 +289,34 @@ test('Test Child.parents query', async () => { } } } - }`, {}) - expect(queryChild.data.getChild).toBeDefined() - const items = queryChild.data.getChild.parents.items - expect(items.length).toEqual(1) - expect(items[0].id).toEqual(createParent1.data.createParent.id) - expect(items[0].childID).toEqual(createParent1.data.createParent.childID) - expect(items[0].childName).toEqual(createParent1.data.createParent.childName) -}) + }`, + {} + ); + expect(queryChild.data.getChild).toBeDefined(); + const items = queryChild.data.getChild.parents.items; + expect(items.length).toEqual(1); + expect(items[0].id).toEqual(createParent1.data.createParent.id); + expect(items[0].childID).toEqual(createParent1.data.createParent.childID); + expect(items[0].childName).toEqual(createParent1.data.createParent.childName); +}); test('Test PostModel.singleAuthor GetItem with composite sortkey', async () => { - const createUser = await GRAPHQL_CLIENT.query(`mutation { + const createUser = await GRAPHQL_CLIENT.query( + `mutation { createUser(input: { id: "123", name: "Bob", surname: "Rob" }) { id name surname } - }`, {}) - - expect(createUser.data.createUser.id).toBeDefined() - expect(createUser.data.createUser.name).toEqual('Bob') - expect(createUser.data.createUser.surname).toEqual('Rob') - const createPostModel = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + + expect(createUser.data.createUser.id).toBeDefined(); + expect(createUser.data.createUser.name).toEqual('Bob'); + expect(createUser.data.createUser.surname).toEqual('Rob'); + const createPostModel = await GRAPHQL_CLIENT.query( + `mutation { createPostModel(input: { authorID: "${createUser.data.createUser.id}", authorName: "${createUser.data.createUser.name}", authorSurname: "${createUser.data.createUser.surname}", @@ -296,12 +327,15 @@ test('Test PostModel.singleAuthor GetItem with composite sortkey', async () => { authorSurname postContents } - }`, {}) - expect(createPostModel.data.createPostModel.id).toBeDefined() - expect(createPostModel.data.createPostModel.authorID).toEqual(createUser.data.createUser.id) - expect(createPostModel.data.createPostModel.authorName).toEqual(createUser.data.createUser.name) - expect(createPostModel.data.createPostModel.authorSurname).toEqual(createUser.data.createUser.surname) - const queryPostModel = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createPostModel.data.createPostModel.id).toBeDefined(); + expect(createPostModel.data.createPostModel.authorID).toEqual(createUser.data.createUser.id); + expect(createPostModel.data.createPostModel.authorName).toEqual(createUser.data.createUser.name); + expect(createPostModel.data.createPostModel.authorSurname).toEqual(createUser.data.createUser.surname); + const queryPostModel = await GRAPHQL_CLIENT.query( + `query { getPostModel(id: "${createPostModel.data.createPostModel.id}") { id singleAuthor { @@ -310,41 +344,49 @@ test('Test PostModel.singleAuthor GetItem with composite sortkey', async () => { surname } } - }`, {}) - expect(queryPostModel.data.getPostModel).toBeDefined() - const author = queryPostModel.data.getPostModel.singleAuthor - expect(author.id).toEqual(createUser.data.createUser.id) - expect(author.name).toEqual(createUser.data.createUser.name) - expect(author.surname).toEqual(createUser.data.createUser.surname) - -}) + }`, + {} + ); + expect(queryPostModel.data.getPostModel).toBeDefined(); + const author = queryPostModel.data.getPostModel.singleAuthor; + expect(author.id).toEqual(createUser.data.createUser.id); + expect(author.name).toEqual(createUser.data.createUser.name); + expect(author.surname).toEqual(createUser.data.createUser.surname); +}); test('Test PostModel.authors query with composite sortkey', async () => { - const createUser = await GRAPHQL_CLIENT.query(`mutation { + const createUser = await GRAPHQL_CLIENT.query( + `mutation { createUserModel(input: { id: "123", rollNumber: 1, name: "Bob", surname: "Rob" }) { id rollNumber name surname } - }`, {}) - expect(createUser.data.createUserModel.id).toBeDefined() - expect(createUser.data.createUserModel.name).toEqual('Bob') - expect(createUser.data.createUserModel.rollNumber).toEqual(1) - expect(createUser.data.createUserModel.surname).toEqual('Rob') - const createUser2 = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createUser.data.createUserModel.id).toBeDefined(); + expect(createUser.data.createUserModel.name).toEqual('Bob'); + expect(createUser.data.createUserModel.rollNumber).toEqual(1); + expect(createUser.data.createUserModel.surname).toEqual('Rob'); + const createUser2 = await GRAPHQL_CLIENT.query( + `mutation { createUserModel(input: { id: "123", rollNumber: 2, name: "Bob", surname: "Rob" }) { id rollNumber name surname } - }`, {}) - expect(createUser2.data.createUserModel.id).toBeDefined() - expect(createUser2.data.createUserModel.name).toEqual('Bob') - expect(createUser2.data.createUserModel.rollNumber).toEqual(2) - expect(createUser2.data.createUserModel.surname).toEqual('Rob') - const createPostModel = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createUser2.data.createUserModel.id).toBeDefined(); + expect(createUser2.data.createUserModel.name).toEqual('Bob'); + expect(createUser2.data.createUserModel.rollNumber).toEqual(2); + expect(createUser2.data.createUserModel.surname).toEqual('Rob'); + const createPostModel = await GRAPHQL_CLIENT.query( + `mutation { createPostModel(input: { authorID: "${createUser.data.createUserModel.id}", authorName: "${createUser.data.createUserModel.name}", authorSurname: "${createUser.data.createUserModel.surname}", @@ -355,12 +397,15 @@ test('Test PostModel.authors query with composite sortkey', async () => { authorSurname postContents } - }`, {}) - expect(createPostModel.data.createPostModel.id).toBeDefined() - expect(createPostModel.data.createPostModel.authorID).toEqual(createUser.data.createUserModel.id) - expect(createPostModel.data.createPostModel.authorName).toEqual(createUser.data.createUserModel.name) - expect(createPostModel.data.createPostModel.authorSurname).toEqual(createUser.data.createUserModel.surname) - const queryPostModel = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createPostModel.data.createPostModel.id).toBeDefined(); + expect(createPostModel.data.createPostModel.authorID).toEqual(createUser.data.createUserModel.id); + expect(createPostModel.data.createPostModel.authorName).toEqual(createUser.data.createUserModel.name); + expect(createPostModel.data.createPostModel.authorSurname).toEqual(createUser.data.createUserModel.surname); + const queryPostModel = await GRAPHQL_CLIENT.query( + `query { getPostModel(id: "${createPostModel.data.createPostModel.id}") { id authors { @@ -372,47 +417,55 @@ test('Test PostModel.authors query with composite sortkey', async () => { } } } - }`, {}) - expect(queryPostModel.data.getPostModel).toBeDefined() - const items = queryPostModel.data.getPostModel.authors.items - expect(items.length).toEqual(2) - expect(items[0].id).toEqual(createUser.data.createUserModel.id) - try { - expect(items[0].rollNumber).toEqual(createUser.data.createUserModel.rollNumber) - expect(items[1].rollNumber).toEqual(createUser2.data.createUserModel.rollNumber) - } catch (error) { - expect(items[1].rollNumber).toEqual(createUser.data.createUserModel.rollNumber) - expect(items[0].rollNumber).toEqual(createUser2.data.createUserModel.rollNumber) - } - expect(items[0].name).toEqual(createUser.data.createUserModel.name) - expect(items[0].surname).toEqual(createUser.data.createUserModel.surname) - expect(items[1].id).toEqual(createUser2.data.createUserModel.id) - expect(items[1].surname).toEqual(createUser2.data.createUserModel.surname) - expect(items[1].name).toEqual(createUser2.data.createUserModel.name) - -}) + }`, + {} + ); + expect(queryPostModel.data.getPostModel).toBeDefined(); + const items = queryPostModel.data.getPostModel.authors.items; + expect(items.length).toEqual(2); + expect(items[0].id).toEqual(createUser.data.createUserModel.id); + try { + expect(items[0].rollNumber).toEqual(createUser.data.createUserModel.rollNumber); + expect(items[1].rollNumber).toEqual(createUser2.data.createUserModel.rollNumber); + } catch (error) { + expect(items[1].rollNumber).toEqual(createUser.data.createUserModel.rollNumber); + expect(items[0].rollNumber).toEqual(createUser2.data.createUserModel.rollNumber); + } + expect(items[0].name).toEqual(createUser.data.createUserModel.name); + expect(items[0].surname).toEqual(createUser.data.createUserModel.surname); + expect(items[1].id).toEqual(createUser2.data.createUserModel.id); + expect(items[1].surname).toEqual(createUser2.data.createUserModel.surname); + expect(items[1].name).toEqual(createUser2.data.createUserModel.name); +}); test('Test PostModel.authors query with composite sortkey passed as arg.', async () => { - const createUser = await GRAPHQL_CLIENT.query(`mutation { + const createUser = await GRAPHQL_CLIENT.query( + `mutation { createUser(input: { id: "123", name: "Bobby", surname: "Rob" }) { id name surname } - }`, {}) - expect(createUser.data.createUser.id).toBeDefined() - expect(createUser.data.createUser.name).toEqual('Bobby') - expect(createUser.data.createUser.surname).toEqual('Rob') - const createPost = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createUser.data.createUser.id).toBeDefined(); + expect(createUser.data.createUser.name).toEqual('Bobby'); + expect(createUser.data.createUser.surname).toEqual('Rob'); + const createPost = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { id: "321", authorID: "${createUser.data.createUser.id}", postContents: "potato"}) { id authorID postContents } - }`, {}) - expect(createPost.data.createPost.id).toBeDefined() - expect(createPost.data.createPost.authorID).toEqual(createUser.data.createUser.id) - const queryPost = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createPost.data.createPost.id).toBeDefined(); + expect(createPost.data.createPost.authorID).toEqual(createUser.data.createUser.id); + const queryPost = await GRAPHQL_CLIENT.query( + `query { getPost(id: "${createPost.data.createPost.id}") { id authors(nameSurname: {beginsWith: {name: "${createUser.data.createUser.name}", surname: "${createUser.data.createUser.surname}"}}) { @@ -423,28 +476,30 @@ test('Test PostModel.authors query with composite sortkey passed as arg.', async } } } - }`, {}) - expect(queryPost.data.getPost).toBeDefined() - const items = queryPost.data.getPost.authors.items - expect(items.length).toEqual(1) - expect(items[0].id).toEqual(createUser.data.createUser.id) - expect(items[0].name).toEqual(createUser.data.createUser.name) - expect(items[0].surname).toEqual(createUser.data.createUser.surname) - -}) + }`, + {} + ); + expect(queryPost.data.getPost).toBeDefined(); + const items = queryPost.data.getPost.authors.items; + expect(items.length).toEqual(1); + expect(items[0].id).toEqual(createUser.data.createUser.id); + expect(items[0].name).toEqual(createUser.data.createUser.name); + expect(items[0].surname).toEqual(createUser.data.createUser.surname); +}); test('Test User.authorPosts.posts query followed by getItem (intermediary model)', async () => { - const createPostAuthor = await GRAPHQL_CLIENT.query(`mutation { + const createPostAuthor = await GRAPHQL_CLIENT.query(`mutation { createPostAuthor(input: { authorID: "123", postID: "321" }) { id authorID postID } - }`) - expect(createPostAuthor.data.createPostAuthor.id).toBeDefined() - expect(createPostAuthor.data.createPostAuthor.authorID).toEqual("123") - expect(createPostAuthor.data.createPostAuthor.postID).toEqual("321") - const queryUser = await GRAPHQL_CLIENT.query(`query { + }`); + expect(createPostAuthor.data.createPostAuthor.id).toBeDefined(); + expect(createPostAuthor.data.createPostAuthor.authorID).toEqual('123'); + expect(createPostAuthor.data.createPostAuthor.postID).toEqual('321'); + const queryUser = await GRAPHQL_CLIENT.query( + `query { getUserModel(id: "123", rollNumber: 1) { id authorPosts { @@ -456,46 +511,58 @@ test('Test User.authorPosts.posts query followed by getItem (intermediary model) } } } - }`, {}) - expect(queryUser.data.getUserModel).toBeDefined() - const items = queryUser.data.getUserModel.authorPosts.items - expect(items.length).toEqual(1) - expect(items[0].post.id).toEqual("321") - expect(items[0].post.postContents).toEqual(["potato"]) -}) + }`, + {} + ); + expect(queryUser.data.getUserModel).toBeDefined(); + const items = queryUser.data.getUserModel.authorPosts.items; + expect(items.length).toEqual(1); + expect(items[0].post.id).toEqual('321'); + expect(items[0].post.postContents).toEqual(['potato']); +}); test('Test User.friendship.friend query (reflexive has many).', async () => { - const createUser = await GRAPHQL_CLIENT.query(`mutation { + const createUser = await GRAPHQL_CLIENT.query( + `mutation { createUser(input: { id: "12", name: "Bobby", surname: "Rob" }) { id name surname } - }`, {}) - expect(createUser.data.createUser.id).toBeDefined() - expect(createUser.data.createUser.name).toEqual('Bobby') - expect(createUser.data.createUser.surname).toEqual('Rob') - const createUser1 = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createUser.data.createUser.id).toBeDefined(); + expect(createUser.data.createUser.name).toEqual('Bobby'); + expect(createUser.data.createUser.surname).toEqual('Rob'); + const createUser1 = await GRAPHQL_CLIENT.query( + `mutation { createUser(input: { id: "13", name: "Bob", surname: "Rob" }) { id name surname } - }`, {}) - expect(createUser1.data.createUser.id).toBeDefined() - expect(createUser1.data.createUser.name).toEqual('Bob') - expect(createUser1.data.createUser.surname).toEqual('Rob') - const createFriendship = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createUser1.data.createUser.id).toBeDefined(); + expect(createUser1.data.createUser.name).toEqual('Bob'); + expect(createUser1.data.createUser.surname).toEqual('Rob'); + const createFriendship = await GRAPHQL_CLIENT.query( + `mutation { createFriendship(input: { id: "1", userID: 13, friendID: 12 }) { id userID friendID } - }`, {}) - expect(createFriendship.data.createFriendship.id).toBeDefined() - expect(createFriendship.data.createFriendship.userID).toEqual('13') - expect(createFriendship.data.createFriendship.friendID).toEqual('12') - const queryUser = await GRAPHQL_CLIENT.query(`query { + }`, + {} + ); + expect(createFriendship.data.createFriendship.id).toBeDefined(); + expect(createFriendship.data.createFriendship.userID).toEqual('13'); + expect(createFriendship.data.createFriendship.friendID).toEqual('12'); + const queryUser = await GRAPHQL_CLIENT.query( + `query { getUser(id: "13", name: "Bob", surname: "Rob") { id friendships { @@ -509,10 +576,12 @@ test('Test User.friendship.friend query (reflexive has many).', async () => { } } } - }`, {}) - expect(queryUser.data.getUser).toBeDefined() - const items = queryUser.data.getUser.friendships.items - expect(items.length).toEqual(1) - expect(items[0].friend.items[0].id).toEqual("12") - expect(items[0].friend.items[0].name).toEqual("Bobby") -}) \ No newline at end of file + }`, + {} + ); + expect(queryUser.data.getUser).toBeDefined(); + const items = queryUser.data.getUser.friendships.items; + expect(items.length).toEqual(1); + expect(items[0].friend.items[0].id).toEqual('12'); + expect(items[0].friend.items[0].name).toEqual('Bobby'); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts index b41573aa59..5348aef3ff 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NewConnectionWithAuth.e2e.test.ts @@ -1,40 +1,44 @@ import Amplify from 'aws-amplify'; -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import KeyTransformer from 'graphql-key-transformer' -import ModelConnectionTransformer from 'graphql-connection-transformer' -import * as fs from 'fs' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import * as S3 from 'aws-sdk/clients/s3' -import { CreateBucketRequest } from 'aws-sdk/clients/s3' -import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import KeyTransformer from 'graphql-key-transformer'; +import ModelConnectionTransformer from 'graphql-connection-transformer'; +import * as fs from 'fs'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import * as S3 from 'aws-sdk/clients/s3'; +import { CreateBucketRequest } from 'aws-sdk/clients/s3'; +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; +import { GraphQLClient } from '../GraphQLClient'; import { S3Client } from '../S3Client'; -import * as path from 'path' -import { deploy } from '../deployNestedStacks' +import * as path from 'path'; +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; import { - createUserPool, createUserPoolClient, deleteUserPool, - signupAndAuthenticateUser, createGroup, addUserToGroup, - configureAmplify + createUserPool, + createUserPoolClient, + deleteUserPool, + signupAndAuthenticateUser, + createGroup, + addUserToGroup, + configureAmplify, } from '../cognitoUtils'; // to deal with bug in cognito-identity-js -(global as any).fetch = require("node-fetch"); +(global as any).fetch = require('node-fetch'); jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') +const cf = new CloudFormationClient('us-west-2'); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `NewConnectionsWithAuthTests-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `new-connections-with-auth-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_BUILD_ROOT = '/tmp/new_connections_with_auth_test/' -const DEPLOYMENT_ROOT_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `NewConnectionsWithAuthTests-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `new-connections-with-auth-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_BUILD_ROOT = '/tmp/new_connections_with_auth_test/'; +const DEPLOYMENT_ROOT_KEY = 'deployments'; let GRAPHQL_ENDPOINT = undefined; @@ -55,53 +59,53 @@ let GRAPHQL_CLIENT_3 = undefined; let USER_POOL_ID = undefined; -const USERNAME1 = 'user1@test.com' -const USERNAME2 = 'user2@test.com' -const USERNAME3 = 'user3@test.com' -const TMP_PASSWORD = 'Password123!' -const REAL_PASSWORD = 'Password1234!' +const USERNAME1 = 'user1@test.com'; +const USERNAME2 = 'user2@test.com'; +const USERNAME3 = 'user3@test.com'; +const TMP_PASSWORD = 'Password123!'; +const REAL_PASSWORD = 'Password1234!'; const ADMIN_GROUP_NAME = 'Admin'; const DEVS_GROUP_NAME = 'Devs'; const PARTICIPANT_GROUP_NAME = 'Participant'; const WATCHER_GROUP_NAME = 'Watcher'; -const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }) -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } async function createBucket(name: string) { - return new Promise((res, rej) => { - const params: CreateBucketRequest = { - Bucket: name, - } - awsS3Client.createBucket(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + }; + awsS3Client.createBucket(params, (err, data) => (err ? rej(err) : res(data))); + }); } async function deleteBucket(name: string) { - return new Promise((res, rej) => { - const params: CreateBucketRequest = { - Bucket: name, - } - awsS3Client.deleteBucket(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + }; + awsS3Client.deleteBucket(params, (err, data) => (err ? rej(err) : res(data))); + }); } beforeAll(async () => { - // Create a stack for the post model with auth enabled. - if (!fs.existsSync(LOCAL_BUILD_ROOT)) { - fs.mkdirSync(LOCAL_BUILD_ROOT); - } - await createBucket(BUCKET_NAME) - const validSchema = ` + // Create a stack for the post model with auth enabled. + if (!fs.existsSync(LOCAL_BUILD_ROOT)) { + fs.mkdirSync(LOCAL_BUILD_ROOT); + } + await createBucket(BUCKET_NAME); + const validSchema = ` type Post @model @auth(rules: [{ allow: owner }]) @@ -138,130 +142,143 @@ beforeAll(async () => { topLevelID: ID! topLevel: OpenTopLevel @connection(fields: ["topLevelID"]) } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new KeyTransformer(), - new ModelConnectionTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: 'AMAZON_COGNITO_USER_POOLS', - }, - additionalAuthenticationProviders: [], - }, - }), - ] - }) - const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); - USER_POOL_ID = userPoolResponse.UserPool.Id; - const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); - const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; - try { - // Clean the bucket - const out = transformer.transform(validSchema) - - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { AuthCognitoUserPoolId: USER_POOL_ID }, LOCAL_BUILD_ROOT, BUCKET_NAME, DEPLOYMENT_ROOT_KEY, - BUILD_TIMESTAMP - ) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy() - expect(USER_POOL_ID).toBeTruthy() - expect(userPoolClientId).toBeTruthy() - - // Configure Amplify, create users, and sign in. - configureAmplify(USER_POOL_ID, userPoolClientId) - - const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - - await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME) - await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME) - await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME) - await createGroup(USER_POOL_ID, DEVS_GROUP_NAME) - await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID) - const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - - const idToken = authResAfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }) - - const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }) - - const idToken3 = authRes3.getIdToken().getJwtToken() - GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }) - - // Wait for any propagation to avoid random - // "The security token included in the request is invalid" errors - await new Promise((res) => setTimeout(() => res(), 5000)) - } catch (e) { - console.error(e) - throw e; - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new KeyTransformer(), + new ModelConnectionTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + try { + // Clean the bucket + const out = transformer.transform(validSchema); + + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { AuthCognitoUserPoolId: USER_POOL_ID }, + LOCAL_BUILD_ROOT, + BUCKET_NAME, + DEPLOYMENT_ROOT_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + expect(USER_POOL_ID).toBeTruthy(); + expect(userPoolClientId).toBeTruthy(); + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId); + + const authRes: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + const authRes2: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + + await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME); + await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME); + await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME); + await createGroup(USER_POOL_ID, DEVS_GROUP_NAME); + await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID); + const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + + const idToken = authResAfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }); + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }); + + const idToken3 = authRes3.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }); + + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + throw e; + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await deleteUserPool(cognitoClient, USER_POOL_ID) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e); - throw e; - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await deleteUserPool(cognitoClient, USER_POOL_ID); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + throw e; } -}) - + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Tests */ test('Test creating a post and immediately view it via the User.posts connection.', async () => { - const createUser1 = await GRAPHQL_CLIENT_1.query(`mutation { + const createUser1 = await GRAPHQL_CLIENT_1.query( + `mutation { createUser(input: { id: "user1@test.com" }) { id } - }`, {}) - console.log(createUser1); - expect(createUser1.data.createUser.id).toEqual("user1@test.com") - - const response = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(createUser1); + expect(createUser1.data.createUser.id).toEqual('user1@test.com'); + + const response = await GRAPHQL_CLIENT_1.query( + `mutation { createPost(input: { title: "Hello, World!", owner: "user1@test.com" }) { id title owner } - }`, {}) - console.log(response); - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.owner).toBeDefined() - - const getResponse = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + console.log(response); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.owner).toBeDefined(); + + const getResponse = await GRAPHQL_CLIENT_1.query( + `query { getUser(id: "user1@test.com") { posts { items { @@ -274,78 +291,96 @@ test('Test creating a post and immediately view it via the User.posts connection } } } - }`, {}) - console.log(JSON.stringify(getResponse, null, 4)); - expect(getResponse.data.getUser.posts.items[0].id).toBeDefined() - expect(getResponse.data.getUser.posts.items[0].title).toEqual("Hello, World!") - expect(getResponse.data.getUser.posts.items[0].owner).toEqual("user1@test.com") - expect(getResponse.data.getUser.posts.items[0].author.id).toEqual("user1@test.com") -}) + }`, + {} + ); + console.log(JSON.stringify(getResponse, null, 4)); + expect(getResponse.data.getUser.posts.items[0].id).toBeDefined(); + expect(getResponse.data.getUser.posts.items[0].title).toEqual('Hello, World!'); + expect(getResponse.data.getUser.posts.items[0].owner).toEqual('user1@test.com'); + expect(getResponse.data.getUser.posts.items[0].author.id).toEqual('user1@test.com'); +}); test('Testing reading an owner protected field as a non owner', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "1", owner: "${USERNAME1}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(response1); - expect(response1.data.createFieldProtected.id).toEqual("1") - expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1) - expect(response1.data.createFieldProtected.ownerOnly).toEqual(null) - - const response2 = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + console.log(response1); + expect(response1.data.createFieldProtected.id).toEqual('1'); + expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1); + expect(response1.data.createFieldProtected.ownerOnly).toEqual(null); + + const response2 = await GRAPHQL_CLIENT_2.query( + `query { getFieldProtected(id: "1") { id owner ownerOnly } - }`, {}) - console.log(response2); - expect(response2.data.getFieldProtected.ownerOnly).toBeNull() - expect(response2.errors).toHaveLength(1) - - const response3 = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + console.log(response2); + expect(response2.data.getFieldProtected.ownerOnly).toBeNull(); + expect(response2.errors).toHaveLength(1); + + const response3 = await GRAPHQL_CLIENT_1.query( + `query { getFieldProtected(id: "1") { id owner ownerOnly } - }`, {}) - console.log(response3); - expect(response3.data.getFieldProtected.id).toEqual("1") - expect(response3.data.getFieldProtected.owner).toEqual(USERNAME1) - expect(response3.data.getFieldProtected.ownerOnly).toEqual("owner-protected") -}) + }`, + {} + ); + console.log(response3); + expect(response3.data.getFieldProtected.id).toEqual('1'); + expect(response3.data.getFieldProtected.owner).toEqual(USERNAME1); + expect(response3.data.getFieldProtected.ownerOnly).toEqual('owner-protected'); +}); test('Test that @connection resolvers respect @model read operations.', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createOpenTopLevel(input: { id: "1", owner: "${USERNAME1}", name: "open" }) { id owner name } - }`, {}) - console.log(response1); - expect(response1.data.createOpenTopLevel.id).toEqual("1") - expect(response1.data.createOpenTopLevel.owner).toEqual(USERNAME1) - expect(response1.data.createOpenTopLevel.name).toEqual("open") - - const response2 = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(response1); + expect(response1.data.createOpenTopLevel.id).toEqual('1'); + expect(response1.data.createOpenTopLevel.owner).toEqual(USERNAME1); + expect(response1.data.createOpenTopLevel.name).toEqual('open'); + + const response2 = await GRAPHQL_CLIENT_2.query( + `mutation { createConnectionProtected(input: { id: "1", owner: "${USERNAME2}", name: "closed", topLevelID: "1" }) { id owner name topLevelID } - }`, {}) - console.log(response2); - expect(response2.data.createConnectionProtected.id).toEqual("1") - expect(response2.data.createConnectionProtected.owner).toEqual(USERNAME2) - expect(response2.data.createConnectionProtected.name).toEqual("closed") - - const response3 = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + console.log(response2); + expect(response2.data.createConnectionProtected.id).toEqual('1'); + expect(response2.data.createConnectionProtected.owner).toEqual(USERNAME2); + expect(response2.data.createConnectionProtected.name).toEqual('closed'); + + const response3 = await GRAPHQL_CLIENT_1.query( + `query { getOpenTopLevel(id: "1") { id protected { @@ -356,12 +391,15 @@ test('Test that @connection resolvers respect @model read operations.', async () } } } - }`, {}) - console.log(response3); - expect(response3.data.getOpenTopLevel.id).toEqual("1") - expect(response3.data.getOpenTopLevel.protected.items).toHaveLength(0) - - const response4 = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + console.log(response3); + expect(response3.data.getOpenTopLevel.id).toEqual('1'); + expect(response3.data.getOpenTopLevel.protected.items).toHaveLength(0); + + const response4 = await GRAPHQL_CLIENT_2.query( + `query { getOpenTopLevel(id: "1") { id protected { @@ -372,122 +410,148 @@ test('Test that @connection resolvers respect @model read operations.', async () } } } - }`, {}) - console.log(response4); - expect(response4.data.getOpenTopLevel.id).toEqual("1") - expect(response4.data.getOpenTopLevel.protected.items).toHaveLength(1) -}) + }`, + {} + ); + console.log(response4); + expect(response4.data.getOpenTopLevel.id).toEqual('1'); + expect(response4.data.getOpenTopLevel.protected.items).toHaveLength(1); +}); // Per field auth in mutations test('Test that owners cannot set the field of a FieldProtected object unless authorized.', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "2", owner: "${USERNAME1}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(JSON.stringify(response1)); - expect(response1.data.createFieldProtected.id).toEqual("2") - expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1) - expect(response1.data.createFieldProtected.ownerOnly).toEqual(null) - - const response2 = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(JSON.stringify(response1)); + expect(response1.data.createFieldProtected.id).toEqual('2'); + expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1); + expect(response1.data.createFieldProtected.ownerOnly).toEqual(null); + + const response2 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "3", owner: "${USERNAME2}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(response2); - expect(response2.data.createFieldProtected).toBeNull() - expect(response2.errors).toHaveLength(1) - - // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will - // not trigger the @auth check - const response3 = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(response2); + expect(response2.data.createFieldProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); + + // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will + // not trigger the @auth check + const response3 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { id: "4", owner: "${USERNAME2}" }) { id owner ownerOnly } - }`, {}) - console.log(response3); - expect(response3.data.createFieldProtected.id).toEqual("4") - expect(response3.data.createFieldProtected.owner).toEqual(USERNAME2) - // The length is one because the 'ownerOnly' field is protected on reads. - // Since the caller is not the owner this will throw after the mutation succeeds - // and return partial results. - expect(response3.errors).toHaveLength(1) -}) + }`, + {} + ); + console.log(response3); + expect(response3.data.createFieldProtected.id).toEqual('4'); + expect(response3.data.createFieldProtected.owner).toEqual(USERNAME2); + // The length is one because the 'ownerOnly' field is protected on reads. + // Since the caller is not the owner this will throw after the mutation succeeds + // and return partial results. + expect(response3.errors).toHaveLength(1); +}); test('Test that owners cannot update the field of a FieldProtected object unless authorized.', async () => { - const response1 = await GRAPHQL_CLIENT_1.query(`mutation { + const response1 = await GRAPHQL_CLIENT_1.query( + `mutation { createFieldProtected(input: { owner: "${USERNAME1}", ownerOnly: "owner-protected" }) { id owner ownerOnly } - }`, {}) - console.log(JSON.stringify(response1)); - expect(response1.data.createFieldProtected.id).not.toBeNull() - expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1) - expect(response1.data.createFieldProtected.ownerOnly).toEqual(null) - - const response2 = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(JSON.stringify(response1)); + expect(response1.data.createFieldProtected.id).not.toBeNull(); + expect(response1.data.createFieldProtected.owner).toEqual(USERNAME1); + expect(response1.data.createFieldProtected.ownerOnly).toEqual(null); + + const response2 = await GRAPHQL_CLIENT_2.query( + `mutation { updateFieldProtected(input: { id: "${response1.data.createFieldProtected.id}", ownerOnly: "owner2-protected" }) { id owner ownerOnly } - }`, {}) - console.log(response2); - expect(response2.data.updateFieldProtected).toBeNull() - expect(response2.errors).toHaveLength(1) - - // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will - // not trigger the @auth check - const response3 = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(response2); + expect(response2.data.updateFieldProtected).toBeNull(); + expect(response2.errors).toHaveLength(1); + + // The auth rule is on ownerOnly. Omitting the "ownerOnly" field will + // not trigger the @auth check + const response3 = await GRAPHQL_CLIENT_1.query( + `mutation { updateFieldProtected(input: { id: "${response1.data.createFieldProtected.id}", ownerOnly: "updated" }) { id owner ownerOnly } - }`, {}) - console.log(response3); - const resposne3ID = response3.data.updateFieldProtected.id; - expect(resposne3ID).toEqual(response1.data.createFieldProtected.id) - expect(response3.data.updateFieldProtected.owner).toEqual(USERNAME1) - - const response3query = await GRAPHQL_CLIENT_1.query(`query getMake1 { + }`, + {} + ); + console.log(response3); + const resposne3ID = response3.data.updateFieldProtected.id; + expect(resposne3ID).toEqual(response1.data.createFieldProtected.id); + expect(response3.data.updateFieldProtected.owner).toEqual(USERNAME1); + + const response3query = await GRAPHQL_CLIENT_1.query(`query getMake1 { getFieldProtected(id: "${resposne3ID}"){ id owner ownerOnly } - }`) - expect(response3query.data.getFieldProtected.ownerOnly).toEqual("updated") + }`); + expect(response3query.data.getFieldProtected.ownerOnly).toEqual('updated'); - // This request should succeed since we are not updating the protected field. - const response4 = await GRAPHQL_CLIENT_3.query(`mutation { + // This request should succeed since we are not updating the protected field. + const response4 = await GRAPHQL_CLIENT_3.query( + `mutation { updateFieldProtected(input: { id: "${response1.data.createFieldProtected.id}", owner: "${USERNAME3}" }) { id owner ownerOnly } - }`, {}) - console.log(response4); - expect(response4.data.updateFieldProtected.id).toEqual(response1.data.createFieldProtected.id) - expect(response4.data.updateFieldProtected.owner).toEqual(USERNAME3) - expect(response4.data.updateFieldProtected.ownerOnly).toBeNull() - - const response5 = await GRAPHQL_CLIENT_3.query(`query { + }`, + {} + ); + console.log(response4); + expect(response4.data.updateFieldProtected.id).toEqual(response1.data.createFieldProtected.id); + expect(response4.data.updateFieldProtected.owner).toEqual(USERNAME3); + expect(response4.data.updateFieldProtected.ownerOnly).toBeNull(); + + const response5 = await GRAPHQL_CLIENT_3.query( + `query { getFieldProtected( id: "${response1.data.createFieldProtected.id}" ) { id owner ownerOnly } - }`, {}) - console.log(response5); - expect(response5.data.getFieldProtected.ownerOnly).toEqual("updated") -}) + }`, + {} + ); + console.log(response5); + expect(response5.data.getFieldProtected.ownerOnly).toEqual('updated'); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts index 5cd1523c9f..f0ffff3458 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/NoneEnvFunctionTransformer.e2e.test.ts @@ -1,34 +1,34 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import ModelTransformer from 'graphql-dynamodb-transformer' -import FunctionTransformer from 'graphql-function-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import ModelTransformer from 'graphql-dynamodb-transformer'; +import FunctionTransformer from 'graphql-function-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; -import { deploy } from '../deployNestedStacks' +import { deploy } from '../deployNestedStacks'; import { S3Client } from '../S3Client'; -import * as S3 from 'aws-sdk/clients/s3' +import * as S3 from 'aws-sdk/clients/s3'; import { LambdaHelper } from '../LambdaHelper'; import { IAMHelper } from '../IAMHelper'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cf = new CloudFormationClient('us-west-2'); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `NoneEnvFunctionTransformerTests-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `appsync-none-env-func-trans-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/none_env_function_transformer_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `NoneEnvFunctionTransformerTests-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `appsync-none-env-func-trans-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/none_env_function_transformer_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; // Note: there is no env in the name. -const ECHO_FUNCTION_NAME = `e2e-tests-echo-${BUILD_TIMESTAMP}` -const LAMBDA_EXECUTION_ROLE_NAME = `amplify_e2e_tests_lambda_basic_none_${BUILD_TIMESTAMP}` -const LAMBDA_EXECUTION_POLICY_NAME = `amplify_e2e_tests_lambda_basic_access_none_${BUILD_TIMESTAMP}` +const ECHO_FUNCTION_NAME = `e2e-tests-echo-${BUILD_TIMESTAMP}`; +const LAMBDA_EXECUTION_ROLE_NAME = `amplify_e2e_tests_lambda_basic_none_${BUILD_TIMESTAMP}`; +const LAMBDA_EXECUTION_POLICY_NAME = `amplify_e2e_tests_lambda_basic_access_none_${BUILD_TIMESTAMP}`; let LAMBDA_EXECUTION_POLICY_ARN = ''; let GRAPHQL_CLIENT = undefined; @@ -37,14 +37,14 @@ const LAMBDA_HELPER = new LambdaHelper(); const IAM_HELPER = new IAMHelper(); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Query { echoNoEnv(msg: String!): Context @function(name: "e2e-tests-echo-${BUILD_TIMESTAMP}-\${env}") } @@ -56,90 +56,113 @@ beforeAll(async () => { type Arguments { msg: String! } - ` - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { console.warn(`Could not create bucket: ${e}`) } - try { - const role = await IAM_HELPER.createLambdaExecutionRole(LAMBDA_EXECUTION_ROLE_NAME); - await wait(5000); - const policy = await IAM_HELPER.createLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_NAME); - await wait(5000); - LAMBDA_EXECUTION_POLICY_ARN = policy.Policy.Arn; - await IAM_HELPER.attachLambdaExecutionPolicy(policy.Policy.Arn, role.Role.RoleName) - await wait(10000); - await LAMBDA_HELPER.createFunction(ECHO_FUNCTION_NAME, role.Role.Arn, 'echoFunction'); - } catch (e) { console.warn(`Could not setup function: ${e}`) } - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new FunctionTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}) - ] - }) - const out = transformer.transform(validSchema); - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - // Arbitrary wait to make sure everything is ready. - await cf.wait(5, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - console.log(finishedStack) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) + `; + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.warn(`Could not create bucket: ${e}`); + } + try { + const role = await IAM_HELPER.createLambdaExecutionRole(LAMBDA_EXECUTION_ROLE_NAME); + await wait(5000); + const policy = await IAM_HELPER.createLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_NAME); + await wait(5000); + LAMBDA_EXECUTION_POLICY_ARN = policy.Policy.Arn; + await IAM_HELPER.attachLambdaExecutionPolicy(policy.Policy.Arn, role.Role.RoleName); + await wait(10000); + await LAMBDA_HELPER.createFunction(ECHO_FUNCTION_NAME, role.Role.Arn, 'echoFunction'); + } catch (e) { + console.warn(`Could not setup function: ${e}`); + } + const transformer = new GraphQLTransform({ + transformers: [ + new ModelTransformer(), + new FunctionTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const out = transformer.transform(validSchema); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + // Arbitrary wait to make sure everything is ready. + await cf.wait(5, () => Promise.resolve()); + console.log('Successfully created stack ' + STACK_NAME); + console.log(finishedStack); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { console.warn(`Error during bucket cleanup: ${e}`)} - try { - await LAMBDA_HELPER.deleteFunction(ECHO_FUNCTION_NAME); - } catch (e) { console.warn(`Error during function cleanup: ${e}`)} - try { - await IAM_HELPER.detachLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_ARN, LAMBDA_EXECUTION_ROLE_NAME) - } catch (e) { console.warn(`Error during policy dissociation: ${e}`)} - try { - await IAM_HELPER.deleteRole(LAMBDA_EXECUTION_ROLE_NAME); - } catch (e) { console.warn(`Error during role cleanup: ${e}`)} - try { - await IAM_HELPER.deletePolicy(LAMBDA_EXECUTION_POLICY_ARN); - } catch (e) { console.warn(`Error during policy cleanup: ${e}`)} -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.warn(`Error during bucket cleanup: ${e}`); + } + try { + await LAMBDA_HELPER.deleteFunction(ECHO_FUNCTION_NAME); + } catch (e) { + console.warn(`Error during function cleanup: ${e}`); + } + try { + await IAM_HELPER.detachLambdaExecutionPolicy(LAMBDA_EXECUTION_POLICY_ARN, LAMBDA_EXECUTION_ROLE_NAME); + } catch (e) { + console.warn(`Error during policy dissociation: ${e}`); + } + try { + await IAM_HELPER.deleteRole(LAMBDA_EXECUTION_ROLE_NAME); + } catch (e) { + console.warn(`Error during role cleanup: ${e}`); + } + try { + await IAM_HELPER.deletePolicy(LAMBDA_EXECUTION_POLICY_ARN); + } catch (e) { + console.warn(`Error during policy cleanup: ${e}`); + } +}); /** * Test queries below */ test('Test simple echo function', async () => { - const response = await GRAPHQL_CLIENT.query(`query { + const response = await GRAPHQL_CLIENT.query( + `query { echoNoEnv(msg: "Hello") { arguments { msg @@ -147,15 +170,17 @@ test('Test simple echo function', async () => { typeName fieldName } - }`, {}) - console.log(JSON.stringify(response, null, 4)); - expect(response.data.echoNoEnv.arguments.msg).toEqual("Hello") - expect(response.data.echoNoEnv.typeName).toEqual("Query") - expect(response.data.echoNoEnv.fieldName).toEqual("echoNoEnv") -}) + }`, + {} + ); + console.log(JSON.stringify(response, null, 4)); + expect(response.data.echoNoEnv.arguments.msg).toEqual('Hello'); + expect(response.data.echoNoEnv.typeName).toEqual('Query'); + expect(response.data.echoNoEnv.fieldName).toEqual('echoNoEnv'); +}); function wait(ms: number) { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), ms) - }) + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), ms); + }); } diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts index 333bb2adc7..22ddaf1943 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/PerFieldAuthTests.e2e.test.ts @@ -1,38 +1,43 @@ import Amplify from 'aws-amplify'; -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import ModelConnectionTransformer from 'graphql-connection-transformer' -import * as fs from 'fs' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import * as S3 from 'aws-sdk/clients/s3' -import { CreateBucketRequest } from 'aws-sdk/clients/s3' -import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import ModelConnectionTransformer from 'graphql-connection-transformer'; +import * as fs from 'fs'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import * as S3 from 'aws-sdk/clients/s3'; +import { CreateBucketRequest } from 'aws-sdk/clients/s3'; +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; +import { GraphQLClient } from '../GraphQLClient'; import { S3Client } from '../S3Client'; -import * as path from 'path' -import { deploy } from '../deployNestedStacks' +import * as path from 'path'; +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; import { - createUserPool, createUserPoolClient, deleteUserPool, - signupAndAuthenticateUser, createGroup, addUserToGroup, configureAmplify + createUserPool, + createUserPoolClient, + deleteUserPool, + signupAndAuthenticateUser, + createGroup, + addUserToGroup, + configureAmplify, } from '../cognitoUtils'; // to deal with bug in cognito-identity-js -(global as any).fetch = require("node-fetch"); +(global as any).fetch = require('node-fetch'); jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') +const cf = new CloudFormationClient('us-west-2'); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `PerFieldAuthTests-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `per-field-auth-tests-bucket-${BUILD_TIMESTAMP}` -const LOCAL_BUILD_ROOT = '/tmp/per_field_auth_tests/' -const DEPLOYMENT_ROOT_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `PerFieldAuthTests-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `per-field-auth-tests-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_BUILD_ROOT = '/tmp/per_field_auth_tests/'; +const DEPLOYMENT_ROOT_KEY = 'deployments'; let GRAPHQL_ENDPOINT = undefined; @@ -53,11 +58,11 @@ let GRAPHQL_CLIENT_3 = undefined; let USER_POOL_ID = undefined; -const USERNAME1 = 'user1@test.com' -const USERNAME2 = 'user2@test.com' -const USERNAME3 = 'user3@test.com' -const TMP_PASSWORD = 'Password123!' -const REAL_PASSWORD = 'Password1234!' +const USERNAME1 = 'user1@test.com'; +const USERNAME2 = 'user2@test.com'; +const USERNAME3 = 'user3@test.com'; +const TMP_PASSWORD = 'Password123!'; +const REAL_PASSWORD = 'Password1234!'; const ADMIN_GROUP_NAME = 'Admin'; const DEVS_GROUP_NAME = 'Devs'; @@ -65,42 +70,42 @@ const PARTICIPANT_GROUP_NAME = 'Participant'; const WATCHER_GROUP_NAME = 'Watcher'; const INSTRUCTOR_GROUP_NAME = 'Instructor'; -const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }) -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }); +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } async function createBucket(name: string) { - return new Promise((res, rej) => { - const params: CreateBucketRequest = { - Bucket: name, - } - awsS3Client.createBucket(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + }; + awsS3Client.createBucket(params, (err, data) => (err ? rej(err) : res(data))); + }); } async function deleteBucket(name: string) { - return new Promise((res, rej) => { - const params: CreateBucketRequest = { - Bucket: name, - } - awsS3Client.deleteBucket(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + }; + awsS3Client.deleteBucket(params, (err, data) => (err ? rej(err) : res(data))); + }); } beforeAll(async () => { - // Create a stack for the post model with auth enabled. - if (!fs.existsSync(LOCAL_BUILD_ROOT)) { - fs.mkdirSync(LOCAL_BUILD_ROOT); - } - await createBucket(BUCKET_NAME) - const validSchema = ` + // Create a stack for the post model with auth enabled. + if (!fs.existsSync(LOCAL_BUILD_ROOT)) { + fs.mkdirSync(LOCAL_BUILD_ROOT); + } + await createBucket(BUCKET_NAME); + const validSchema = ` # Owners may update their owned records. # Admins may create Employee records. # Any authenticated user may view Employee ids & emails. @@ -160,351 +165,415 @@ beforeAll(async () => { owner1: String! @auth(rules: [{allow: owner, ownerField: "notAllowed", operations: [update]}]) text: String @auth(rules: [{ allow: owner, ownerField: "owner1", operations : [update]}]) } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}), - ] - }) - const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); - USER_POOL_ID = userPoolResponse.UserPool.Id; - const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); - const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; - try { - // Clean the bucket - const out = transformer.transform(validSchema) - - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { AuthCognitoUserPoolId: USER_POOL_ID }, LOCAL_BUILD_ROOT, BUCKET_NAME, DEPLOYMENT_ROOT_KEY, - BUILD_TIMESTAMP - ) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy() - expect(USER_POOL_ID).toBeTruthy() - expect(userPoolClientId).toBeTruthy() - - // Configure Amplify, create users, and sign in. - configureAmplify(USER_POOL_ID, userPoolClientId) - - await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME) - await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME) - await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME) - await createGroup(USER_POOL_ID, DEVS_GROUP_NAME) - await createGroup(USER_POOL_ID, INSTRUCTOR_GROUP_NAME) - await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID) - await addUserToGroup(INSTRUCTOR_GROUP_NAME, USERNAME1, USER_POOL_ID) - await addUserToGroup(INSTRUCTOR_GROUP_NAME, USERNAME2, USER_POOL_ID) - - const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - const idToken = authResAfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }) - - const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }) - - const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - const idToken3 = authRes3.getIdToken().getJwtToken() - GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }) - - // Wait for any propagation to avoid random - // "The security token included in the request is invalid" errors - await new Promise((res) => setTimeout(() => res(), 5000)) - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelConnectionTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + try { + // Clean the bucket + const out = transformer.transform(validSchema); + + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { AuthCognitoUserPoolId: USER_POOL_ID }, + LOCAL_BUILD_ROOT, + BUCKET_NAME, + DEPLOYMENT_ROOT_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + expect(USER_POOL_ID).toBeTruthy(); + expect(userPoolClientId).toBeTruthy(); + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId); + + await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME); + await createGroup(USER_POOL_ID, PARTICIPANT_GROUP_NAME); + await createGroup(USER_POOL_ID, WATCHER_GROUP_NAME); + await createGroup(USER_POOL_ID, DEVS_GROUP_NAME); + await createGroup(USER_POOL_ID, INSTRUCTOR_GROUP_NAME); + await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, USER_POOL_ID); + await addUserToGroup(INSTRUCTOR_GROUP_NAME, USERNAME1, USER_POOL_ID); + await addUserToGroup(INSTRUCTOR_GROUP_NAME, USERNAME2, USER_POOL_ID); + + const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + const idToken = authResAfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_1 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken }); + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken2 }); + + const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + const idToken3 = authRes3.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, { Authorization: idToken3 }); + + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); - afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await deleteUserPool(cognitoClient, USER_POOL_ID) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - throw e; - } + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await deleteUserPool(cognitoClient, USER_POOL_ID); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + throw e; } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) - } -}) - + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Tests */ test('Test that only Admins can create Employee records.', async () => { - const createUser1 = await GRAPHQL_CLIENT_1.query(`mutation { + const createUser1 = await GRAPHQL_CLIENT_1.query( + `mutation { createEmployee(input: { email: "user2@test.com", salary: 100 }) { id email salary } - }`, {}) - console.log(createUser1); - expect(createUser1.data.createEmployee.email).toEqual("user2@test.com") - expect(createUser1.data.createEmployee.salary).toEqual(100) - - const tryToCreateAsNonAdmin = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(createUser1); + expect(createUser1.data.createEmployee.email).toEqual('user2@test.com'); + expect(createUser1.data.createEmployee.salary).toEqual(100); + + const tryToCreateAsNonAdmin = await GRAPHQL_CLIENT_2.query( + `mutation { createEmployee(input: { email: "user2@test.com", salary: 101 }) { id email salary } - }`, {}) - console.log(tryToCreateAsNonAdmin); - expect(tryToCreateAsNonAdmin.data.createEmployee).toBeNull(); - expect(tryToCreateAsNonAdmin.errors).toHaveLength(1); - - const tryToCreateAsNonAdmin2 = await GRAPHQL_CLIENT_3.query(`mutation { + }`, + {} + ); + console.log(tryToCreateAsNonAdmin); + expect(tryToCreateAsNonAdmin.data.createEmployee).toBeNull(); + expect(tryToCreateAsNonAdmin.errors).toHaveLength(1); + + const tryToCreateAsNonAdmin2 = await GRAPHQL_CLIENT_3.query( + `mutation { createEmployee(input: { email: "user2@test.com", salary: 101 }) { id email salary } - }`, {}) - console.log(tryToCreateAsNonAdmin2); - expect(tryToCreateAsNonAdmin2.data.createEmployee).toBeNull(); - expect(tryToCreateAsNonAdmin2.errors).toHaveLength(1); -}) + }`, + {} + ); + console.log(tryToCreateAsNonAdmin2); + expect(tryToCreateAsNonAdmin2.data.createEmployee).toBeNull(); + expect(tryToCreateAsNonAdmin2.errors).toHaveLength(1); +}); test('Test that only Admins may update salary & email.', async () => { - const createUser1 = await GRAPHQL_CLIENT_1.query(`mutation { + const createUser1 = await GRAPHQL_CLIENT_1.query( + `mutation { createEmployee(input: { email: "user2@test.com", salary: 100 }) { id email salary } - }`, {}) - console.log(createUser1); - const employeeId = createUser1.data.createEmployee.id; - expect(employeeId).not.toBeNull(); - expect(createUser1.data.createEmployee.email).toEqual("user2@test.com") - expect(createUser1.data.createEmployee.salary).toEqual(100) - - const tryToUpdateAsNonAdmin = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(createUser1); + const employeeId = createUser1.data.createEmployee.id; + expect(employeeId).not.toBeNull(); + expect(createUser1.data.createEmployee.email).toEqual('user2@test.com'); + expect(createUser1.data.createEmployee.salary).toEqual(100); + + const tryToUpdateAsNonAdmin = await GRAPHQL_CLIENT_2.query( + `mutation { updateEmployee(input: { id: "${employeeId}", salary: 101 }) { id email salary } - }`, {}) - console.log(tryToUpdateAsNonAdmin); - expect(tryToUpdateAsNonAdmin.data.updateEmployee).toBeNull(); - expect(tryToUpdateAsNonAdmin.errors).toHaveLength(1); - - const tryToUpdateAsNonAdmin2 = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(tryToUpdateAsNonAdmin); + expect(tryToUpdateAsNonAdmin.data.updateEmployee).toBeNull(); + expect(tryToUpdateAsNonAdmin.errors).toHaveLength(1); + + const tryToUpdateAsNonAdmin2 = await GRAPHQL_CLIENT_2.query( + `mutation { updateEmployee(input: { id: "${employeeId}", email: "someonelese@gmail.com" }) { id email salary } - }`, {}) - console.log(tryToUpdateAsNonAdmin2); - expect(tryToUpdateAsNonAdmin2.data.updateEmployee).toBeNull(); - expect(tryToUpdateAsNonAdmin2.errors).toHaveLength(1); - - const tryToUpdateAsNonAdmin3 = await GRAPHQL_CLIENT_3.query(`mutation { + }`, + {} + ); + console.log(tryToUpdateAsNonAdmin2); + expect(tryToUpdateAsNonAdmin2.data.updateEmployee).toBeNull(); + expect(tryToUpdateAsNonAdmin2.errors).toHaveLength(1); + + const tryToUpdateAsNonAdmin3 = await GRAPHQL_CLIENT_3.query( + `mutation { updateEmployee(input: { id: "${employeeId}", email: "someonelese@gmail.com" }) { id email salary } - }`, {}) - console.log(tryToUpdateAsNonAdmin3); - expect(tryToUpdateAsNonAdmin3.data.updateEmployee).toBeNull(); - expect(tryToUpdateAsNonAdmin3.errors).toHaveLength(1); - - const updateAsAdmin = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(tryToUpdateAsNonAdmin3); + expect(tryToUpdateAsNonAdmin3.data.updateEmployee).toBeNull(); + expect(tryToUpdateAsNonAdmin3.errors).toHaveLength(1); + + const updateAsAdmin = await GRAPHQL_CLIENT_1.query( + `mutation { updateEmployee(input: { id: "${employeeId}", email: "someonelese@gmail.com" }) { id email salary } - }`, {}) - console.log(updateAsAdmin); - expect(updateAsAdmin.data.updateEmployee.email).toEqual("someonelese@gmail.com") - expect(updateAsAdmin.data.updateEmployee.salary).toEqual(100) - - const updateAsAdmin2 = await GRAPHQL_CLIENT_1.query(`mutation { + }`, + {} + ); + console.log(updateAsAdmin); + expect(updateAsAdmin.data.updateEmployee.email).toEqual('someonelese@gmail.com'); + expect(updateAsAdmin.data.updateEmployee.salary).toEqual(100); + + const updateAsAdmin2 = await GRAPHQL_CLIENT_1.query( + `mutation { updateEmployee(input: { id: "${employeeId}", salary: 99 }) { id email salary } - }`, {}) - console.log(updateAsAdmin2); - expect(updateAsAdmin2.data.updateEmployee.email).toEqual("someonelese@gmail.com") - expect(updateAsAdmin2.data.updateEmployee.salary).toEqual(99) -}) + }`, + {} + ); + console.log(updateAsAdmin2); + expect(updateAsAdmin2.data.updateEmployee.email).toEqual('someonelese@gmail.com'); + expect(updateAsAdmin2.data.updateEmployee.salary).toEqual(99); +}); test('Test that owners may update their bio.', async () => { - const createUser1 = await GRAPHQL_CLIENT_1.query(`mutation { + const createUser1 = await GRAPHQL_CLIENT_1.query( + `mutation { createEmployee(input: { email: "user2@test.com", salary: 100 }) { id email salary } - }`, {}) - console.log(createUser1); - const employeeId = createUser1.data.createEmployee.id; - expect(employeeId).not.toBeNull(); - expect(createUser1.data.createEmployee.email).toEqual("user2@test.com") - expect(createUser1.data.createEmployee.salary).toEqual(100) - - const tryToUpdateAsNonAdmin = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(createUser1); + const employeeId = createUser1.data.createEmployee.id; + expect(employeeId).not.toBeNull(); + expect(createUser1.data.createEmployee.email).toEqual('user2@test.com'); + expect(createUser1.data.createEmployee.salary).toEqual(100); + + const tryToUpdateAsNonAdmin = await GRAPHQL_CLIENT_2.query( + `mutation { updateEmployee(input: { id: "${employeeId}", bio: "Does cool stuff." }) { id email salary bio } - }`, {}) - console.log(tryToUpdateAsNonAdmin); - expect(tryToUpdateAsNonAdmin.data.updateEmployee.bio).toEqual("Does cool stuff.") - expect(tryToUpdateAsNonAdmin.data.updateEmployee.email).toEqual("user2@test.com") - expect(tryToUpdateAsNonAdmin.data.updateEmployee.salary).toEqual(100) -}) + }`, + {} + ); + console.log(tryToUpdateAsNonAdmin); + expect(tryToUpdateAsNonAdmin.data.updateEmployee.bio).toEqual('Does cool stuff.'); + expect(tryToUpdateAsNonAdmin.data.updateEmployee.email).toEqual('user2@test.com'); + expect(tryToUpdateAsNonAdmin.data.updateEmployee.salary).toEqual(100); +}); test('Test that everyone may view employee bios.', async () => { - const createUser1 = await GRAPHQL_CLIENT_1.query(`mutation { + const createUser1 = await GRAPHQL_CLIENT_1.query( + `mutation { createEmployee(input: { email: "user3@test.com", salary: 100, bio: "Likes long walks on the beach" }) { id email salary bio } - }`, {}) - console.log(createUser1); - const employeeId = createUser1.data.createEmployee.id; - expect(employeeId).not.toBeNull(); - expect(createUser1.data.createEmployee.email).toEqual("user3@test.com") - expect(createUser1.data.createEmployee.salary).toEqual(100) - expect(createUser1.data.createEmployee.bio).toEqual('Likes long walks on the beach') - - const getAsNonAdmin = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + console.log(createUser1); + const employeeId = createUser1.data.createEmployee.id; + expect(employeeId).not.toBeNull(); + expect(createUser1.data.createEmployee.email).toEqual('user3@test.com'); + expect(createUser1.data.createEmployee.salary).toEqual(100); + expect(createUser1.data.createEmployee.bio).toEqual('Likes long walks on the beach'); + + const getAsNonAdmin = await GRAPHQL_CLIENT_2.query( + `query { getEmployee(id: "${employeeId}") { id email bio } - }`, {}) - console.log(getAsNonAdmin); - // Should not be able to view the email as the non owner - expect(getAsNonAdmin.data.getEmployee.email).toBeNull(); - // Should be able to view the bio. - expect(getAsNonAdmin.data.getEmployee.bio).toEqual('Likes long walks on the beach'); - expect(getAsNonAdmin.errors).toHaveLength(1); - - const listAsNonAdmin = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + console.log(getAsNonAdmin); + // Should not be able to view the email as the non owner + expect(getAsNonAdmin.data.getEmployee.email).toBeNull(); + // Should be able to view the bio. + expect(getAsNonAdmin.data.getEmployee.bio).toEqual('Likes long walks on the beach'); + expect(getAsNonAdmin.errors).toHaveLength(1); + + const listAsNonAdmin = await GRAPHQL_CLIENT_2.query( + `query { listEmployees { items { id bio } } - }`, {}) - console.log(listAsNonAdmin); - expect(listAsNonAdmin.data.listEmployees.items.length).toBeGreaterThan(1); - let seenId = false; - for (const item of listAsNonAdmin.data.listEmployees.items) { - if (item.id === employeeId) { - seenId = true; - expect(item.bio).toEqual('Likes long walks on the beach'); - } + }`, + {} + ); + console.log(listAsNonAdmin); + expect(listAsNonAdmin.data.listEmployees.items.length).toBeGreaterThan(1); + let seenId = false; + for (const item of listAsNonAdmin.data.listEmployees.items) { + if (item.id === employeeId) { + seenId = true; + expect(item.bio).toEqual('Likes long walks on the beach'); } - expect(seenId).toEqual(true) -}) + } + expect(seenId).toEqual(true); +}); test('Test that only owners may "delete" i.e. update the field to null.', async () => { - const createUser1 = await GRAPHQL_CLIENT_1.query(`mutation { + const createUser1 = await GRAPHQL_CLIENT_1.query( + `mutation { createEmployee(input: { email: "user3@test.com", salary: 200, notes: "note1" }) { id email salary notes } - }`, {}) - console.log(createUser1); - const employeeId = createUser1.data.createEmployee.id; - expect(employeeId).not.toBeNull(); - expect(createUser1.data.createEmployee.email).toEqual("user3@test.com") - expect(createUser1.data.createEmployee.salary).toEqual(200) - expect(createUser1.data.createEmployee.notes).toEqual('note1') - - const tryToDeleteUserNotes = await GRAPHQL_CLIENT_2.query(`mutation { + }`, + {} + ); + console.log(createUser1); + const employeeId = createUser1.data.createEmployee.id; + expect(employeeId).not.toBeNull(); + expect(createUser1.data.createEmployee.email).toEqual('user3@test.com'); + expect(createUser1.data.createEmployee.salary).toEqual(200); + expect(createUser1.data.createEmployee.notes).toEqual('note1'); + + const tryToDeleteUserNotes = await GRAPHQL_CLIENT_2.query( + `mutation { updateEmployee(input: { id: "${employeeId}", notes: null }) { id notes } - }`, {}) - console.log(tryToDeleteUserNotes); - expect(tryToDeleteUserNotes.data.updateEmployee).toBeNull() - expect(tryToDeleteUserNotes.errors).toHaveLength(1) - - const updateNewsWithNotes = await GRAPHQL_CLIENT_3.query(`mutation { + }`, + {} + ); + console.log(tryToDeleteUserNotes); + expect(tryToDeleteUserNotes.data.updateEmployee).toBeNull(); + expect(tryToDeleteUserNotes.errors).toHaveLength(1); + + const updateNewsWithNotes = await GRAPHQL_CLIENT_3.query( + `mutation { updateEmployee(input: { id: "${employeeId}", notes: "something else" }) { id notes } - }`, {}) - expect(updateNewsWithNotes.data.updateEmployee.notes).toEqual('something else') + }`, + {} + ); + expect(updateNewsWithNotes.data.updateEmployee.notes).toEqual('something else'); - const updateAsAdmin = await GRAPHQL_CLIENT_1.query(`mutation { + const updateAsAdmin = await GRAPHQL_CLIENT_1.query( + `mutation { updateEmployee(input: { id: "${employeeId}", notes: null }) { id notes } - }`, {}) - expect(updateAsAdmin.data.updateEmployee).toBeNull() - expect(updateAsAdmin.errors).toHaveLength(1) - - const deleteNotes = await GRAPHQL_CLIENT_3.query(`mutation { + }`, + {} + ); + expect(updateAsAdmin.data.updateEmployee).toBeNull(); + expect(updateAsAdmin.errors).toHaveLength(1); + + const deleteNotes = await GRAPHQL_CLIENT_3.query( + `mutation { updateEmployee(input: { id: "${employeeId}", notes: null }) { id notes } - }`, {}) - expect(deleteNotes.data.updateEmployee.notes).toBeNull() -}) + }`, + {} + ); + expect(deleteNotes.data.updateEmployee.notes).toBeNull(); +}); test('Test with auth with subscriptions on default behavior', async () => { - /** - * client 1 and 2 are in the same user pool though client 1 should - * not be able to see notes if they are created by client 2 - * */ - const secureNote1 = "secureNote1" - const createStudent2 = await GRAPHQL_CLIENT_2.query(`mutation { + /** + * client 1 and 2 are in the same user pool though client 1 should + * not be able to see notes if they are created by client 2 + * */ + const secureNote1 = 'secureNote1'; + const createStudent2 = await GRAPHQL_CLIENT_2.query( + `mutation { createStudent(input: {bio: "bio1", name: "student1", notes: "${secureNote1}"}) { id bio @@ -512,14 +581,17 @@ test('Test with auth with subscriptions on default behavior', async () => { notes owner } - }`, {}) - console.log(createStudent2) - expect(createStudent2.data.createStudent.id).toBeDefined() - const createStudent1queryID = createStudent2.data.createStudent.id - expect(createStudent2.data.createStudent.bio).toEqual('bio1') - expect(createStudent2.data.createStudent.notes).toBeNull() - // running query as username2 should return value - const queryForStudent2 = await GRAPHQL_CLIENT_2.query(`query { + }`, + {} + ); + console.log(createStudent2); + expect(createStudent2.data.createStudent.id).toBeDefined(); + const createStudent1queryID = createStudent2.data.createStudent.id; + expect(createStudent2.data.createStudent.bio).toEqual('bio1'); + expect(createStudent2.data.createStudent.notes).toBeNull(); + // running query as username2 should return value + const queryForStudent2 = await GRAPHQL_CLIENT_2.query( + `query { getStudent(id: "${createStudent1queryID}") { bio id @@ -527,12 +599,15 @@ test('Test with auth with subscriptions on default behavior', async () => { notes owner } - }`, {}) - console.log(queryForStudent2) - expect(queryForStudent2.data.getStudent.notes).toEqual(secureNote1) - - // running query as username3 should return the type though return notes as null - const queryAsStudent1 = await GRAPHQL_CLIENT_1.query(`query { + }`, + {} + ); + console.log(queryForStudent2); + expect(queryForStudent2.data.getStudent.notes).toEqual(secureNote1); + + // running query as username3 should return the type though return notes as null + const queryAsStudent1 = await GRAPHQL_CLIENT_1.query( + `query { getStudent(id: "${createStudent1queryID}") { bio id @@ -540,44 +615,46 @@ test('Test with auth with subscriptions on default behavior', async () => { notes owner } - }`, {}) - console.log(queryAsStudent1) - expect(queryAsStudent1.data.getStudent.notes).toBeNull() -}) + }`, + {} + ); + console.log(queryAsStudent1); + expect(queryAsStudent1.data.getStudent.notes).toBeNull(); +}); test('AND per-field dynamic auth rule test', async () => { - const createPostResponse = await GRAPHQL_CLIENT_1.query(`mutation CreatePost { + const createPostResponse = await GRAPHQL_CLIENT_1.query(`mutation CreatePost { createPost(input: {owner1: "${USERNAME1}", text: "mytext"}) { id text owner1 } - }`) - console.log(createPostResponse) - const postID1 = createPostResponse.data.createPost.id; - expect(postID1).toBeDefined() - expect(createPostResponse.data.createPost.text).toEqual('mytext') - expect(createPostResponse.data.createPost.owner1).toEqual(USERNAME1) - - const badUpdatePostResponse = await GRAPHQL_CLIENT_1.query(`mutation UpdatePost { + }`); + console.log(createPostResponse); + const postID1 = createPostResponse.data.createPost.id; + expect(postID1).toBeDefined(); + expect(createPostResponse.data.createPost.text).toEqual('mytext'); + expect(createPostResponse.data.createPost.owner1).toEqual(USERNAME1); + + const badUpdatePostResponse = await GRAPHQL_CLIENT_1.query(`mutation UpdatePost { updatePost(input: {id: "${postID1}", text: "newText", owner1: "${USERNAME1}"}) { id owner1 text } } - `) - console.log(badUpdatePostResponse) - expect(badUpdatePostResponse.errors[0].errorType).toEqual('DynamoDB:ConditionalCheckFailedException') + `); + console.log(badUpdatePostResponse); + expect(badUpdatePostResponse.errors[0].errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); - const correctUpdatePostResponse = await GRAPHQL_CLIENT_1.query(`mutation UpdatePost { + const correctUpdatePostResponse = await GRAPHQL_CLIENT_1.query(`mutation UpdatePost { updatePost(input: {id: "${postID1}", text: "newText"}) { id owner1 text } - }`) - console.log(correctUpdatePostResponse) - expect(correctUpdatePostResponse.data.updatePost.owner1).toEqual(USERNAME1) - expect(correctUpdatePostResponse.data.updatePost.text).toEqual('newText') -}) \ No newline at end of file + }`); + console.log(correctUpdatePostResponse); + expect(correctUpdatePostResponse.data.updatePost.owner1).toEqual(USERNAME1); + expect(correctUpdatePostResponse.data.updatePost.text).toEqual('newText'); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts index bd9a3b6f3c..27d12b8c7a 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformer.e2e.test.ts @@ -1,84 +1,66 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import SearchableModelTransformer from 'graphql-elasticsearch-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { S3Client } from '../S3Client' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' -import { deploy } from '../deployNestedStacks' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import SearchableModelTransformer from 'graphql-elasticsearch-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { S3Client } from '../S3Client'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; -import * as S3 from 'aws-sdk/clients/s3' -import emptyBucket from '../emptyBucket' +import * as S3 from 'aws-sdk/clients/s3'; +import emptyBucket from '../emptyBucket'; jest.setTimeout(60000 * 60); -const s3 = new S3Client('us-west-2') -const cf = new CloudFormationClient('us-west-2') +const s3 = new S3Client('us-west-2'); +const cf = new CloudFormationClient('us-west-2'); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `TestSearchableModelTransformer-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `testsearchablemodeltransformer-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/model_searchable_transform_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `TestSearchableModelTransformer-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `testsearchablemodeltransformer-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/model_searchable_transform_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); -const fragments = [ - `fragment FullPost on Post { id author title ups downs percentageUp isPublished createdAt }` -] +const fragments = [`fragment FullPost on Post { id author title ups downs percentageUp isPublished createdAt }`]; const createPosts = async () => { - const logContent = 'createPost response: ' - - await runQuery(getCreatePostsQuery( - "snvishna", "test", 157, 10, 97.4, true - ), logContent) - await runQuery(getCreatePostsQuery( - "snvishna", "test title", 60, 30, 21.0, false - ), logContent) - await runQuery(getCreatePostsQuery( - "shankar", "test title", 160, 30, 97.6, false - ), logContent) - await runQuery(getCreatePostsQuery( - "snvishna", "test TITLE", 170, 30, 88.8, true - ), logContent) - await runQuery(getCreatePostsQuery( - "snvishna", "test title", 200, 50, 11.9, false - ), logContent) - await runQuery(getCreatePostsQuery( - "snvishna", "test title", 170, 30, 88.8, true - ), logContent) - await runQuery(getCreatePostsQuery( - "snvishna", "test title", 160, 30, 97.6, false - ), logContent) - await runQuery(getCreatePostsQuery( - "snvishna", "test title", 170, 30, 77.7, true - ), logContent) - - // Waiting for the ES Cluster + Streaming Lambda infra to be setup - console.log('Waiting for the ES Cluster + Streaming Lambda infra to be setup') - await cf.wait(120, () => Promise.resolve()) -} + const logContent = 'createPost response: '; + + await runQuery(getCreatePostsQuery('snvishna', 'test', 157, 10, 97.4, true), logContent); + await runQuery(getCreatePostsQuery('snvishna', 'test title', 60, 30, 21.0, false), logContent); + await runQuery(getCreatePostsQuery('shankar', 'test title', 160, 30, 97.6, false), logContent); + await runQuery(getCreatePostsQuery('snvishna', 'test TITLE', 170, 30, 88.8, true), logContent); + await runQuery(getCreatePostsQuery('snvishna', 'test title', 200, 50, 11.9, false), logContent); + await runQuery(getCreatePostsQuery('snvishna', 'test title', 170, 30, 88.8, true), logContent); + await runQuery(getCreatePostsQuery('snvishna', 'test title', 160, 30, 97.6, false), logContent); + await runQuery(getCreatePostsQuery('snvishna', 'test title', 170, 30, 77.7, true), logContent); + + // Waiting for the ES Cluster + Streaming Lambda infra to be setup + console.log('Waiting for the ES Cluster + Streaming Lambda infra to be setup'); + await cf.wait(120, () => Promise.resolve()); +}; const runQuery = async (query: string, logContent: string) => { - try { - const q = [query, ...fragments].join('\n'); - const response = await GRAPHQL_CLIENT.query(q, {}); - console.log(logContent + JSON.stringify(response, null, 4)); - return response; - } catch (e) { - console.error(e); - return null; - } -} + try { + const q = [query, ...fragments].join('\n'); + const response = await GRAPHQL_CLIENT.query(q, {}); + console.log(logContent + JSON.stringify(response, null, 4)); + return response; + } catch (e) { + console.error(e); + return null; + } +}; let GRAPHQL_CLIENT = undefined; beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Post @model @searchable { id: ID! author: String! @@ -97,80 +79,89 @@ beforeAll(async () => { isPublished: Boolean jsonField: AWSJSON } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}), - new SearchableModelTransformer() - ] - }) - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { - console.error(`Failed to create bucket: ${e}`) - } - try { - const out = transformer.transform(validSchema); - // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)) - // create stack with additional params - // const additionalParams = generateParams() - console.log('Creating Stack ' + STACK_NAME) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - // Arbitrary wait to make sure everything is ready. - await cf.wait(120, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) - - // Create sample mutations to test search queries - await createPosts(); - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + new SearchableModelTransformer(), + ], + }); + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.error(`Failed to create bucket: ${e}`); + } + try { + const out = transformer.transform(validSchema); + // fs.writeFileSync('./out.json', JSON.stringify(out, null, 4)) + // create stack with additional params + // const additionalParams = generateParams() + console.log('Creating Stack ' + STACK_NAME); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + // Arbitrary wait to make sure everything is ready. + await cf.wait(120, () => Promise.resolve()); + console.log('Successfully created stack ' + STACK_NAME); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); + + // Create sample mutations to test search queries + await createPosts(); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } }); test('Test searchPosts with sort field on a string field', async () => { - const firstQuery = await runQuery(`query { + const firstQuery = await runQuery( + `query { searchPosts(sort: { field: id direction: desc @@ -180,11 +171,14 @@ test('Test searchPosts with sort field on a string field', async () => { } nextToken } - }`, 'Test searchPosts with filter ') - expect(firstQuery).toBeDefined() - expect(firstQuery.data.searchPosts).toBeDefined() - const fourthItemOfFirstQuery = firstQuery.data.searchPosts.items[3] - const secondQuery = await runQuery(`query { + }`, + 'Test searchPosts with filter ' + ); + expect(firstQuery).toBeDefined(); + expect(firstQuery.data.searchPosts).toBeDefined(); + const fourthItemOfFirstQuery = firstQuery.data.searchPosts.items[3]; + const secondQuery = await runQuery( + `query { searchPosts(limit: 3, sort: { field: id direction: desc @@ -194,12 +188,15 @@ test('Test searchPosts with sort field on a string field', async () => { } nextToken } - }`, 'Test searchPosts with limit ') - expect(secondQuery).toBeDefined() - expect(secondQuery.data.searchPosts).toBeDefined() - const nextToken = secondQuery.data.searchPosts.nextToken - expect(nextToken).toBeDefined() - const thirdQuery = await runQuery(`query { + }`, + 'Test searchPosts with limit ' + ); + expect(secondQuery).toBeDefined(); + expect(secondQuery.data.searchPosts).toBeDefined(); + const nextToken = secondQuery.data.searchPosts.nextToken; + expect(nextToken).toBeDefined(); + const thirdQuery = await runQuery( + `query { searchPosts(nextToken: "${nextToken}", limit: 3, sort: { field: id direction: desc @@ -209,15 +206,18 @@ test('Test searchPosts with sort field on a string field', async () => { } nextToken } - }`, 'Test searchPosts with sort limit and nextToken ') - expect(thirdQuery).toBeDefined() - expect(thirdQuery.data.searchPosts).toBeDefined() - const firstItemOfThirdQuery = thirdQuery.data.searchPosts.items[0] - expect(firstItemOfThirdQuery).toEqual(fourthItemOfFirstQuery) -}) + }`, + 'Test searchPosts with sort limit and nextToken ' + ); + expect(thirdQuery).toBeDefined(); + expect(thirdQuery.data.searchPosts).toBeDefined(); + const firstItemOfThirdQuery = thirdQuery.data.searchPosts.items[0]; + expect(firstItemOfThirdQuery).toEqual(fourthItemOfFirstQuery); +}); test('Test searchPosts with sort on date type', async () => { - const query = await runQuery(`query { + const query = await runQuery( + `query { searchPosts( sort: { field: createdAt @@ -227,45 +227,51 @@ test('Test searchPosts with sort on date type', async () => { ...FullPost } } - }`, 'Test search posts with date type response: ') - expect(query).toBeDefined() - expect(query.data.searchPosts).toBeDefined() - const recentItem = new Date(query.data.searchPosts.items[0].createdAt) - const oldestItem = new Date(query.data.searchPosts.items[ - query.data.searchPosts.items.length - 1 - ].createdAt) - expect(recentItem > oldestItem) - -}) + }`, + 'Test search posts with date type response: ' + ); + expect(query).toBeDefined(); + expect(query.data.searchPosts).toBeDefined(); + const recentItem = new Date(query.data.searchPosts.items[0].createdAt); + const oldestItem = new Date(query.data.searchPosts.items[query.data.searchPosts.items.length - 1].createdAt); + expect(recentItem > oldestItem); +}); test('Test searchPosts query without filter', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts { items { ...FullPost } } - }`, 'Test searchPosts response without filter response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toBeGreaterThan(0) -}) + }`, + 'Test searchPosts response without filter response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toBeGreaterThan(0); +}); test('Test searchPosts query with basic filter', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { author: { eq: "snvishna" } }) { items { ...FullPost } } - }`, 'Test searchPosts response with basic filter response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(7) -}) + }`, + 'Test searchPosts response with basic filter response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(7); +}); test('Test searchPosts query with non-recursive filter', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { title: { eq: "test title" } ups: { gte: 100 } @@ -276,22 +282,25 @@ test('Test searchPosts query with non-recursive filter', async () => { }) { items { ...FullPost } } - }`, 'Test searchPosts response with non-recursive filter response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(1) - expect(items[0].id).toBeDefined() - expect(items[0].author).toEqual("snvishna") - expect(items[0].title).toEqual("test title") - expect(items[0].ups).toEqual(170) - expect(items[0].downs).toEqual(30) - expect(items[0].percentageUp).toEqual(88.8) - expect(items[0].isPublished).toEqual(true) -}) + }`, + 'Test searchPosts response with non-recursive filter response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(1); + expect(items[0].id).toBeDefined(); + expect(items[0].author).toEqual('snvishna'); + expect(items[0].title).toEqual('test title'); + expect(items[0].ups).toEqual(170); + expect(items[0].downs).toEqual(30); + expect(items[0].percentageUp).toEqual(88.8); + expect(items[0].isPublished).toEqual(true); +}); test('Test searchPosts query with recursive filter 1', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { downs: { eq: 10 } or: [ @@ -306,22 +315,25 @@ test('Test searchPosts query with recursive filter 1', async () => { }) { items { ...FullPost } } - }`, 'Test searchPosts response with recursive filter 1 response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(1) - expect(items[0].id).toBeDefined() - expect(items[0].author).toEqual("snvishna") - expect(items[0].title).toEqual("test") - expect(items[0].ups).toEqual(157) - expect(items[0].downs).toEqual(10) - expect(items[0].percentageUp).toEqual(97.4) - expect(items[0].isPublished).toEqual(true) -}) + }`, + 'Test searchPosts response with recursive filter 1 response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(1); + expect(items[0].id).toBeDefined(); + expect(items[0].author).toEqual('snvishna'); + expect(items[0].title).toEqual('test'); + expect(items[0].ups).toEqual(157); + expect(items[0].downs).toEqual(10); + expect(items[0].percentageUp).toEqual(97.4); + expect(items[0].isPublished).toEqual(true); +}); test('Test searchPosts query with recursive filter 2', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { downs: { eq: 30 } or: [ @@ -336,15 +348,18 @@ test('Test searchPosts query with recursive filter 2', async () => { }) { items { ...FullPost } } - }`, 'Test searchPosts response with recursive filter 2 response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(5) -}) + }`, + 'Test searchPosts response with recursive filter 2 response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(5); +}); test('Test searchPosts query with recursive filter 3', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { ups:{ gt:199 } and:[ @@ -365,22 +380,25 @@ test('Test searchPosts query with recursive filter 3', async () => { }) { items { ...FullPost } } - }`, 'Test searchPosts query with recursive filter 3 response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(1) - expect(items[0].id).toBeDefined() - expect(items[0].author).toEqual("snvishna") - expect(items[0].title).toEqual("test title") - expect(items[0].ups).toEqual(200) - expect(items[0].downs).toEqual(50) - expect(items[0].percentageUp).toEqual(11.9) - expect(items[0].isPublished).toEqual(false) -}) + }`, + 'Test searchPosts query with recursive filter 3 response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(1); + expect(items[0].id).toBeDefined(); + expect(items[0].author).toEqual('snvishna'); + expect(items[0].title).toEqual('test title'); + expect(items[0].ups).toEqual(200); + expect(items[0].downs).toEqual(50); + expect(items[0].percentageUp).toEqual(11.9); + expect(items[0].isPublished).toEqual(false); +}); test('Test searchPosts query with recursive filter 4', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { ups:{ gt:100 } and:[ @@ -404,22 +422,25 @@ test('Test searchPosts query with recursive filter 4', async () => { }) { items { ...FullPost } } - }`, 'Test searchPosts query with recursive filter 4 response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(1) - expect(items[0].id).toBeDefined() - expect(items[0].author).toEqual("snvishna") - expect(items[0].title).toEqual("test title") - expect(items[0].ups).toEqual(160) - expect(items[0].downs).toEqual(30) - expect(items[0].percentageUp).toEqual(97.6) - expect(items[0].isPublished).toEqual(false) -}) + }`, + 'Test searchPosts query with recursive filter 4 response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(1); + expect(items[0].id).toBeDefined(); + expect(items[0].author).toEqual('snvishna'); + expect(items[0].title).toEqual('test title'); + expect(items[0].ups).toEqual(160); + expect(items[0].downs).toEqual(30); + expect(items[0].percentageUp).toEqual(97.6); + expect(items[0].isPublished).toEqual(false); +}); test('Test searchPosts query with recursive filter 5', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { downs:{ ne:30 } or:[ @@ -443,22 +464,25 @@ test('Test searchPosts query with recursive filter 5', async () => { }) { items { ...FullPost } } - }`, 'Test searchPosts query with recursive filter 5 response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(1) - expect(items[0].id).toBeDefined() - expect(items[0].author).toEqual("snvishna") - expect(items[0].title).toEqual("test title") - expect(items[0].ups).toEqual(200) - expect(items[0].downs).toEqual(50) - expect(items[0].percentageUp).toEqual(11.9) - expect(items[0].isPublished).toEqual(false) -}) + }`, + 'Test searchPosts query with recursive filter 5 response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(1); + expect(items[0].id).toBeDefined(); + expect(items[0].author).toEqual('snvishna'); + expect(items[0].title).toEqual('test title'); + expect(items[0].ups).toEqual(200); + expect(items[0].downs).toEqual(50); + expect(items[0].percentageUp).toEqual(11.9); + expect(items[0].isPublished).toEqual(false); +}); test('Test searchPosts query with recursive filter 6', async () => { - const response = await runQuery(`query { + const response = await runQuery( + `query { searchPosts(filter: { not: { title:{ wildcard: "*test*" } @@ -481,114 +505,131 @@ test('Test searchPosts query with recursive filter 6', async () => { }) { items { ...FullPost } } - }`, 'Test searchPosts query with recursive filter 6 response: ') - expect(response).toBeDefined() - expect(response.data.searchPosts.items).toBeDefined() - const items = response.data.searchPosts.items - expect(items.length).toEqual(0) -}) + }`, + 'Test searchPosts query with recursive filter 6 response: ' + ); + expect(response).toBeDefined(); + expect(response.data.searchPosts.items).toBeDefined(); + const items = response.data.searchPosts.items; + expect(items.length).toEqual(0); +}); test('Test deletePosts syncing with Elasticsearch', async () => { - // Create Post - const title = 'to be deleted'; - const postToBeDeletedResponse = await runQuery(getCreatePostsQuery( - "test author new", title, 1157, 1000, 22.2, true - ), 'createPost (to be deleted) response: '); - expect(postToBeDeletedResponse).toBeDefined() - expect(postToBeDeletedResponse.data.createPost).toBeDefined() - expect(postToBeDeletedResponse.data.createPost.id).toBeDefined() - - // Wait for the Post to sync to Elasticsearch - await cf.wait(10, () => Promise.resolve()) - - const searchResponse1 = await runQuery(`query { + // Create Post + const title = 'to be deleted'; + const postToBeDeletedResponse = await runQuery( + getCreatePostsQuery('test author new', title, 1157, 1000, 22.2, true), + 'createPost (to be deleted) response: ' + ); + expect(postToBeDeletedResponse).toBeDefined(); + expect(postToBeDeletedResponse.data.createPost).toBeDefined(); + expect(postToBeDeletedResponse.data.createPost.id).toBeDefined(); + + // Wait for the Post to sync to Elasticsearch + await cf.wait(10, () => Promise.resolve()); + + const searchResponse1 = await runQuery( + `query { searchPosts(filter: { title: { eq: "${title}" } }) { items { ...FullPost } } - }`, 'Test deletePosts syncing with Elasticsearch Search_Before response: ') - expect(searchResponse1).toBeDefined() - expect(searchResponse1.data.searchPosts.items).toBeDefined() - const items1 = searchResponse1.data.searchPosts.items - expect(items1.length).toEqual(1) - expect(items1[0].id).toEqual(postToBeDeletedResponse.data.createPost.id) - expect(items1[0].author).toEqual("test author new") - expect(items1[0].title).toEqual(title) - expect(items1[0].ups).toEqual(1157) - expect(items1[0].downs).toEqual(1000) - expect(items1[0].percentageUp).toEqual(22.2) - expect(items1[0].isPublished).toEqual(true) - - const deleteResponse = await runQuery(`mutation { + }`, + 'Test deletePosts syncing with Elasticsearch Search_Before response: ' + ); + expect(searchResponse1).toBeDefined(); + expect(searchResponse1.data.searchPosts.items).toBeDefined(); + const items1 = searchResponse1.data.searchPosts.items; + expect(items1.length).toEqual(1); + expect(items1[0].id).toEqual(postToBeDeletedResponse.data.createPost.id); + expect(items1[0].author).toEqual('test author new'); + expect(items1[0].title).toEqual(title); + expect(items1[0].ups).toEqual(1157); + expect(items1[0].downs).toEqual(1000); + expect(items1[0].percentageUp).toEqual(22.2); + expect(items1[0].isPublished).toEqual(true); + + const deleteResponse = await runQuery( + `mutation { deletePost(input: { id: "${postToBeDeletedResponse.data.createPost.id}" }) { ...FullPost } - }`, 'Test deletePosts syncing with Elasticsearch Perform_Delete response: ') - expect(deleteResponse).toBeDefined() - expect(deleteResponse.data.deletePost).toBeDefined() - expect(deleteResponse.data.deletePost.id).toEqual(postToBeDeletedResponse.data.createPost.id) - - // Wait for the Deleted Post to sync to Elasticsearch - await cf.wait(10, () => Promise.resolve()) - - const searchResponse2 = await runQuery(`query { + }`, + 'Test deletePosts syncing with Elasticsearch Perform_Delete response: ' + ); + expect(deleteResponse).toBeDefined(); + expect(deleteResponse.data.deletePost).toBeDefined(); + expect(deleteResponse.data.deletePost.id).toEqual(postToBeDeletedResponse.data.createPost.id); + + // Wait for the Deleted Post to sync to Elasticsearch + await cf.wait(10, () => Promise.resolve()); + + const searchResponse2 = await runQuery( + `query { searchPosts(filter: { title: { eq: "${title}" } }) { items { ...FullPost } } - }`, 'Test deletePosts syncing with Elasticsearch Search_After response: ') - expect(searchResponse2).toBeDefined() - expect(searchResponse2.data.searchPosts.items).toBeDefined() - const items2 = searchResponse2.data.searchPosts.items - expect(items2.length).toEqual(0) -}) + }`, + 'Test deletePosts syncing with Elasticsearch Search_After response: ' + ); + expect(searchResponse2).toBeDefined(); + expect(searchResponse2.data.searchPosts.items).toBeDefined(); + const items2 = searchResponse2.data.searchPosts.items; + expect(items2.length).toEqual(0); +}); test('Test updatePost syncing with Elasticsearch', async () => { - // Create Post - const author = 'test author update new'; - const title = 'to be updated new'; - const ups = 2157; - const downs = 2000; - const percentageUp = 22.2; - const isPublished = true; - - const postToBeUpdatedResponse = await runQuery(getCreatePostsQuery( - author, title, ups, downs, percentageUp, isPublished - ), 'createPost (to be updated) response: '); - expect(postToBeUpdatedResponse).toBeDefined(); - expect(postToBeUpdatedResponse.data.createPost).toBeDefined(); - - const id = postToBeUpdatedResponse.data.createPost.id; - expect(id).toBeDefined() - - // Wait for the Post to sync to Elasticsearch - await cf.wait(10, () => Promise.resolve()) - - const searchResponse1 = await runQuery(`query { + // Create Post + const author = 'test author update new'; + const title = 'to be updated new'; + const ups = 2157; + const downs = 2000; + const percentageUp = 22.2; + const isPublished = true; + + const postToBeUpdatedResponse = await runQuery( + getCreatePostsQuery(author, title, ups, downs, percentageUp, isPublished), + 'createPost (to be updated) response: ' + ); + expect(postToBeUpdatedResponse).toBeDefined(); + expect(postToBeUpdatedResponse.data.createPost).toBeDefined(); + + const id = postToBeUpdatedResponse.data.createPost.id; + expect(id).toBeDefined(); + + // Wait for the Post to sync to Elasticsearch + await cf.wait(10, () => Promise.resolve()); + + const searchResponse1 = await runQuery( + `query { searchPosts(filter: { id: { eq: "${id}" } }) { items { ...FullPost } } - }`, 'Test updatePost syncing with Elasticsearch Search_Before response: ') - expect(searchResponse1).toBeDefined(); - expect(searchResponse1.data.searchPosts.items).toBeDefined(); - const items1 = searchResponse1.data.searchPosts.items; - expect(items1.length).toEqual(1); - expect(items1[0].id).toEqual(id); - expect(items1[0].author).toEqual(author); - expect(items1[0].title).toEqual(title); - expect(items1[0].ups).toEqual(ups); - expect(items1[0].downs).toEqual(downs); - expect(items1[0].percentageUp).toEqual(percentageUp); - expect(items1[0].isPublished).toEqual(isPublished); - - const newTitle = title.concat('_updated'); - const updateResponse = await runQuery(`mutation { + }`, + 'Test updatePost syncing with Elasticsearch Search_Before response: ' + ); + expect(searchResponse1).toBeDefined(); + expect(searchResponse1.data.searchPosts.items).toBeDefined(); + const items1 = searchResponse1.data.searchPosts.items; + expect(items1.length).toEqual(1); + expect(items1[0].id).toEqual(id); + expect(items1[0].author).toEqual(author); + expect(items1[0].title).toEqual(title); + expect(items1[0].ups).toEqual(ups); + expect(items1[0].downs).toEqual(downs); + expect(items1[0].percentageUp).toEqual(percentageUp); + expect(items1[0].isPublished).toEqual(isPublished); + + const newTitle = title.concat('_updated'); + const updateResponse = await runQuery( + `mutation { updatePost(input: { id: "${id}" author: "${author}" @@ -600,53 +641,58 @@ test('Test updatePost syncing with Elasticsearch', async () => { }) { ...FullPost } - }`, 'Test updatePost syncing with Elasticsearch Perform_Update response: ') - expect(updateResponse).toBeDefined() - expect(updateResponse.data.updatePost).toBeDefined() - expect(updateResponse.data.updatePost.id).toEqual(id) - expect(updateResponse.data.updatePost.title).toEqual(newTitle) - - // Wait for the Update Post to sync to Elasticsearch - await cf.wait(10, () => Promise.resolve()) - - const searchResponse2 = await runQuery(`query { + }`, + 'Test updatePost syncing with Elasticsearch Perform_Update response: ' + ); + expect(updateResponse).toBeDefined(); + expect(updateResponse.data.updatePost).toBeDefined(); + expect(updateResponse.data.updatePost.id).toEqual(id); + expect(updateResponse.data.updatePost.title).toEqual(newTitle); + + // Wait for the Update Post to sync to Elasticsearch + await cf.wait(10, () => Promise.resolve()); + + const searchResponse2 = await runQuery( + `query { searchPosts(filter: { id: { eq: "${id}" } }) { items { ...FullPost } } - }`, 'Test updatePost syncing with Elasticsearch Search_After response: ') - expect(searchResponse2).toBeDefined(); - expect(searchResponse2.data.searchPosts.items).toBeDefined(); - const items2 = searchResponse2.data.searchPosts.items; - expect(items2.length).toEqual(1); - expect(items2[0].id).toEqual(id); - expect(items2[0].author).toEqual(author); - expect(items2[0].title).toEqual(newTitle); - expect(items2[0].ups).toEqual(ups); - expect(items2[0].downs).toEqual(downs); - expect(items2[0].percentageUp).toEqual(percentageUp); - expect(items2[0].isPublished).toEqual(isPublished); -}) + }`, + 'Test updatePost syncing with Elasticsearch Search_After response: ' + ); + expect(searchResponse2).toBeDefined(); + expect(searchResponse2.data.searchPosts.items).toBeDefined(); + const items2 = searchResponse2.data.searchPosts.items; + expect(items2.length).toEqual(1); + expect(items2[0].id).toEqual(id); + expect(items2[0].author).toEqual(author); + expect(items2[0].title).toEqual(newTitle); + expect(items2[0].ups).toEqual(ups); + expect(items2[0].downs).toEqual(downs); + expect(items2[0].percentageUp).toEqual(percentageUp); + expect(items2[0].isPublished).toEqual(isPublished); +}); function generateParams() { - const params = { - [ResourceConstants.PARAMETERS.ElasticsearchAccessIAMRoleName]: 'ElasticsearchAccessIAMRoleTest', - [ResourceConstants.PARAMETERS.ElasticsearchStreamingIAMRoleName]: 'ElasticsearchStreamingIAMRoleTest' - } + const params = { + [ResourceConstants.PARAMETERS.ElasticsearchAccessIAMRoleName]: 'ElasticsearchAccessIAMRoleTest', + [ResourceConstants.PARAMETERS.ElasticsearchStreamingIAMRoleName]: 'ElasticsearchStreamingIAMRoleTest', + }; - return params + return params; } function getCreatePostsQuery( - author: string, - title: string, - ups: number, - downs: number, - percentageUp: number, - isPublished: boolean + author: string, + title: string, + ups: number, + downs: number, + percentageUp: number, + isPublished: boolean ): string { - return `mutation { + return `mutation { createPost(input: { author: "${author}" title: "${title}" @@ -655,12 +701,12 @@ function getCreatePostsQuery( percentageUp: ${percentageUp} isPublished: ${isPublished} }) { ...FullPost } - }` + }`; } function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts index 3f6aae2686..3cd52f1f7c 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableWithAuthTests.e2e.test.ts @@ -1,48 +1,54 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import ModelConnectionTransformer from 'graphql-connection-transformer' -import SearchableModelTransformer from 'graphql-elasticsearch-transformer' -import * as fs from 'fs' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import * as S3 from 'aws-sdk/clients/s3' -import { CreateBucketRequest } from 'aws-sdk/clients/s3' -import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import ModelConnectionTransformer from 'graphql-connection-transformer'; +import SearchableModelTransformer from 'graphql-elasticsearch-transformer'; +import * as fs from 'fs'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import * as S3 from 'aws-sdk/clients/s3'; +import { CreateBucketRequest } from 'aws-sdk/clients/s3'; +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync'; import { AWS } from '@aws-amplify/core'; import { Auth } from 'aws-amplify'; import gql from 'graphql-tag'; import { S3Client } from '../S3Client'; -import { deploy } from '../deployNestedStacks' +import { deploy } from '../deployNestedStacks'; import * as moment from 'moment'; import emptyBucket from '../emptyBucket'; import { - createUserPool, createUserPoolClient, deleteUserPool, addIAMRolesToCFNStack, - signupAndAuthenticateUser, createGroup, addUserToGroup, configureAmplify, + createUserPool, + createUserPoolClient, + deleteUserPool, + addIAMRolesToCFNStack, + signupAndAuthenticateUser, + createGroup, + addUserToGroup, + configureAmplify, } from '../cognitoUtils'; import 'isomorphic-fetch'; // To overcome of the way of how AmplifyJS picks up currentUserCredentials -const anyAWS = (AWS as any); +const anyAWS = AWS as any; if (anyAWS && anyAWS.config && anyAWS.config.credentials) { - delete anyAWS.config.credentials; + delete anyAWS.config.credentials; } // to deal with bug in cognito-identity-js -(global as any).fetch = require("node-fetch"); +(global as any).fetch = require('node-fetch'); jest.setTimeout(9700000); -const AWS_REGION = 'us-west-2' -const cf = new CloudFormationClient(AWS_REGION) -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `SearchableAuthTests-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `searchable-auth-tests-bucket-${BUILD_TIMESTAMP}` -const LOCAL_BUILD_ROOT = '/tmp/searchable_auth_tests/' -const DEPLOYMENT_ROOT_KEY = 'deployments' +const AWS_REGION = 'us-west-2'; +const cf = new CloudFormationClient(AWS_REGION); +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `SearchableAuthTests-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `searchable-auth-tests-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_BUILD_ROOT = '/tmp/searchable_auth_tests/'; +const DEPLOYMENT_ROOT_KEY = 'deployments'; const AUTH_ROLE_NAME = `${STACK_NAME}-authRole`; const UNAUTH_ROLE_NAME = `${STACK_NAME}-unauthRole`; const IDENTITY_POOL_NAME = `SearchableAuthModelAuthTransformerTest_${BUILD_TIMESTAMP}_identity_pool`; @@ -78,42 +84,41 @@ let GRAPHQL_APIKEY_CLIENT: AWSAppSyncClient = undefined; let USER_POOL_ID = undefined; -const USERNAME1 = 'user1@test.com' -const USERNAME2 = 'user2@test.com' -const USERNAME3 = 'user3@test.com' -const TMP_PASSWORD = 'Password123!' -const REAL_PASSWORD = 'Password1234!' -const WRITER_GROUP_NAME = 'writer' -const ADMIN_GROUP_NAME = 'admin' - -const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: AWS_REGION }) -const customS3Client = new S3Client(AWS_REGION) -const awsS3Client = new S3({ region: AWS_REGION }) +const USERNAME1 = 'user1@test.com'; +const USERNAME2 = 'user2@test.com'; +const USERNAME3 = 'user3@test.com'; +const TMP_PASSWORD = 'Password123!'; +const REAL_PASSWORD = 'Password1234!'; +const WRITER_GROUP_NAME = 'writer'; +const ADMIN_GROUP_NAME = 'admin'; +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: AWS_REGION }); +const customS3Client = new S3Client(AWS_REGION); +const awsS3Client = new S3({ region: AWS_REGION }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } async function createBucket(name: string) { - return new Promise((res, rej) => { - const params: CreateBucketRequest = { - Bucket: name, - } - awsS3Client.createBucket(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + }; + awsS3Client.createBucket(params, (err, data) => (err ? rej(err) : res(data))); + }); } beforeAll(async () => { - // Create a stack for the post model with auth enabled. - if (!fs.existsSync(LOCAL_BUILD_ROOT)) { - fs.mkdirSync(LOCAL_BUILD_ROOT); - } - await createBucket(BUCKET_NAME) - const validSchema = ` + // Create a stack for the post model with auth enabled. + if (!fs.existsSync(LOCAL_BUILD_ROOT)) { + fs.mkdirSync(LOCAL_BUILD_ROOT); + } + await createBucket(BUCKET_NAME); + const validSchema = ` # Owners and Users in writer group # can execute crud operations their owned records. type Comment @model @@ -146,208 +151,226 @@ beforeAll(async () => { id: ID! content: String secret: String @auth(rules: [{ allow: private, provider: iam }]) - }` - - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new ModelConnectionTransformer(), - new SearchableModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: 'AMAZON_COGNITO_USER_POOLS' - }, - additionalAuthenticationProviders: [ - { - authenticationType: 'API_KEY', - apiKeyConfig: { - description: 'E2E Test API Key', - apiKeyExpirationDays: 300 - } - }, - { - authenticationType: 'AWS_IAM' - }, - ], - } - }), - ] - }) - const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); - USER_POOL_ID = userPoolResponse.UserPool.Id; - const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); - const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; - try { - // Clean the bucket - let out = transformer.transform(validSchema) - - out = addIAMRolesToCFNStack(out, { - AUTH_ROLE_NAME, - UNAUTH_ROLE_NAME, - IDENTITY_POOL_NAME, - USER_POOL_CLIENTWEB_NAME, - USER_POOL_CLIENT_NAME, - USER_POOL_ID, - }) - - const params = { - CreateAPIKey: '1', - AuthCognitoUserPoolId: USER_POOL_ID - } - - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, params, - LOCAL_BUILD_ROOT, BUCKET_NAME, DEPLOYMENT_ROOT_KEY, BUILD_TIMESTAMP - ) - // Wait for any propagation to avoid random errors - await cf.wait(120, () => Promise.resolve()) - - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); - GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) - console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); - - - const apiKey = getApiKey(finishedStack.Outputs); - console.log(`API KEY: ${apiKey}`); - expect(apiKey).toBeTruthy(); - - const getIdentityPoolId = outputValueSelector('IdentityPoolId') - const identityPoolId = getIdentityPoolId(finishedStack.Outputs); - expect(identityPoolId).toBeTruthy(); - console.log(`Identity Pool Id: ${identityPoolId}`); - - - console.log(`User pool Id: ${USER_POOL_ID}`); - console.log(`User pool ClientId: ${userPoolClientId}`); - - // Verify we have all the details - expect(GRAPHQL_ENDPOINT).toBeTruthy() - expect(USER_POOL_ID).toBeTruthy() - expect(userPoolClientId).toBeTruthy() - - // Configure Amplify, create users, and sign in. - configureAmplify(USER_POOL_ID, userPoolClientId, identityPoolId); - - await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - await createGroup(USER_POOL_ID, WRITER_GROUP_NAME) - await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME) - await addUserToGroup(WRITER_GROUP_NAME, USERNAME2, USER_POOL_ID) - await addUserToGroup(ADMIN_GROUP_NAME, USERNAME2, USER_POOL_ID) - - const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - const idToken = authResAfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_1 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'userPools' - }, - auth: { - type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, - jwtToken: idToken, - }}) - - const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() - GRAPHQL_CLIENT_2 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'userPools' - }, - auth: { - type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, - jwtToken: idToken2, - }}) - - const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - const idToken3 = authRes3.getIdToken().getJwtToken() - GRAPHQL_CLIENT_3 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'userPools' + }`; + + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelConnectionTransformer(), + new SearchableModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [ + { + authenticationType: 'API_KEY', + apiKeyConfig: { + description: 'E2E Test API Key', + apiKeyExpirationDays: 300, + }, }, - auth: { - type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, - jwtToken: idToken3, - }}) - - await Auth.signIn(USERNAME1, REAL_PASSWORD); - const authCredentials = await Auth.currentUserCredentials(); - GRAPHQL_IAM_AUTH_CLIENT = new AWSAppSyncClient({ url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'iam' - }, - auth: { - type: AUTH_TYPE.AWS_IAM, - credentials: Auth.essentialCredentials(authCredentials) - } - }) - - GRAPHQL_APIKEY_CLIENT = new AWSAppSyncClient({ url: GRAPHQL_ENDPOINT, region: AWS_REGION, - disableOffline: true, - offlineConfig: { - keyPrefix: 'apikey' + { + authenticationType: 'AWS_IAM', }, - auth: { - type: AUTH_TYPE.API_KEY, - apiKey: apiKey, - } - }) - // create records for post, comment, and todo - await createRecords() - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + ], + }, + }), + ], + }); + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + try { + // Clean the bucket + let out = transformer.transform(validSchema); + + out = addIAMRolesToCFNStack(out, { + AUTH_ROLE_NAME, + UNAUTH_ROLE_NAME, + IDENTITY_POOL_NAME, + USER_POOL_CLIENTWEB_NAME, + USER_POOL_CLIENT_NAME, + USER_POOL_ID, + }); + + const params = { + CreateAPIKey: '1', + AuthCognitoUserPoolId: USER_POOL_ID, + }; + + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + params, + LOCAL_BUILD_ROOT, + BUCKET_NAME, + DEPLOYMENT_ROOT_KEY, + BUILD_TIMESTAMP + ); + // Wait for any propagation to avoid random errors + await cf.wait(120, () => Promise.resolve()); + + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs); + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + const apiKey = getApiKey(finishedStack.Outputs); + console.log(`API KEY: ${apiKey}`); + expect(apiKey).toBeTruthy(); + + const getIdentityPoolId = outputValueSelector('IdentityPoolId'); + const identityPoolId = getIdentityPoolId(finishedStack.Outputs); + expect(identityPoolId).toBeTruthy(); + console.log(`Identity Pool Id: ${identityPoolId}`); + + console.log(`User pool Id: ${USER_POOL_ID}`); + console.log(`User pool ClientId: ${userPoolClientId}`); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + expect(USER_POOL_ID).toBeTruthy(); + expect(userPoolClientId).toBeTruthy(); + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId, identityPoolId); + + await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + await createGroup(USER_POOL_ID, WRITER_GROUP_NAME); + await createGroup(USER_POOL_ID, ADMIN_GROUP_NAME); + await addUserToGroup(WRITER_GROUP_NAME, USERNAME2, USER_POOL_ID); + await addUserToGroup(ADMIN_GROUP_NAME, USERNAME2, USER_POOL_ID); + + const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + const idToken = authResAfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_1 = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools', + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken, + }, + }); + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_2 = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools', + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken2, + }, + }); + + const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + const idToken3 = authRes3.getIdToken().getJwtToken(); + GRAPHQL_CLIENT_3 = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools', + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken3, + }, + }); + + await Auth.signIn(USERNAME1, REAL_PASSWORD); + const authCredentials = await Auth.currentUserCredentials(); + GRAPHQL_IAM_AUTH_CLIENT = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'iam', + }, + auth: { + type: AUTH_TYPE.AWS_IAM, + credentials: Auth.essentialCredentials(authCredentials), + }, + }); + + GRAPHQL_APIKEY_CLIENT = new AWSAppSyncClient({ + url: GRAPHQL_ENDPOINT, + region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'apikey', + }, + auth: { + type: AUTH_TYPE.API_KEY, + apiKey: apiKey, + }, + }); + // create records for post, comment, and todo + await createRecords(); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await deleteUserPool(cognitoClient, USER_POOL_ID) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - throw e; - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await deleteUserPool(cognitoClient, USER_POOL_ID); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + throw e; } -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Input types * */ type CreateCommentInput = { - id?: string | null, - content?: string | null, + id?: string | null; + content?: string | null; }; type CreateTodoInput = { - id?: string | null, - groups?: string | null, - content?: string | null, + id?: string | null; + groups?: string | null; + content?: string | null; }; type CreatePostInput = { - id?: string | null, - content?: string | null, - secret?: string | null, + id?: string | null; + content?: string | null; + secret?: string | null; }; /** @@ -356,238 +379,254 @@ type CreatePostInput = { // cognito owner check test('test Comments as owner', async () => { - const ownerResponse: any = await GRAPHQL_CLIENT_1.query({query: gql` - query SearchComments { - searchComments { - items { - id - content - owner - } - nextToken - } - } - `}) - expect(ownerResponse.data.searchComments).toBeDefined() - expect(ownerResponse.data.searchComments.items.length).toEqual(1) - expect(ownerResponse.data.searchComments.items[0].content).toEqual('ownerContent') -}) + const ownerResponse: any = await GRAPHQL_CLIENT_1.query({ + query: gql` + query SearchComments { + searchComments { + items { + id + content + owner + } + nextToken + } + } + `, + }); + expect(ownerResponse.data.searchComments).toBeDefined(); + expect(ownerResponse.data.searchComments.items.length).toEqual(1); + expect(ownerResponse.data.searchComments.items[0].content).toEqual('ownerContent'); +}); // cognito static group check test('test Comments as user in writer group', async () => { - const writerResponse: any = await GRAPHQL_CLIENT_2.query({ query: gql` - query SearchComments { - searchComments { - items { - id - content - owner - } - nextToken - } - } - `}) - expect(writerResponse.data.searchComments).toBeDefined() - expect(writerResponse.data.searchComments.items.length).toEqual(4) - const writerItems = writerResponse.data.searchComments.items; - writerItems.forEach( (writerItem: { id: string, content: string | null, owner: string | null }) => { - expect(['ownerContent', 'content1', 'content2', 'content3']).toContain(writerItem.content) - if (writerItem.content === 'ownerContent') { - expect(writerItem.owner).toEqual(USERNAME1) - } else { - expect(writerItem.owner).toEqual(USERNAME2) + const writerResponse: any = await GRAPHQL_CLIENT_2.query({ + query: gql` + query SearchComments { + searchComments { + items { + id + content + owner + } + nextToken } - }) -}) + } + `, + }); + expect(writerResponse.data.searchComments).toBeDefined(); + expect(writerResponse.data.searchComments.items.length).toEqual(4); + const writerItems = writerResponse.data.searchComments.items; + writerItems.forEach((writerItem: { id: string; content: string | null; owner: string | null }) => { + expect(['ownerContent', 'content1', 'content2', 'content3']).toContain(writerItem.content); + if (writerItem.content === 'ownerContent') { + expect(writerItem.owner).toEqual(USERNAME1); + } else { + expect(writerItem.owner).toEqual(USERNAME2); + } + }); +}); // cognito test as unauthorized user test('test Comments as user that is not an owner nor is in writer group', async () => { - const user3Response: any = await GRAPHQL_CLIENT_3.query({query: gql` - query SearchComments { - searchComments { - items { - id - content - owner - } - nextToken - } + const user3Response: any = await GRAPHQL_CLIENT_3.query({ + query: gql` + query SearchComments { + searchComments { + items { + id + content + owner + } + nextToken } - `}) - expect(user3Response.data.searchComments).toBeDefined() - expect(user3Response.data.searchComments.items.length).toEqual(0) - expect(user3Response.data.searchComments.nextToken).toBeNull() -}) + } + `, + }); + expect(user3Response.data.searchComments).toBeDefined(); + expect(user3Response.data.searchComments.items.length).toEqual(0); + expect(user3Response.data.searchComments.nextToken).toBeNull(); +}); // cognito dynamic group check test('test Todo as user in the dynamic group admin', async () => { - const adminResponse: any = await GRAPHQL_CLIENT_2.query({ query: gql` - query SearchTodos { - searchTodos { - items { - id - groups - content - } - nextToken - } + const adminResponse: any = await GRAPHQL_CLIENT_2.query({ + query: gql` + query SearchTodos { + searchTodos { + items { + id + groups + content + } + nextToken } - `}) - expect(adminResponse.data.searchTodos).toBeDefined() - expect(adminResponse.data.searchTodos.items.length).toEqual(3) - const adminItems = adminResponse.data.searchTodos.items - adminItems.forEach( (adminItem: { id: string, content: string, groups: string }) => { - expect(['adminContent1', 'adminContent2', 'adminContent3']).toContain(adminItem.content) - expect(adminItem.groups).toEqual('admin') - }) -}) + } + `, + }); + expect(adminResponse.data.searchTodos).toBeDefined(); + expect(adminResponse.data.searchTodos.items.length).toEqual(3); + const adminItems = adminResponse.data.searchTodos.items; + adminItems.forEach((adminItem: { id: string; content: string; groups: string }) => { + expect(['adminContent1', 'adminContent2', 'adminContent3']).toContain(adminItem.content); + expect(adminItem.groups).toEqual('admin'); + }); +}); // iam test test('test Post as authorized user', async () => { - const authUser: any = await GRAPHQL_IAM_AUTH_CLIENT.query({ query: gql` - query SearchPosts { - searchPosts{ - items { - id - content - secret - } - nextToken - } + const authUser: any = await GRAPHQL_IAM_AUTH_CLIENT.query({ + query: gql` + query SearchPosts { + searchPosts { + items { + id + content + secret + } + nextToken } - `}) - expect(authUser.data.searchPosts).toBeDefined() - expect(authUser.data.searchPosts.items.length).toEqual(4) - const authUserItems = authUser.data.searchPosts.items - authUserItems.forEach( (authUserItem: { id: string, content: string, secret: string }) => { - expect(['publicPost', 'post1', 'post2', 'post3']).toContain(authUserItem.content) - expect(['notViewableToPublic', 'post1secret', 'post2secret', 'post3secret']).toContain(authUserItem.secret) - }) -}) + } + `, + }); + expect(authUser.data.searchPosts).toBeDefined(); + expect(authUser.data.searchPosts.items.length).toEqual(4); + const authUserItems = authUser.data.searchPosts.items; + authUserItems.forEach((authUserItem: { id: string; content: string; secret: string }) => { + expect(['publicPost', 'post1', 'post2', 'post3']).toContain(authUserItem.content); + expect(['notViewableToPublic', 'post1secret', 'post2secret', 'post3secret']).toContain(authUserItem.secret); + }); +}); // test apikey 2nd scenario test('test searchPosts with apikey and secret removed', async () => { - const apiKeyResponse: any = await GRAPHQL_APIKEY_CLIENT.query({ query: gql` - query SearchPosts { - searchPosts{ - items { - id - content - } - nextToken - } - } - `}) - expect(apiKeyResponse.data.searchPosts).toBeDefined() - const apiKeyResponseItems = apiKeyResponse.data.searchPosts.items - apiKeyResponseItems.forEach( (item: { id: string, content: string | null }) => { - expect(item.id).toBeDefined() - if (item.content) { - expect(['publicPost', 'post1', 'post2', 'post3']).toContain(item.content) + const apiKeyResponse: any = await GRAPHQL_APIKEY_CLIENT.query({ + query: gql` + query SearchPosts { + searchPosts { + items { + id + content + } + nextToken } - }) -}) + } + `, + }); + expect(apiKeyResponse.data.searchPosts).toBeDefined(); + const apiKeyResponseItems = apiKeyResponse.data.searchPosts.items; + apiKeyResponseItems.forEach((item: { id: string; content: string | null }) => { + expect(item.id).toBeDefined(); + if (item.content) { + expect(['publicPost', 'post1', 'post2', 'post3']).toContain(item.content); + } + }); +}); // test iam/apiKey schema with unauth user test('test post as an cognito user that is not allowed in this schema', async () => { - try { - await GRAPHQL_CLIENT_3.query({query: gql` + try { + await GRAPHQL_CLIENT_3.query({ + query: gql` query SearchPosts { - searchPosts{ - items { - id - content - secret - } - nextToken + searchPosts { + items { + id + content + secret } + nextToken + } } - `}) - } catch (err) { - console.log(err) - expect(err.graphQLErrors[0].errorType).toEqual('Unauthorized') - expect(err.graphQLErrors[0].message).toEqual('Not Authorized to access searchPosts on type Query') - } -}) + `, + }); + } catch (err) { + console.log(err); + expect(err.graphQLErrors[0].errorType).toEqual('Unauthorized'); + expect(err.graphQLErrors[0].message).toEqual('Not Authorized to access searchPosts on type Query'); + } +}); // mutations async function createComment(client: AWSAppSyncClient, input: CreateCommentInput) { - const create = gql`mutation CreateComment($input: CreateCommentInput!) { - createComment(input: $input) { - id - content - owner - } - }` - return await client.mutate({ mutation: create, variables: { input } }); + const create = gql` + mutation CreateComment($input: CreateCommentInput!) { + createComment(input: $input) { + id + content + owner + } + } + `; + return await client.mutate({ mutation: create, variables: { input } }); } async function createTodo(client: AWSAppSyncClient, input: CreateTodoInput) { - const create = gql`mutation CreateTodo($input: CreateTodoInput!) { - createTodo(input: $input) { - id - groups - content - } - }` - return await client.mutate({ mutation: create, variables: { input } }); + const create = gql` + mutation CreateTodo($input: CreateTodoInput!) { + createTodo(input: $input) { + id + groups + content + } + } + `; + return await client.mutate({ mutation: create, variables: { input } }); } async function createPost(client: AWSAppSyncClient, input: CreatePostInput) { - const create = gql`mutation CreatePost($input: CreatePostInput!) { - createPost(input: $input) { - id - content - } - }` - return await client.mutate({ mutation: create, variables: { input } }); + const create = gql` + mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + content + } + } + `; + return await client.mutate({ mutation: create, variables: { input } }); } async function createRecords() { - console.log('create records') - try { - // create a comment as an owner not in the writer group - const ownerCreate = await createComment(GRAPHQL_CLIENT_1, { - content: 'ownerContent' - }) - console.log(ownerCreate) - // create comments as user 2 that is in the writer group - const createCommentList: CreateCommentInput[] = [ - { content: 'content1' }, - { content: 'content2' }, - { content: 'content3' } - ] - createCommentList.forEach( async (commentInput: CreateCommentInput ) => { - await createComment(GRAPHQL_CLIENT_2, commentInput) - }) - // create todos as user in the admin group - const createTodoList: CreateTodoInput[] = [ - { groups: 'admin', content: 'adminContent1' }, - { groups: 'admin', content: 'adminContent2' }, - { groups: 'admin', content: 'adminContent3' } - ] - createTodoList.forEach( async (todoInput: CreateTodoInput) => { - await createTodo(GRAPHQL_CLIENT_2, todoInput) - }) - // create a post as a user with the apiKey - const apiKeyResponse = await createPost(GRAPHQL_APIKEY_CLIENT, { - content: 'publicPost', - secret: 'notViewableToPublic' - }) - console.log(apiKeyResponse) - // create posts as user that has auth role - const createPostList: CreatePostInput[] = [ - { content: 'post1', secret: 'post1secret' }, - { content: 'post2', secret: 'post2secret' }, - { content: 'post3', secret: 'post3secret' }, - ] - createPostList.forEach( async (postInput: CreatePostInput) => { - await createPost(GRAPHQL_IAM_AUTH_CLIENT, postInput); - }) - // Waiting for the ES Cluster + Streaming Lambda infra to be setup - console.log('Waiting for the ES Cluster + Streaming Lambda infra to be setup') - await cf.wait(120, () => Promise.resolve()) - } catch (err) { - console.log(err) - } -} \ No newline at end of file + console.log('create records'); + try { + // create a comment as an owner not in the writer group + const ownerCreate = await createComment(GRAPHQL_CLIENT_1, { + content: 'ownerContent', + }); + console.log(ownerCreate); + // create comments as user 2 that is in the writer group + const createCommentList: CreateCommentInput[] = [{ content: 'content1' }, { content: 'content2' }, { content: 'content3' }]; + createCommentList.forEach(async (commentInput: CreateCommentInput) => { + await createComment(GRAPHQL_CLIENT_2, commentInput); + }); + // create todos as user in the admin group + const createTodoList: CreateTodoInput[] = [ + { groups: 'admin', content: 'adminContent1' }, + { groups: 'admin', content: 'adminContent2' }, + { groups: 'admin', content: 'adminContent3' }, + ]; + createTodoList.forEach(async (todoInput: CreateTodoInput) => { + await createTodo(GRAPHQL_CLIENT_2, todoInput); + }); + // create a post as a user with the apiKey + const apiKeyResponse = await createPost(GRAPHQL_APIKEY_CLIENT, { + content: 'publicPost', + secret: 'notViewableToPublic', + }); + console.log(apiKeyResponse); + // create posts as user that has auth role + const createPostList: CreatePostInput[] = [ + { content: 'post1', secret: 'post1secret' }, + { content: 'post2', secret: 'post2secret' }, + { content: 'post3', secret: 'post3secret' }, + ]; + createPostList.forEach(async (postInput: CreatePostInput) => { + await createPost(GRAPHQL_IAM_AUTH_CLIENT, postInput); + }); + // Waiting for the ES Cluster + Streaming Lambda infra to be setup + console.log('Waiting for the ES Cluster + Streaming Lambda infra to be setup'); + await cf.wait(120, () => Promise.resolve()); + } catch (err) { + console.log(err); + } +} diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts index e99810e02d..e69f4b1dad 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/TestComplexStackMappingsLocal.e2e.test.ts @@ -1,16 +1,20 @@ -import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core' +import GraphQLTransform, { Transformer, InvalidDirectiveError } from 'graphql-transformer-core'; import ModelTransformer from 'graphql-dynamodb-transformer'; import ElasticsearchTransformer from 'graphql-elasticsearch-transformer'; import ConnectionTransformer from 'graphql-connection-transformer'; import HttpTransformer from 'graphql-http-transformer'; import AuthTransformer from 'graphql-auth-transformer'; import FunctionTransformer from 'graphql-function-transformer'; -import Template from "cloudform-types/types/template"; -import { parse, FieldDefinitionNode, ObjectTypeDefinitionNode, - Kind, InputObjectTypeDefinitionNode } from 'graphql'; -import { expectExactKeys, expectNonNullFields, expectNullableFields, - expectNonNullInputValues, expectNullableInputValues, expectInputValueToHandle } from '../testUtil'; - +import Template from 'cloudform-types/types/template'; +import { parse, FieldDefinitionNode, ObjectTypeDefinitionNode, Kind, InputObjectTypeDefinitionNode } from 'graphql'; +import { + expectExactKeys, + expectNonNullFields, + expectNullableFields, + expectNonNullInputValues, + expectNullableInputValues, + expectInputValueToHandle, +} from '../testUtil'; const userType = ` type User @model @auth(rules: [{ allow: owner }]) { @@ -40,138 +44,144 @@ type Post @model @searchable { * that caused the order to impact the stack that a resource got mapped to. */ test('Test that every resource exists in the correct stack given a complex schema with overlapping names.', () => { - const schema = [userType, userPostType, postType].join('\n'); - transpileAndCheck(schema); -}) + const schema = [userType, userPostType, postType].join('\n'); + transpileAndCheck(schema); +}); test('Test that every resource exists in the correct stack given a complex schema with overlapping names. Rotation 1.', () => { - const schema = [userPostType, postType, userType].join('\n'); - transpileAndCheck(schema); -}) + const schema = [userPostType, postType, userType].join('\n'); + transpileAndCheck(schema); +}); test('Test that every resource exists in the correct stack given a complex schema with overlapping names. Rotation 2.', () => { - const schema = [postType, userType, userPostType].join('\n'); - transpileAndCheck(schema); -}) - + const schema = [postType, userType, userPostType].join('\n'); + transpileAndCheck(schema); +}); function transpileAndCheck(schema: string) { - const transformer = new GraphQLTransform({ - transformers: [ - new ModelTransformer(), - new HttpTransformer(), - new ElasticsearchTransformer(), - new ConnectionTransformer(), - new FunctionTransformer, - new AuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "AMAZON_COGNITO_USER_POOLS" - }, - additionalAuthenticationProviders: [] - }}), - ] - }); - - const out = transformer.transform(schema); - - // Check root - expectExactKeys( - out.rootStack.Resources, - new Set([ - 'GraphQLAPI', 'GraphQLAPIKey', 'GraphQLSchema', 'User', 'UserPost', - 'Post', 'ConnectionStack', 'SearchableStack', 'FunctionDirectiveStack', - 'HttpStack', 'NoneDataSource' - ]) - ); - expectExactKeys( - out.rootStack.Outputs, - new Set(['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput', 'GraphQLAPIKeyOutput']) - ); - - // Check User - expectExactKeys( - out.stacks.User.Resources, - new Set([ - 'UserTable', 'UserIAMRole', 'UserDataSource', 'GetUserResolver', - 'ListUserResolver', 'CreateUserResolver', 'UpdateUserResolver', - 'DeleteUserResolver', 'SubscriptiononCreateUserResolver', 'SubscriptiononDeleteUserResolver', - 'SubscriptiononUpdateUserResolver' - ]) - ); - expectExactKeys( - out.stacks.User.Outputs, - new Set(['GetAttUserTableStreamArn', 'GetAttUserDataSourceName', 'GetAttUserTableName']) - ); - - // Check UserPost - expectExactKeys( - out.stacks.UserPost.Resources, - new Set([ - 'UserPostTable', 'UserPostIAMRole', 'UserPostDataSource', 'GetUserPostResolver', - 'ListUserPostResolver', 'CreateUserPostResolver', 'UpdateUserPostResolver', - 'DeleteUserPostResolver' - ]) - ); - expectExactKeys( - out.stacks.UserPost.Outputs, - new Set(['GetAttUserPostTableStreamArn', 'GetAttUserPostDataSourceName', 'GetAttUserPostTableName']) - ); - - // Check Post - expectExactKeys( - out.stacks.Post.Resources, - new Set([ - 'PostTable', 'PostIAMRole', 'PostDataSource', 'GetPostResolver', - 'ListPostResolver', 'CreatePostResolver', 'UpdatePostResolver', - 'DeletePostResolver' - ]) - ); - expectExactKeys( - out.stacks.Post.Outputs, - new Set(['GetAttPostTableStreamArn', 'GetAttPostDataSourceName', 'GetAttPostTableName']) - ); - - // Check SearchableStack - expectExactKeys( - out.stacks.SearchableStack.Resources, - new Set([ - 'ElasticSearchAccessIAMRole', 'ElasticSearchDataSource', 'ElasticSearchDomain', 'ElasticSearchStreamingLambdaIAMRole', - 'ElasticSearchStreamingLambdaFunction', 'SearchablePostLambdaMapping', 'SearchPostResolver' - ]) - ); - expectExactKeys( - out.stacks.SearchableStack.Outputs, - new Set(['ElasticsearchDomainArn', 'ElasticsearchDomainEndpoint']) - ); - - // Check connections - expectExactKeys( - out.stacks.ConnectionStack.Resources, - new Set(['UserpostsResolver', 'UserPostuserResolver', 'UserPostpostResolver', 'PostauthorsResolver']) - ); - expectExactKeys( - out.stacks.ConnectionStack.Outputs, - new Set([]) - ); - - // Check function stack - expectExactKeys( - out.stacks.FunctionDirectiveStack.Resources, - new Set(['ScorefuncLambdaDataSourceRole', 'ScorefuncLambdaDataSource', 'InvokeScorefuncLambdaDataSource', 'PostscoreResolver']) - ); - expectExactKeys( - out.stacks.ConnectionStack.Outputs, - new Set([]) - ); - - // Check http stack - expectExactKeys( - out.stacks.HttpStack.Resources, - new Set(['httpswwwprofpicorgDataSource', 'UserprofpicResolver']) - ) - expectExactKeys( - out.stacks.HttpStack.Outputs, - new Set([]) - ); + const transformer = new GraphQLTransform({ + transformers: [ + new ModelTransformer(), + new HttpTransformer(), + new ElasticsearchTransformer(), + new ConnectionTransformer(), + new FunctionTransformer(), + new AuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + + const out = transformer.transform(schema); + + // Check root + expectExactKeys( + out.rootStack.Resources, + new Set([ + 'GraphQLAPI', + 'GraphQLAPIKey', + 'GraphQLSchema', + 'User', + 'UserPost', + 'Post', + 'ConnectionStack', + 'SearchableStack', + 'FunctionDirectiveStack', + 'HttpStack', + 'NoneDataSource', + ]) + ); + expectExactKeys(out.rootStack.Outputs, new Set(['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput', 'GraphQLAPIKeyOutput'])); + + // Check User + expectExactKeys( + out.stacks.User.Resources, + new Set([ + 'UserTable', + 'UserIAMRole', + 'UserDataSource', + 'GetUserResolver', + 'ListUserResolver', + 'CreateUserResolver', + 'UpdateUserResolver', + 'DeleteUserResolver', + 'SubscriptiononCreateUserResolver', + 'SubscriptiononDeleteUserResolver', + 'SubscriptiononUpdateUserResolver', + ]) + ); + expectExactKeys(out.stacks.User.Outputs, new Set(['GetAttUserTableStreamArn', 'GetAttUserDataSourceName', 'GetAttUserTableName'])); + + // Check UserPost + expectExactKeys( + out.stacks.UserPost.Resources, + new Set([ + 'UserPostTable', + 'UserPostIAMRole', + 'UserPostDataSource', + 'GetUserPostResolver', + 'ListUserPostResolver', + 'CreateUserPostResolver', + 'UpdateUserPostResolver', + 'DeleteUserPostResolver', + ]) + ); + expectExactKeys( + out.stacks.UserPost.Outputs, + new Set(['GetAttUserPostTableStreamArn', 'GetAttUserPostDataSourceName', 'GetAttUserPostTableName']) + ); + + // Check Post + expectExactKeys( + out.stacks.Post.Resources, + new Set([ + 'PostTable', + 'PostIAMRole', + 'PostDataSource', + 'GetPostResolver', + 'ListPostResolver', + 'CreatePostResolver', + 'UpdatePostResolver', + 'DeletePostResolver', + ]) + ); + expectExactKeys(out.stacks.Post.Outputs, new Set(['GetAttPostTableStreamArn', 'GetAttPostDataSourceName', 'GetAttPostTableName'])); + + // Check SearchableStack + expectExactKeys( + out.stacks.SearchableStack.Resources, + new Set([ + 'ElasticSearchAccessIAMRole', + 'ElasticSearchDataSource', + 'ElasticSearchDomain', + 'ElasticSearchStreamingLambdaIAMRole', + 'ElasticSearchStreamingLambdaFunction', + 'SearchablePostLambdaMapping', + 'SearchPostResolver', + ]) + ); + expectExactKeys(out.stacks.SearchableStack.Outputs, new Set(['ElasticsearchDomainArn', 'ElasticsearchDomainEndpoint'])); + + // Check connections + expectExactKeys( + out.stacks.ConnectionStack.Resources, + new Set(['UserpostsResolver', 'UserPostuserResolver', 'UserPostpostResolver', 'PostauthorsResolver']) + ); + expectExactKeys(out.stacks.ConnectionStack.Outputs, new Set([])); + + // Check function stack + expectExactKeys( + out.stacks.FunctionDirectiveStack.Resources, + new Set(['ScorefuncLambdaDataSourceRole', 'ScorefuncLambdaDataSource', 'InvokeScorefuncLambdaDataSource', 'PostscoreResolver']) + ); + expectExactKeys(out.stacks.ConnectionStack.Outputs, new Set([])); + + // Check http stack + expectExactKeys(out.stacks.HttpStack.Resources, new Set(['httpswwwprofpicorgDataSource', 'UserprofpicResolver'])); + expectExactKeys(out.stacks.HttpStack.Outputs, new Set([])); } diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts index 45325c2d07..29a8fa77f9 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/VersionedModelTransformer.e2e.test.ts @@ -1,41 +1,41 @@ -import { ResourceConstants } from 'graphql-transformer-common' -import GraphQLTransform from 'graphql-transformer-core' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' -import VersionedModelTransformer from 'graphql-versioned-transformer' -import ModelAuthTransformer from 'graphql-auth-transformer' -import { CloudFormationClient } from '../CloudFormationClient' -import { Output } from 'aws-sdk/clients/cloudformation' -import { GraphQLClient } from '../GraphQLClient' +import { ResourceConstants } from 'graphql-transformer-common'; +import GraphQLTransform from 'graphql-transformer-core'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import VersionedModelTransformer from 'graphql-versioned-transformer'; +import ModelAuthTransformer from 'graphql-auth-transformer'; +import { CloudFormationClient } from '../CloudFormationClient'; +import { Output } from 'aws-sdk/clients/cloudformation'; +import { GraphQLClient } from '../GraphQLClient'; import * as moment from 'moment'; -import * as S3 from 'aws-sdk/clients/s3' +import * as S3 from 'aws-sdk/clients/s3'; import { S3Client } from '../S3Client'; import { deploy } from '../deployNestedStacks'; import emptyBucket from '../emptyBucket'; jest.setTimeout(2000000); -const cf = new CloudFormationClient('us-west-2') +const cf = new CloudFormationClient('us-west-2'); -const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') -const STACK_NAME = `VersionedTest-${BUILD_TIMESTAMP}` -const BUCKET_NAME = `versioned-test-bucket-${BUILD_TIMESTAMP}` -const LOCAL_FS_BUILD_DIR = '/tmp/model_transform_versioned_tests/' -const S3_ROOT_DIR_KEY = 'deployments' +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss'); +const STACK_NAME = `VersionedTest-${BUILD_TIMESTAMP}`; +const BUCKET_NAME = `versioned-test-bucket-${BUILD_TIMESTAMP}`; +const LOCAL_FS_BUILD_DIR = '/tmp/model_transform_versioned_tests/'; +const S3_ROOT_DIR_KEY = 'deployments'; let GRAPHQL_CLIENT = undefined; -const customS3Client = new S3Client('us-west-2') -const awsS3Client = new S3({ region: 'us-west-2' }) +const customS3Client = new S3Client('us-west-2'); +const awsS3Client = new S3({ region: 'us-west-2' }); function outputValueSelector(key: string) { - return (outputs: Output[]) => { - const output = outputs.find((o: Output) => o.OutputKey === key) - return output ? output.OutputValue : null - } + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key); + return output ? output.OutputValue : null; + }; } beforeAll(async () => { - const validSchema = ` + const validSchema = ` type Post @model @versioned { id: ID! title: String! @@ -43,80 +43,89 @@ beforeAll(async () => { createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new VersionedModelTransformer(), - new ModelAuthTransformer({ - authConfig: { - defaultAuthentication: { - authenticationType: "API_KEY" - }, - additionalAuthenticationProviders: [] - }}), - ] - }) - try { - await awsS3Client.createBucket({Bucket: BUCKET_NAME}).promise() - } catch (e) { - console.error(`Failed to create bucket: ${e}`) - } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new VersionedModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [], + }, + }), + ], + }); + try { + await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); + } catch (e) { + console.error(`Failed to create bucket: ${e}`); + } - try { - const out = transformer.transform(validSchema); - console.log('Creating Stack ' + STACK_NAME) - const finishedStack = await deploy( - customS3Client, cf, STACK_NAME, out, { CreateAPIKey: '1' }, LOCAL_FS_BUILD_DIR, BUCKET_NAME, S3_ROOT_DIR_KEY, - BUILD_TIMESTAMP - ) - expect(finishedStack).toBeDefined() + try { + const out = transformer.transform(validSchema); + console.log('Creating Stack ' + STACK_NAME); + const finishedStack = await deploy( + customS3Client, + cf, + STACK_NAME, + out, + { CreateAPIKey: '1' }, + LOCAL_FS_BUILD_DIR, + BUCKET_NAME, + S3_ROOT_DIR_KEY, + BUILD_TIMESTAMP + ); + expect(finishedStack).toBeDefined(); - // Arbitrary wait to make sure everything is ready. - //await cf.wait(10, () => Promise.resolve()) - console.log('Successfully created stack ' + STACK_NAME) - expect(finishedStack).toBeDefined() - const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) - const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput) - const endpoint = getApiEndpoint(finishedStack.Outputs) - const apiKey = getApiKey(finishedStack.Outputs) - expect(apiKey).toBeDefined() - expect(endpoint).toBeDefined() - GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }) - } catch (e) { - console.error(e) - expect(true).toEqual(false) - } + // Arbitrary wait to make sure everything is ready. + //await cf.wait(10, () => Promise.resolve()) + console.log('Successfully created stack ' + STACK_NAME); + expect(finishedStack).toBeDefined(); + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput); + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + const endpoint = getApiEndpoint(finishedStack.Outputs); + const apiKey = getApiKey(finishedStack.Outputs); + expect(apiKey).toBeDefined(); + expect(endpoint).toBeDefined(); + GRAPHQL_CLIENT = new GraphQLClient(endpoint, { 'x-api-key': apiKey }); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } }); afterAll(async () => { - try { - console.log('Deleting stack ' + STACK_NAME) - await cf.deleteStack(STACK_NAME) - await cf.waitForStack(STACK_NAME) - console.log('Successfully deleted stack ' + STACK_NAME) - } catch (e) { - if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { - // The stack was deleted. This is good. - expect(true).toEqual(true) - console.log('Successfully deleted stack ' + STACK_NAME) - } else { - console.error(e) - expect(true).toEqual(false) - } - } - try { - await emptyBucket(BUCKET_NAME); - } catch (e) { - console.error(`Failed to empty S3 bucket: ${e}`) + try { + console.log('Deleting stack ' + STACK_NAME); + await cf.deleteStack(STACK_NAME); + await cf.waitForStack(STACK_NAME); + console.log('Successfully deleted stack ' + STACK_NAME); + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true); + console.log('Successfully deleted stack ' + STACK_NAME); + } else { + console.error(e); + expect(true).toEqual(false); } -}) + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`); + } +}); /** * Test queries below */ test('Test createPost mutation', async () => { - const response = await GRAPHQL_CLIENT.query(`mutation { + const response = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Hello, World!" }) { id title @@ -124,16 +133,19 @@ test('Test createPost mutation', async () => { updatedAt version } - }`, {}) - expect(response.data.createPost.id).toBeDefined() - expect(response.data.createPost.title).toEqual('Hello, World!') - expect(response.data.createPost.createdAt).toBeDefined() - expect(response.data.createPost.updatedAt).toBeDefined() - expect(response.data.createPost.version).toEqual(1) -}) + }`, + {} + ); + expect(response.data.createPost.id).toBeDefined(); + expect(response.data.createPost.title).toEqual('Hello, World!'); + expect(response.data.createPost.createdAt).toBeDefined(); + expect(response.data.createPost.updatedAt).toBeDefined(); + expect(response.data.createPost.version).toEqual(1); +}); test('Test updatePost mutation', async () => { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Update" }) { id title @@ -141,11 +153,14 @@ test('Test updatePost mutation', async () => { updatedAt version } - }`, {}) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test Update') - expect(createResponse.data.createPost.version).toEqual(1) - const updateResponse = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test Update'); + expect(createResponse.data.createPost.version).toEqual(1); + const updateResponse = await GRAPHQL_CLIENT.query( + `mutation { updatePost(input: { id: "${createResponse.data.createPost.id}", title: "Bye, World!", @@ -155,13 +170,16 @@ test('Test updatePost mutation', async () => { title version } - }`, {}) - expect(updateResponse.data.updatePost.title).toEqual('Bye, World!') - expect(updateResponse.data.updatePost.version).toEqual(2) -}) + }`, + {} + ); + expect(updateResponse.data.updatePost.title).toEqual('Bye, World!'); + expect(updateResponse.data.updatePost.version).toEqual(2); +}); test('Test failed updatePost mutation with wrong version', async () => { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Update" }) { id title @@ -169,11 +187,14 @@ test('Test failed updatePost mutation with wrong version', async () => { updatedAt version } - }`, {}) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test Update') - expect(createResponse.data.createPost.version).toEqual(1) - const updateResponse = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test Update'); + expect(createResponse.data.createPost.version).toEqual(1); + const updateResponse = await GRAPHQL_CLIENT.query( + `mutation { updatePost(input: { id: "${createResponse.data.createPost.id}", title: "Bye, World!", @@ -183,13 +204,16 @@ test('Test failed updatePost mutation with wrong version', async () => { title version } - }`, {}) - expect(updateResponse.errors.length).toEqual(1) - expect((updateResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException') -}) + }`, + {} + ); + expect(updateResponse.errors.length).toEqual(1); + expect((updateResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); +}); test('Test deletePost mutation', async () => { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Delete" }) { id title @@ -197,23 +221,29 @@ test('Test deletePost mutation', async () => { createdAt updatedAt } - }`, {}) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test Delete') - expect(createResponse.data.createPost.version).toBeDefined() - const deleteResponse = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test Delete'); + expect(createResponse.data.createPost.version).toBeDefined(); + const deleteResponse = await GRAPHQL_CLIENT.query( + `mutation { deletePost(input: { id: "${createResponse.data.createPost.id}", expectedVersion: ${createResponse.data.createPost.version} }) { id title version } - }`, {}) - expect(deleteResponse.data.deletePost.title).toEqual('Test Delete') - expect(deleteResponse.data.deletePost.version).toEqual(createResponse.data.createPost.version) -}) + }`, + {} + ); + expect(deleteResponse.data.deletePost.title).toEqual('Test Delete'); + expect(deleteResponse.data.deletePost.version).toEqual(createResponse.data.createPost.version); +}); test('Test deletePost mutation with wrong version', async () => { - const createResponse = await GRAPHQL_CLIENT.query(`mutation { + const createResponse = await GRAPHQL_CLIENT.query( + `mutation { createPost(input: { title: "Test Delete" }) { id title @@ -221,17 +251,22 @@ test('Test deletePost mutation with wrong version', async () => { createdAt updatedAt } - }`, {}) - expect(createResponse.data.createPost.id).toBeDefined() - expect(createResponse.data.createPost.title).toEqual('Test Delete') - expect(createResponse.data.createPost.version).toBeDefined() - const deleteResponse = await GRAPHQL_CLIENT.query(`mutation { + }`, + {} + ); + expect(createResponse.data.createPost.id).toBeDefined(); + expect(createResponse.data.createPost.title).toEqual('Test Delete'); + expect(createResponse.data.createPost.version).toBeDefined(); + const deleteResponse = await GRAPHQL_CLIENT.query( + `mutation { deletePost(input: { id: "${createResponse.data.createPost.id}", expectedVersion: 3 }) { id title version } - }`, {}) - expect(deleteResponse.errors.length).toEqual(1) - expect((deleteResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException') -}) + }`, + {} + ); + expect(deleteResponse.errors.length).toEqual(1); + expect((deleteResponse.errors[0] as any).errorType).toEqual('DynamoDB:ConditionalCheckFailedException'); +}); diff --git a/packages/graphql-transformers-e2e-tests/src/cognitoUtils.ts b/packages/graphql-transformers-e2e-tests/src/cognitoUtils.ts index 34e70c54d0..c91f9b1b31 100644 --- a/packages/graphql-transformers-e2e-tests/src/cognitoUtils.ts +++ b/packages/graphql-transformers-e2e-tests/src/cognitoUtils.ts @@ -1,387 +1,396 @@ import Amplify from 'aws-amplify'; import { - CreateGroupRequest, CreateGroupResponse, - AdminAddUserToGroupRequest, CreateUserPoolResponse, - CreateUserPoolRequest, CreateUserPoolClientRequest, - CreateUserPoolClientResponse, DeleteUserPoolRequest, DeleteUserRequest, -} from 'aws-sdk/clients/cognitoidentityserviceprovider' -import { ResourceConstants } from 'graphql-transformer-common' + CreateGroupRequest, + CreateGroupResponse, + AdminAddUserToGroupRequest, + CreateUserPoolResponse, + CreateUserPoolRequest, + CreateUserPoolClientRequest, + CreateUserPoolClientResponse, + DeleteUserPoolRequest, + DeleteUserRequest, +} from 'aws-sdk/clients/cognitoidentityserviceprovider'; +import { ResourceConstants } from 'graphql-transformer-common'; import { IAM as cfnIAM, Cognito as cfnCognito } from 'cloudform-types'; -import { - AuthenticationDetails, -} from 'amazon-cognito-identity-js'; -import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' -import TestStorage from './TestStorage' +import { AuthenticationDetails } from 'amazon-cognito-identity-js'; +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider'; +import TestStorage from './TestStorage'; import DeploymentResources from 'graphql-transformer-core/lib/DeploymentResources'; interface E2Econfiguration { - STACK_NAME?: string, - AUTH_ROLE_NAME?: string, - UNAUTH_ROLE_NAME?: string, - IDENTITY_POOL_NAME?: string, - USER_POOL_CLIENTWEB_NAME?: string, - USER_POOL_CLIENT_NAME?: string, - USER_POOL_ID?: string, + STACK_NAME?: string; + AUTH_ROLE_NAME?: string; + UNAUTH_ROLE_NAME?: string; + IDENTITY_POOL_NAME?: string; + USER_POOL_CLIENTWEB_NAME?: string; + USER_POOL_CLIENT_NAME?: string; + USER_POOL_ID?: string; } -const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }) +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: 'us-west-2' }); export function configureAmplify(userPoolId: string, userPoolClientId: string, identityPoolId?: string) { - Amplify.configure({ - Auth: { - // REQUIRED - Amazon Cognito Region - region: 'us-west-2', - userPoolId: userPoolId, - userPoolWebClientId: userPoolClientId, - storage: new TestStorage(), - identityPoolId: identityPoolId - } - }) + Amplify.configure({ + Auth: { + // REQUIRED - Amazon Cognito Region + region: 'us-west-2', + userPoolId: userPoolId, + userPoolWebClientId: userPoolClientId, + storage: new TestStorage(), + identityPoolId: identityPoolId, + }, + }); } export async function signupUser(userPoolId: string, name: string, pw: string) { - return new Promise((res, rej) => { - const createUser = cognitoClient.adminCreateUser.bind(cognitoClient) as any; - createUser({ - UserPoolId: userPoolId, - UserAttributes: [{ Name: 'email', Value: name }], - Username: name, - TemporaryPassword: pw - }, (err, data) => err ? rej(err) : res(data)); - }) + return new Promise((res, rej) => { + const createUser = cognitoClient.adminCreateUser.bind(cognitoClient) as any; + createUser( + { + UserPoolId: userPoolId, + UserAttributes: [{ Name: 'email', Value: name }], + Username: name, + TemporaryPassword: pw, + }, + (err, data) => (err ? rej(err) : res(data)) + ); + }); } export async function authenticateUser(user: any, details: any, realPw: string) { - return new Promise((res, rej) => { - user.authenticateUser(details, { - onSuccess: function (result: any) { - res(result) - }, - onFailure: function (err: any) { - rej(err) - }, - newPasswordRequired: function (userAttributes: any, requiredAttributes: any) { - user.completeNewPasswordChallenge(realPw, user.Attributes, this) - } - }); - }) + return new Promise((res, rej) => { + user.authenticateUser(details, { + onSuccess: function(result: any) { + res(result); + }, + onFailure: function(err: any) { + rej(err); + }, + newPasswordRequired: function(userAttributes: any, requiredAttributes: any) { + user.completeNewPasswordChallenge(realPw, user.Attributes, this); + }, + }); + }); } export async function signupAndAuthenticateUser(userPoolId: string, username: string, tmpPw: string, realPw: string) { - try { - // Sign up then login user 1.ß - await signupUser(userPoolId, username, tmpPw) - } catch (e) { - console.log(`Trying to login with temp password`) - } + try { + // Sign up then login user 1.ß + await signupUser(userPoolId, username, tmpPw); + } catch (e) { + console.log(`Trying to login with temp password`); + } - try { - const authDetails = new AuthenticationDetails({ - Username: username, - Password: tmpPw - }); - const user = Amplify.Auth.createCognitoUser(username) - const authRes = await authenticateUser(user, authDetails, realPw); - return authRes; - } catch (e) { console.log(`Trying to login with real password`) } + try { + const authDetails = new AuthenticationDetails({ + Username: username, + Password: tmpPw, + }); + const user = Amplify.Auth.createCognitoUser(username); + const authRes = await authenticateUser(user, authDetails, realPw); + return authRes; + } catch (e) { + console.log(`Trying to login with real password`); + } - try { - const authDetails = new AuthenticationDetails({ - Username: username, - Password: realPw - }); - const user = Amplify.Auth.createCognitoUser(username) - const authRes: any = await authenticateUser(user, authDetails, realPw); - console.log(`Logged in ${username} \n${authRes.getIdToken().getJwtToken()}`) - return authRes; - } catch (e) { - console.error(`Failed to login.\n`) - console.error(e) - } + try { + const authDetails = new AuthenticationDetails({ + Username: username, + Password: realPw, + }); + const user = Amplify.Auth.createCognitoUser(username); + const authRes: any = await authenticateUser(user, authDetails, realPw); + console.log(`Logged in ${username} \n${authRes.getIdToken().getJwtToken()}`); + return authRes; + } catch (e) { + console.error(`Failed to login.\n`); + console.error(e); + } } export async function deleteUser(accessToken: string): Promise<{}> { - return new Promise((res, rej) => { - const params: DeleteUserRequest = { - AccessToken: accessToken - }; - cognitoClient.deleteUser(params, (err, data) => err ? rej(err) : res(data)); - }) + return new Promise((res, rej) => { + const params: DeleteUserRequest = { + AccessToken: accessToken, + }; + cognitoClient.deleteUser(params, (err, data) => (err ? rej(err) : res(data))); + }); } export async function createGroup(userPoolId: string, name: string): Promise { - return new Promise((res, rej) => { - const params: CreateGroupRequest = { - GroupName: name, - UserPoolId: userPoolId - } - cognitoClient.createGroup(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateGroupRequest = { + GroupName: name, + UserPoolId: userPoolId, + }; + cognitoClient.createGroup(params, (err, data) => (err ? rej(err) : res(data))); + }); } export async function addUserToGroup(groupName: string, username: string, userPoolId: string) { - return new Promise((res, rej) => { - const params: AdminAddUserToGroupRequest = { - GroupName: groupName, - Username: username, - UserPoolId: userPoolId - } - cognitoClient.adminAddUserToGroup(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: AdminAddUserToGroupRequest = { + GroupName: groupName, + Username: username, + UserPoolId: userPoolId, + }; + cognitoClient.adminAddUserToGroup(params, (err, data) => (err ? rej(err) : res(data))); + }); } export async function createUserPool(client: CognitoClient, userPoolName: string): Promise { - return new Promise((res, rej) => { - const params: CreateUserPoolRequest = { - PoolName: userPoolName, - Policies: { - PasswordPolicy: { - MinimumLength: 8, - RequireLowercase: true, - RequireNumbers: true, - RequireSymbols: true, - RequireUppercase: true, - } - }, - Schema: [ - { - Name: 'email', - Required: true, - Mutable: true - } - ], - AutoVerifiedAttributes: ['email'] - } - client.createUserPool(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: CreateUserPoolRequest = { + PoolName: userPoolName, + Policies: { + PasswordPolicy: { + MinimumLength: 8, + RequireLowercase: true, + RequireNumbers: true, + RequireSymbols: true, + RequireUppercase: true, + }, + }, + Schema: [ + { + Name: 'email', + Required: true, + Mutable: true, + }, + ], + AutoVerifiedAttributes: ['email'], + }; + client.createUserPool(params, (err, data) => (err ? rej(err) : res(data))); + }); } export async function deleteUserPool(client: CognitoClient, userPoolId: string): Promise<{}> { - return new Promise((res, rej) => { - const params: DeleteUserPoolRequest = { - UserPoolId: userPoolId - } - client.deleteUserPool(params, (err, data) => err ? rej(err) : res(data)) - }) + return new Promise((res, rej) => { + const params: DeleteUserPoolRequest = { + UserPoolId: userPoolId, + }; + client.deleteUserPool(params, (err, data) => (err ? rej(err) : res(data))); + }); } -export async function createUserPoolClient(client: CognitoClient, userPoolId: string, clientName: string): Promise { - return new Promise((res, rej) => { - const params: CreateUserPoolClientRequest = { - ClientName: clientName, - UserPoolId: userPoolId, - GenerateSecret: false, - RefreshTokenValidity: 30 - }; - client.createUserPoolClient(params, (err, data) => err ? rej(err) : res(data)) - }) +export async function createUserPoolClient( + client: CognitoClient, + userPoolId: string, + clientName: string +): Promise { + return new Promise((res, rej) => { + const params: CreateUserPoolClientRequest = { + ClientName: clientName, + UserPoolId: userPoolId, + GenerateSecret: false, + RefreshTokenValidity: 30, + }; + client.createUserPoolClient(params, (err, data) => (err ? rej(err) : res(data))); + }); } export function addIAMRolesToCFNStack(out: DeploymentResources, e2eConfig: E2Econfiguration) { - const { - AUTH_ROLE_NAME, - UNAUTH_ROLE_NAME, - IDENTITY_POOL_NAME, - USER_POOL_CLIENTWEB_NAME, - USER_POOL_CLIENT_NAME, - USER_POOL_ID, - } = e2eConfig; + const { AUTH_ROLE_NAME, UNAUTH_ROLE_NAME, IDENTITY_POOL_NAME, USER_POOL_CLIENTWEB_NAME, USER_POOL_CLIENT_NAME, USER_POOL_ID } = e2eConfig; - // logic to add IAM roles to cfn - const authRole = new cfnIAM.Role({ - RoleName: AUTH_ROLE_NAME, - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Sid: '', - Effect: 'Allow', - Principal: { - Federated: 'cognito-identity.amazonaws.com' - }, - Action: 'sts:AssumeRoleWithWebIdentity', - Condition: { - 'ForAnyValue:StringLike': { - 'cognito-identity.amazonaws.com:amr': 'authenticated' - } - } - } - ] - } - }); + // logic to add IAM roles to cfn + const authRole = new cfnIAM.Role({ + RoleName: AUTH_ROLE_NAME, + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Sid: '', + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + }, + ], + }, + }); - const unauthRole = new cfnIAM.Role({ - RoleName: UNAUTH_ROLE_NAME, - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Sid: '', - Effect: 'Allow', - Principal: { - Federated: 'cognito-identity.amazonaws.com' - }, - Action: 'sts:AssumeRoleWithWebIdentity', - Condition: { - 'ForAnyValue:StringLike': { - 'cognito-identity.amazonaws.com:amr': 'unauthenticated' - } - } - } - ] + const unauthRole = new cfnIAM.Role({ + RoleName: UNAUTH_ROLE_NAME, + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Sid: '', + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'unauthenticated', + }, + }, }, - Policies: [ - new cfnIAM.Role.Policy({ - PolicyName: 'appsync-unauthrole-policy', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'appsync:GraphQL' - ], - Resource: [{ - 'Fn::Join': [ - '', - [ - 'arn:aws:appsync:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':apis/', - { - 'Fn::GetAtt': [ - 'GraphQLAPI', - 'ApiId' - ] - }, - '/*', - ], - ], - }], - }], + ], + }, + Policies: [ + new cfnIAM.Role.Policy({ + PolicyName: 'appsync-unauthrole-policy', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['appsync:GraphQL'], + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { + 'Fn::GetAtt': ['GraphQLAPI', 'ApiId'], + }, + '/*', + ], + ], }, - }), - ] - }); + ], + }, + ], + }, + }), + ], + }); - const identityPool = new cfnCognito.IdentityPool({ - IdentityPoolName: IDENTITY_POOL_NAME, - CognitoIdentityProviders: [ + const identityPool = new cfnCognito.IdentityPool({ + IdentityPoolName: IDENTITY_POOL_NAME, + CognitoIdentityProviders: [ + { + ClientId: { + Ref: 'UserPoolClient', + }, + ProviderName: { + 'Fn::Sub': [ + 'cognito-idp.${region}.amazonaws.com/${client}', { - ClientId: { - Ref: 'UserPoolClient' - }, - ProviderName: { - 'Fn::Sub': [ - 'cognito-idp.${region}.amazonaws.com/${client}', - { - 'region': { - Ref: 'AWS::Region' - }, - 'client': USER_POOL_ID - } - ] - } - } as unknown, + region: { + Ref: 'AWS::Region', + }, + client: USER_POOL_ID, + }, + ], + }, + } as unknown, + { + ClientId: { + Ref: 'UserPoolClientWeb', + }, + ProviderName: { + 'Fn::Sub': [ + 'cognito-idp.${region}.amazonaws.com/${client}', { - ClientId: { - Ref: 'UserPoolClientWeb' - }, - ProviderName: { - 'Fn::Sub': [ - 'cognito-idp.${region}.amazonaws.com/${client}', - { - 'region': { - Ref: 'AWS::Region' - }, - 'client': USER_POOL_ID - } - ] - } - } as unknown, - ], - AllowUnauthenticatedIdentities: true - }); + region: { + Ref: 'AWS::Region', + }, + client: USER_POOL_ID, + }, + ], + }, + } as unknown, + ], + AllowUnauthenticatedIdentities: true, + }); - const identityPoolRoleMap = new cfnCognito.IdentityPoolRoleAttachment({ - IdentityPoolId: { Ref: 'IdentityPool' } as unknown as string, - Roles: { - 'unauthenticated': { 'Fn::GetAtt': [ 'UnauthRole', 'Arn' ] }, - 'authenticated': { 'Fn::GetAtt': [ 'AuthRole', 'Arn' ] } - } - }); + const identityPoolRoleMap = new cfnCognito.IdentityPoolRoleAttachment({ + IdentityPoolId: ({ Ref: 'IdentityPool' } as unknown) as string, + Roles: { + unauthenticated: { 'Fn::GetAtt': ['UnauthRole', 'Arn'] }, + authenticated: { 'Fn::GetAtt': ['AuthRole', 'Arn'] }, + }, + }); - const userPoolClientWeb = new cfnCognito.UserPoolClient({ - ClientName: USER_POOL_CLIENTWEB_NAME, - RefreshTokenValidity: 30, - UserPoolId: USER_POOL_ID - }); + const userPoolClientWeb = new cfnCognito.UserPoolClient({ + ClientName: USER_POOL_CLIENTWEB_NAME, + RefreshTokenValidity: 30, + UserPoolId: USER_POOL_ID, + }); - const userPoolClient = new cfnCognito.UserPoolClient({ - ClientName: USER_POOL_CLIENT_NAME, - GenerateSecret: true, - RefreshTokenValidity: 30, - UserPoolId: USER_POOL_ID - }); + const userPoolClient = new cfnCognito.UserPoolClient({ + ClientName: USER_POOL_CLIENT_NAME, + GenerateSecret: true, + RefreshTokenValidity: 30, + UserPoolId: USER_POOL_ID, + }); - out.rootStack.Resources.IdentityPool = identityPool; - out.rootStack.Resources.IdentityPoolRoleMap = identityPoolRoleMap; - out.rootStack.Resources.UserPoolClientWeb = userPoolClientWeb; - out.rootStack.Resources.UserPoolClient = userPoolClient; - out.rootStack.Outputs.IdentityPoolId = { Value: { Ref: 'IdentityPool' } }; - out.rootStack.Outputs.IdentityPoolName = { Value: { 'Fn::GetAtt': [ 'IdentityPool', 'Name' ] } }; + out.rootStack.Resources.IdentityPool = identityPool; + out.rootStack.Resources.IdentityPoolRoleMap = identityPoolRoleMap; + out.rootStack.Resources.UserPoolClientWeb = userPoolClientWeb; + out.rootStack.Resources.UserPoolClient = userPoolClient; + out.rootStack.Outputs.IdentityPoolId = { Value: { Ref: 'IdentityPool' } }; + out.rootStack.Outputs.IdentityPoolName = { Value: { 'Fn::GetAtt': ['IdentityPool', 'Name'] } }; - out.rootStack.Resources.AuthRole = authRole; - out.rootStack.Outputs.AuthRoleArn = { Value: { 'Fn::GetAtt': [ 'AuthRole', 'Arn' ] } }; - out.rootStack.Resources.UnauthRole = unauthRole; - out.rootStack.Outputs.UnauthRoleArn = { Value: { 'Fn::GetAtt': [ 'UnauthRole', 'Arn' ] } }; + out.rootStack.Resources.AuthRole = authRole; + out.rootStack.Outputs.AuthRoleArn = { Value: { 'Fn::GetAtt': ['AuthRole', 'Arn'] } }; + out.rootStack.Resources.UnauthRole = unauthRole; + out.rootStack.Outputs.UnauthRoleArn = { Value: { 'Fn::GetAtt': ['UnauthRole', 'Arn'] } }; - // Since we're doing the policy here we've to remove the transformer generated artifacts from - // the generated stack. - delete out.rootStack.Resources[ResourceConstants.RESOURCES.UnauthRolePolicy]; - delete out.rootStack.Parameters.unauthRoleName; - delete out.rootStack.Resources[ResourceConstants.RESOURCES.AuthRolePolicy]; - delete out.rootStack.Parameters.authRoleName; + // Since we're doing the policy here we've to remove the transformer generated artifacts from + // the generated stack. + delete out.rootStack.Resources[ResourceConstants.RESOURCES.UnauthRolePolicy]; + delete out.rootStack.Parameters.unauthRoleName; + delete out.rootStack.Resources[ResourceConstants.RESOURCES.AuthRolePolicy]; + delete out.rootStack.Parameters.authRoleName; - for (const key of Object.keys(out.rootStack.Resources)) { - if (out.rootStack.Resources[key].Properties && - out.rootStack.Resources[key].Properties.Parameters && - out.rootStack.Resources[key].Properties.Parameters.unauthRoleName) { - delete out.rootStack.Resources[key].Properties.Parameters.unauthRoleName; - } + for (const key of Object.keys(out.rootStack.Resources)) { + if ( + out.rootStack.Resources[key].Properties && + out.rootStack.Resources[key].Properties.Parameters && + out.rootStack.Resources[key].Properties.Parameters.unauthRoleName + ) { + delete out.rootStack.Resources[key].Properties.Parameters.unauthRoleName; + } - if (out.rootStack.Resources[key].Properties && - out.rootStack.Resources[key].Properties.Parameters && - out.rootStack.Resources[key].Properties.Parameters.authRoleName) { - delete out.rootStack.Resources[key].Properties.Parameters.authRoleName; - } + if ( + out.rootStack.Resources[key].Properties && + out.rootStack.Resources[key].Properties.Parameters && + out.rootStack.Resources[key].Properties.Parameters.authRoleName + ) { + delete out.rootStack.Resources[key].Properties.Parameters.authRoleName; } + } - for (const stackKey of Object.keys(out.stacks)) { - const stack = out.stacks[stackKey]; + for (const stackKey of Object.keys(out.stacks)) { + const stack = out.stacks[stackKey]; - for (const key of Object.keys(stack.Resources)) { - if (stack.Parameters && - stack.Parameters.unauthRoleName) { - delete stack.Parameters.unauthRoleName; - } - if (stack.Parameters && - stack.Parameters.authRoleName) { - delete stack.Parameters.authRoleName; - } - if (stack.Resources[key].Properties && - stack.Resources[key].Properties.Parameters && - stack.Resources[key].Properties.Parameters.unauthRoleName) { - delete stack.Resources[key].Properties.Parameters.unauthRoleName; - } - if (stack.Resources[key].Properties && - stack.Resources[key].Properties.Parameters && - stack.Resources[key].Properties.Parameters.authRoleName) { - delete stack.Resources[key].Properties.Parameters.authRoleName; - } - } + for (const key of Object.keys(stack.Resources)) { + if (stack.Parameters && stack.Parameters.unauthRoleName) { + delete stack.Parameters.unauthRoleName; + } + if (stack.Parameters && stack.Parameters.authRoleName) { + delete stack.Parameters.authRoleName; + } + if ( + stack.Resources[key].Properties && + stack.Resources[key].Properties.Parameters && + stack.Resources[key].Properties.Parameters.unauthRoleName + ) { + delete stack.Resources[key].Properties.Parameters.unauthRoleName; + } + if ( + stack.Resources[key].Properties && + stack.Resources[key].Properties.Parameters && + stack.Resources[key].Properties.Parameters.authRoleName + ) { + delete stack.Resources[key].Properties.Parameters.authRoleName; + } } - return out; -} \ No newline at end of file + } + return out; +} diff --git a/packages/graphql-transformers-e2e-tests/src/deployNestedStacks.ts b/packages/graphql-transformers-e2e-tests/src/deployNestedStacks.ts index 6eccf27452..fefcc975c5 100644 --- a/packages/graphql-transformers-e2e-tests/src/deployNestedStacks.ts +++ b/packages/graphql-transformers-e2e-tests/src/deployNestedStacks.ts @@ -1,205 +1,203 @@ -import { S3Client } from './S3Client' -import { CloudFormationClient } from './CloudFormationClient' -import * as fs from 'fs' -import * as path from 'path' +import { S3Client } from './S3Client'; +import { CloudFormationClient } from './CloudFormationClient'; +import * as fs from 'fs'; +import * as path from 'path'; import { DeploymentResources } from 'graphql-transformer-core/lib/DeploymentResources'; function deleteDirectory(directory: string) { - const files = fs.readdirSync(directory) - for (const file of files) { - const contentPath = path.join(directory, file) - if (fs.lstatSync(contentPath).isDirectory()) { - deleteDirectory(contentPath) - fs.rmdirSync(contentPath) - } else { - fs.unlinkSync(contentPath) - } - } + const files = fs.readdirSync(directory); + for (const file of files) { + const contentPath = path.join(directory, file); + if (fs.lstatSync(contentPath).isDirectory()) { + deleteDirectory(contentPath); + fs.rmdirSync(contentPath); + } else { + fs.unlinkSync(contentPath); + } + } } -async function cleanupBucket( - client: S3Client, directory: string, bucket: string, key: string, buildTimestamp: string) { - const files = fs.readdirSync(directory) - for (const file of files) { - const contentPath = path.join(directory, file) - const s3Location = path.join(key, file) - if (fs.lstatSync(contentPath).isDirectory()) { - await cleanupBucket(client, contentPath, bucket, s3Location, buildTimestamp) - } else { - const fileKey = s3Location + '.' + buildTimestamp - await client.deleteFile(bucket, fileKey) - } - } +async function cleanupBucket(client: S3Client, directory: string, bucket: string, key: string, buildTimestamp: string) { + const files = fs.readdirSync(directory); + for (const file of files) { + const contentPath = path.join(directory, file); + const s3Location = path.join(key, file); + if (fs.lstatSync(contentPath).isDirectory()) { + await cleanupBucket(client, contentPath, bucket, s3Location, buildTimestamp); + } else { + const fileKey = s3Location + '.' + buildTimestamp; + await client.deleteFile(bucket, fileKey); + } + } } async function uploadDirectory(client: S3Client, directory: string, bucket: string, key: string) { - let s3LocationMap = {} - const files = fs.readdirSync(directory) - for (const file of files) { - const contentPath = path.join(directory, file) - const s3Location = path.join(key, file) - if (fs.lstatSync(contentPath).isDirectory()) { - const recMap = await uploadDirectory(client, contentPath, bucket, s3Location) - s3LocationMap = { ...recMap, ...s3LocationMap } - } else { - const fileKey = s3Location - await client.wait(.25, () => Promise.resolve()) - const fileContents = await fs.readFileSync(contentPath) - console.log(`Uploading file to ${bucket}/${fileKey}`) - await client.client.putObject({ - Bucket: bucket, - Key: fileKey, - Body: fileContents - }).promise() - const formattedName = file.split('.').map((s, i) => i > 0 ? `${s[0].toUpperCase()}${s.slice(1, s.length)}` : s).join('') - s3LocationMap[formattedName] = 's3://' + path.join(bucket, fileKey) - } - } - return s3LocationMap + let s3LocationMap = {}; + const files = fs.readdirSync(directory); + for (const file of files) { + const contentPath = path.join(directory, file); + const s3Location = path.join(key, file); + if (fs.lstatSync(contentPath).isDirectory()) { + const recMap = await uploadDirectory(client, contentPath, bucket, s3Location); + s3LocationMap = { ...recMap, ...s3LocationMap }; + } else { + const fileKey = s3Location; + await client.wait(0.25, () => Promise.resolve()); + const fileContents = await fs.readFileSync(contentPath); + console.log(`Uploading file to ${bucket}/${fileKey}`); + await client.client + .putObject({ + Bucket: bucket, + Key: fileKey, + Body: fileContents, + }) + .promise(); + const formattedName = file + .split('.') + .map((s, i) => (i > 0 ? `${s[0].toUpperCase()}${s.slice(1, s.length)}` : s)) + .join(''); + s3LocationMap[formattedName] = 's3://' + path.join(bucket, fileKey); + } + } + return s3LocationMap; } function writeDeploymentToDisk(deployment: DeploymentResources, directory: string) { + // Write the schema to disk + const schema = deployment.schema; + const fullSchemaPath = path.normalize(directory + `/schema.graphql`); + fs.writeFileSync(fullSchemaPath, schema); - // Write the schema to disk - const schema = deployment.schema; - const fullSchemaPath = path.normalize(directory + `/schema.graphql`) - fs.writeFileSync(fullSchemaPath, schema) + // Write resolvers to disk + const resolverFileNames = Object.keys(deployment.resolvers); + const resolverRootPath = path.normalize(directory + `/resolvers`); + if (!fs.existsSync(resolverRootPath)) { + fs.mkdirSync(resolverRootPath); + } + for (const resolverFileName of resolverFileNames) { + const fullResolverPath = path.normalize(resolverRootPath + '/' + resolverFileName); + fs.writeFileSync(fullResolverPath, deployment.resolvers[resolverFileName]); + } - // Write resolvers to disk - const resolverFileNames = Object.keys(deployment.resolvers); - const resolverRootPath = path.normalize(directory + `/resolvers`) - if (!fs.existsSync(resolverRootPath)) { - fs.mkdirSync(resolverRootPath); - } - for (const resolverFileName of resolverFileNames) { - const fullResolverPath = path.normalize(resolverRootPath + '/' + resolverFileName); - fs.writeFileSync(fullResolverPath, deployment.resolvers[resolverFileName]); - } + // Write the stacks to disk + const stackNames = Object.keys(deployment.stacks); + const stackRootPath = path.normalize(directory + `/stacks`); + if (!fs.existsSync(stackRootPath)) { + fs.mkdirSync(stackRootPath); + } + for (const stackFileName of stackNames) { + const fullStackPath = path.normalize(stackRootPath + '/' + stackFileName + '.json'); + fs.writeFileSync(fullStackPath, JSON.stringify(deployment.stacks[stackFileName], null, 4)); + } - // Write the stacks to disk - const stackNames = Object.keys(deployment.stacks); - const stackRootPath = path.normalize(directory + `/stacks`) - if (!fs.existsSync(stackRootPath)) { - fs.mkdirSync(stackRootPath); - } - for (const stackFileName of stackNames) { - const fullStackPath = path.normalize(stackRootPath + '/' + stackFileName + '.json'); - fs.writeFileSync(fullStackPath, JSON.stringify(deployment.stacks[stackFileName], null, 4)); - } + // Write any functions to disk + const functionNames = Object.keys(deployment.functions); + const functionRootPath = path.normalize(directory + `/functions`); + if (!fs.existsSync(functionRootPath)) { + fs.mkdirSync(functionRootPath); + } + for (const functionName of functionNames) { + const fullFunctionPath = path.normalize(functionRootPath + '/' + functionName); + const zipContents = fs.readFileSync(deployment.functions[functionName]); + fs.writeFileSync(fullFunctionPath, zipContents); + } - // Write any functions to disk - const functionNames = Object.keys(deployment.functions); - const functionRootPath = path.normalize(directory + `/functions`) - if (!fs.existsSync(functionRootPath)) { - fs.mkdirSync(functionRootPath); - } - for (const functionName of functionNames) { - const fullFunctionPath = path.normalize(functionRootPath + '/' + functionName); - const zipContents = fs.readFileSync(deployment.functions[functionName]) - fs.writeFileSync(fullFunctionPath, zipContents); - } + // Write any pipeline functions to disk + const pipelineFunctions = Object.keys(deployment.pipelineFunctions); + const pipelineFunctionsPath = path.normalize(directory + `/pipelineFunctions`); + if (!fs.existsSync(pipelineFunctionsPath)) { + fs.mkdirSync(pipelineFunctionsPath); + } + for (const pipelineFunctionName of pipelineFunctions) { + const fullFunctionPath = path.normalize(pipelineFunctionsPath + '/' + pipelineFunctionName); + fs.writeFileSync(fullFunctionPath, deployment.pipelineFunctions[pipelineFunctionName]); + } - // Write any pipeline functions to disk - const pipelineFunctions = Object.keys(deployment.pipelineFunctions); - const pipelineFunctionsPath = path.normalize(directory + `/pipelineFunctions`) - if (!fs.existsSync(pipelineFunctionsPath)) { - fs.mkdirSync(pipelineFunctionsPath); - } - for (const pipelineFunctionName of pipelineFunctions) { - const fullFunctionPath = path.normalize(pipelineFunctionsPath + '/' + pipelineFunctionName); - fs.writeFileSync(fullFunctionPath, deployment.pipelineFunctions[pipelineFunctionName]); - } - - const rootStack = deployment.rootStack; - const rootStackPath = path.normalize(directory + `/rootStack.json`); - fs.writeFileSync(rootStackPath, JSON.stringify(rootStack, null, 4)); + const rootStack = deployment.rootStack; + const rootStackPath = path.normalize(directory + `/rootStack.json`); + fs.writeFileSync(rootStackPath, JSON.stringify(rootStack, null, 4)); } -export async function cleanupS3Bucket( - s3Client: S3Client, buildPath: string, bucketName: string, rootKey: string, buildTimestamp: string) { - return cleanupBucket(s3Client, buildPath, bucketName, rootKey, buildTimestamp); +export async function cleanupS3Bucket(s3Client: S3Client, buildPath: string, bucketName: string, rootKey: string, buildTimestamp: string) { + return cleanupBucket(s3Client, buildPath, bucketName, rootKey, buildTimestamp); } export async function deploy( - s3Client: S3Client, cf: CloudFormationClient, stackName: string, - deploymentResources: DeploymentResources, params: any, buildPath: string, - bucketName: string, rootKey: string, buildTimeStamp: string + s3Client: S3Client, + cf: CloudFormationClient, + stackName: string, + deploymentResources: DeploymentResources, + params: any, + buildPath: string, + bucketName: string, + rootKey: string, + buildTimeStamp: string ) { - try { - if (!fs.existsSync(buildPath)) { - fs.mkdirSync(buildPath); - } - console.log(`Cleaning up previous deployments...`) - deleteDirectory(buildPath) - console.log(`Done cleaning up previous deployments.`) - } catch (e) { - console.error(`Error cleaning up build directory: ${e}`) - } - try { - console.log('Adding APIKey to deployment') - addAPIKeys(deploymentResources); - console.log('Finished adding APIKey to deployment') + try { + if (!fs.existsSync(buildPath)) { + fs.mkdirSync(buildPath); + } + console.log(`Cleaning up previous deployments...`); + deleteDirectory(buildPath); + console.log(`Done cleaning up previous deployments.`); + } catch (e) { + console.error(`Error cleaning up build directory: ${e}`); + } + try { + console.log('Adding APIKey to deployment'); + addAPIKeys(deploymentResources); + console.log('Finished adding APIKey to deployment'); - console.log('Writing deployment to disk...') - writeDeploymentToDisk(deploymentResources, buildPath); - console.log('Finished writing deployment to disk.') - } catch (e) { - console.error(`Error writing files to disk: ${e}`) - throw(e); - } - const s3RootKey = `${rootKey}/${buildTimeStamp}` - try { - console.log('Uploading deployment to S3...') - await uploadDirectory(s3Client, buildPath, bucketName, s3RootKey) - console.log('Finished uploading deployment to S3.') - } catch (e) { - console.log(`Error uploading deployment to s3: ${e}`) - throw e; - } - try { - console.log(`Deploying root stack...`); - await cf.createStack( - deploymentResources.rootStack, - stackName, - { - ...params, - S3DeploymentBucket: bucketName, - S3DeploymentRootKey: s3RootKey - } - ) - const finishedStack = await cf.waitForStack(stackName) - console.log(`Done deploying root stack...`); - await cf.wait(10, () => Promise.resolve()) - return finishedStack - } catch (e) { - console.log(`Error deploying cloudformation stack: ${e}`) - throw(e); - } + console.log('Writing deployment to disk...'); + writeDeploymentToDisk(deploymentResources, buildPath); + console.log('Finished writing deployment to disk.'); + } catch (e) { + console.error(`Error writing files to disk: ${e}`); + throw e; + } + const s3RootKey = `${rootKey}/${buildTimeStamp}`; + try { + console.log('Uploading deployment to S3...'); + await uploadDirectory(s3Client, buildPath, bucketName, s3RootKey); + console.log('Finished uploading deployment to S3.'); + } catch (e) { + console.log(`Error uploading deployment to s3: ${e}`); + throw e; + } + try { + console.log(`Deploying root stack...`); + await cf.createStack(deploymentResources.rootStack, stackName, { + ...params, + S3DeploymentBucket: bucketName, + S3DeploymentRootKey: s3RootKey, + }); + const finishedStack = await cf.waitForStack(stackName); + console.log(`Done deploying root stack...`); + await cf.wait(10, () => Promise.resolve()); + return finishedStack; + } catch (e) { + console.log(`Error deploying cloudformation stack: ${e}`); + throw e; + } } function addAPIKeys(stack: DeploymentResources) { - if (!stack.rootStack.Resources.GraphQLAPIKey) { - stack.rootStack.Resources.GraphQLAPIKey = { - "Type": "AWS::AppSync::ApiKey", - "Properties": { - "ApiId": { - "Fn::GetAtt": [ - "GraphQLAPI", - "ApiId" - ] - } - } - } - } + if (!stack.rootStack.Resources.GraphQLAPIKey) { + stack.rootStack.Resources.GraphQLAPIKey = { + Type: 'AWS::AppSync::ApiKey', + Properties: { + ApiId: { + 'Fn::GetAtt': ['GraphQLAPI', 'ApiId'], + }, + }, + }; + } - if (!stack.rootStack.Outputs.GraphQLAPIKeyOutput) { - stack.rootStack.Outputs.GraphQLAPIKeyOutput = { - "Value": { - "Fn::GetAtt": [ - "GraphQLAPIKey", - "ApiKey" - ] - }, - } - } -} \ No newline at end of file + if (!stack.rootStack.Outputs.GraphQLAPIKeyOutput) { + stack.rootStack.Outputs.GraphQLAPIKeyOutput = { + Value: { + 'Fn::GetAtt': ['GraphQLAPIKey', 'ApiKey'], + }, + }; + } +} diff --git a/packages/graphql-transformers-e2e-tests/src/emptyBucket.ts b/packages/graphql-transformers-e2e-tests/src/emptyBucket.ts index 3ee1b6f25e..02cace607d 100644 --- a/packages/graphql-transformers-e2e-tests/src/emptyBucket.ts +++ b/packages/graphql-transformers-e2e-tests/src/emptyBucket.ts @@ -1,43 +1,51 @@ -import * as S3 from 'aws-sdk/clients/s3' -const awsS3Client = new S3({ region: 'us-west-2' }) +import * as S3 from 'aws-sdk/clients/s3'; +const awsS3Client = new S3({ region: 'us-west-2' }); const emptyBucket = async (bucket: string) => { - let listObjects = await awsS3Client.listObjectsV2({ - Bucket: bucket - }).promise() - while (true) { - try { - const objectIds = listObjects.Contents.map(content => ({ - Key: content.Key - })) - console.log(`Deleting keys: \n${JSON.stringify(objectIds, null, 4)}`) - const response = await awsS3Client.deleteObjects({ - Bucket: bucket, - Delete: { - Objects: objectIds - } - }).promise() - console.error(JSON.stringify(response.Errors, null, 4)) - } catch (e) { - console.error(`Error deleting objects: ${e}`) - } - if (listObjects.NextContinuationToken) { - console.log(`Listing next page of objects after token: ${listObjects.NextContinuationToken}`) - listObjects = await awsS3Client.listObjectsV2({ - Bucket: bucket, - ContinuationToken: listObjects.NextContinuationToken - }).promise() - } else { - console.log(`Finished deleting keys`) - break; - } - }; + let listObjects = await awsS3Client + .listObjectsV2({ + Bucket: bucket, + }) + .promise(); + while (true) { try { - await awsS3Client.deleteBucket({ - Bucket: bucket, - }).promise() + const objectIds = listObjects.Contents.map(content => ({ + Key: content.Key, + })); + console.log(`Deleting keys: \n${JSON.stringify(objectIds, null, 4)}`); + const response = await awsS3Client + .deleteObjects({ + Bucket: bucket, + Delete: { + Objects: objectIds, + }, + }) + .promise(); + console.error(JSON.stringify(response.Errors, null, 4)); } catch (e) { - console.error(`Error deleting bucket: ${e}`) + console.error(`Error deleting objects: ${e}`); } -} -export default emptyBucket \ No newline at end of file + if (listObjects.NextContinuationToken) { + console.log(`Listing next page of objects after token: ${listObjects.NextContinuationToken}`); + listObjects = await awsS3Client + .listObjectsV2({ + Bucket: bucket, + ContinuationToken: listObjects.NextContinuationToken, + }) + .promise(); + } else { + console.log(`Finished deleting keys`); + break; + } + } + try { + await awsS3Client + .deleteBucket({ + Bucket: bucket, + }) + .promise(); + } catch (e) { + console.error(`Error deleting bucket: ${e}`); + } +}; +export default emptyBucket; diff --git a/packages/graphql-transformers-e2e-tests/src/setupUserPool.ts b/packages/graphql-transformers-e2e-tests/src/setupUserPool.ts index 1a2117a563..423f551635 100644 --- a/packages/graphql-transformers-e2e-tests/src/setupUserPool.ts +++ b/packages/graphql-transformers-e2e-tests/src/setupUserPool.ts @@ -1,45 +1,39 @@ -import { - signupAndAuthenticateUser, createGroup, addUserToGroup, - configureAmplify -} from './cognitoUtils'; +import { signupAndAuthenticateUser, createGroup, addUserToGroup, configureAmplify } from './cognitoUtils'; -const USERNAME1 = 'user1@test.com' -const USERNAME2 = 'user2@test.com' -const USERNAME3 = 'user3@test.com' -const TMP_PASSWORD = 'Password123!' -const REAL_PASSWORD = 'Password1234!' +const USERNAME1 = 'user1@test.com'; +const USERNAME2 = 'user2@test.com'; +const USERNAME3 = 'user3@test.com'; +const TMP_PASSWORD = 'Password123!'; +const REAL_PASSWORD = 'Password1234!'; const ADMIN_GROUP_NAME = 'Admin'; const DEVS_GROUP_NAME = 'Devs'; const PARTICIPANT_GROUP_NAME = 'Participant'; const WATCHER_GROUP_NAME = 'Watcher'; -export async function setupUserPool( - userPoolId: string, - userPoolClientId: string -) { - configureAmplify(userPoolId, userPoolClientId) - - await signupAndAuthenticateUser(userPoolId, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - await signupAndAuthenticateUser(userPoolId, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const authRes3: any = await signupAndAuthenticateUser(userPoolId, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) - - await createGroup(userPoolId, ADMIN_GROUP_NAME) - await createGroup(userPoolId, PARTICIPANT_GROUP_NAME) - await createGroup(userPoolId, WATCHER_GROUP_NAME) - await createGroup(userPoolId, DEVS_GROUP_NAME) - await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, userPoolId) - await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, userPoolId) - await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, userPoolId) - await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, userPoolId) - const authResAfterGroup: any = await signupAndAuthenticateUser(userPoolId, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) - - const idToken = authResAfterGroup.getIdToken().getJwtToken() - - const accessToken = authResAfterGroup.getAccessToken().getJwtToken() - - const authRes2AfterGroup: any = await signupAndAuthenticateUser(userPoolId, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) - const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() - const idToken3 = authRes3.getIdToken().getJwtToken() - return [idToken, idToken2, idToken3]; -} \ No newline at end of file +export async function setupUserPool(userPoolId: string, userPoolClientId: string) { + configureAmplify(userPoolId, userPoolClientId); + + await signupAndAuthenticateUser(userPoolId, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + await signupAndAuthenticateUser(userPoolId, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const authRes3: any = await signupAndAuthenticateUser(userPoolId, USERNAME3, TMP_PASSWORD, REAL_PASSWORD); + + await createGroup(userPoolId, ADMIN_GROUP_NAME); + await createGroup(userPoolId, PARTICIPANT_GROUP_NAME); + await createGroup(userPoolId, WATCHER_GROUP_NAME); + await createGroup(userPoolId, DEVS_GROUP_NAME); + await addUserToGroup(ADMIN_GROUP_NAME, USERNAME1, userPoolId); + await addUserToGroup(PARTICIPANT_GROUP_NAME, USERNAME1, userPoolId); + await addUserToGroup(WATCHER_GROUP_NAME, USERNAME1, userPoolId); + await addUserToGroup(DEVS_GROUP_NAME, USERNAME2, userPoolId); + const authResAfterGroup: any = await signupAndAuthenticateUser(userPoolId, USERNAME1, TMP_PASSWORD, REAL_PASSWORD); + + const idToken = authResAfterGroup.getIdToken().getJwtToken(); + + const accessToken = authResAfterGroup.getAccessToken().getJwtToken(); + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(userPoolId, USERNAME2, TMP_PASSWORD, REAL_PASSWORD); + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken(); + const idToken3 = authRes3.getIdToken().getJwtToken(); + return [idToken, idToken2, idToken3]; +} diff --git a/packages/graphql-transformers-e2e-tests/src/testUtil.ts b/packages/graphql-transformers-e2e-tests/src/testUtil.ts index 75f6067812..4e9ce37e62 100644 --- a/packages/graphql-transformers-e2e-tests/src/testUtil.ts +++ b/packages/graphql-transformers-e2e-tests/src/testUtil.ts @@ -1,93 +1,95 @@ import { - ObjectTypeDefinitionNode, FieldDefinitionNode, DocumentNode, - InputObjectTypeDefinitionNode, Kind, InputValueDefinitionNode, - DefinitionNode + ObjectTypeDefinitionNode, + FieldDefinitionNode, + DocumentNode, + InputObjectTypeDefinitionNode, + Kind, + InputValueDefinitionNode, + DefinitionNode, } from 'graphql'; import { isNonNullType } from 'graphql-transformer-common'; export function expectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } export function expectNonNullFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - expect(isNonNullType(foundField.type)).toBeTruthy(); - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + expect(isNonNullType(foundField.type)).toBeTruthy(); + } } export function expectNullableFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - expect(isNonNullType(foundField.type)).toBeFalsy(); - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + expect(isNonNullType(foundField.type)).toBeFalsy(); + } } export function expectArguments(field: FieldDefinitionNode, args: string[]) { - for (const argName of args) { - const foundArg = field.arguments.find((a: InputValueDefinitionNode) => a.name.value === argName) - expect(foundArg).toBeDefined() - } + for (const argName of args) { + const foundArg = field.arguments.find((a: InputValueDefinitionNode) => a.name.value === argName); + expect(foundArg).toBeDefined(); + } } export function doNotExpectFields(type: ObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - expect( - type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName) - ).toBeUndefined() - } + for (const fieldName of fields) { + expect(type.fields.find((f: FieldDefinitionNode) => f.name.value === fieldName)).toBeUndefined(); + } } export function getObjectType(doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type - ) as ObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; } export function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitionNode | undefined { - return doc.definitions.find( - (def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type - ) as InputObjectTypeDefinitionNode | undefined + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type) as + | InputObjectTypeDefinitionNode + | undefined; } export function expectInputValues(type: InputObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } } export function expectInputValueToHandle(type: InputObjectTypeDefinitionNode, f: (input: InputValueDefinitionNode) => boolean) { - for (const field of type.fields) { - expect(f(field)).toBeTruthy() - } + for (const field of type.fields) { + expect(f(field)).toBeTruthy(); + } } export function expectNonNullInputValues(type: InputObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - expect(isNonNullType(foundField.type)).toBeTruthy(); - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + expect(isNonNullType(foundField.type)).toBeTruthy(); + } } export function expectNullableInputValues(type: InputObjectTypeDefinitionNode, fields: string[]) { - for (const fieldName of fields) { - const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName) - expect(foundField).toBeDefined() - expect(isNonNullType(foundField.type)).toBeFalsy(); - } + for (const fieldName of fields) { + const foundField = type.fields.find((f: InputValueDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + expect(isNonNullType(foundField.type)).toBeFalsy(); + } } export function expectExactKeys(obj: Object, expectedSet: Set) { - const resourceSet = new Set(Object.keys(obj)); - expectedSet.forEach(item => { - expect(resourceSet.has(item)).toBeTruthy(); - }) - expect(resourceSet.size).toEqual(expectedSet.size); -} \ No newline at end of file + const resourceSet = new Set(Object.keys(obj)); + expectedSet.forEach(item => { + expect(resourceSet.has(item)).toBeTruthy(); + }); + expect(resourceSet.size).toEqual(expectedSet.size); +} diff --git a/packages/graphql-transformers-e2e-tests/src/testfunctions/echoFunction.js b/packages/graphql-transformers-e2e-tests/src/testfunctions/echoFunction.js index 9682315472..edce029863 100644 --- a/packages/graphql-transformers-e2e-tests/src/testfunctions/echoFunction.js +++ b/packages/graphql-transformers-e2e-tests/src/testfunctions/echoFunction.js @@ -1,4 +1,4 @@ -exports.handler = async (event) => { - console.log(event); - return event; -}; \ No newline at end of file +exports.handler = async event => { + console.log(event); + return event; +}; diff --git a/packages/graphql-transformers-e2e-tests/src/testfunctions/hello.js b/packages/graphql-transformers-e2e-tests/src/testfunctions/hello.js index 78ccc18ce3..e9ac3b7246 100644 --- a/packages/graphql-transformers-e2e-tests/src/testfunctions/hello.js +++ b/packages/graphql-transformers-e2e-tests/src/testfunctions/hello.js @@ -1,4 +1,4 @@ -exports.handler = async (event) => { - console.log(event); - return "Hello, world!"; -}; \ No newline at end of file +exports.handler = async event => { + console.log(event); + return 'Hello, world!'; +}; diff --git a/packages/graphql-versioned-transformer/src/VersionedModelTransformer.ts b/packages/graphql-versioned-transformer/src/VersionedModelTransformer.ts index b319ac5433..8dedf953bb 100644 --- a/packages/graphql-versioned-transformer/src/VersionedModelTransformer.ts +++ b/packages/graphql-versioned-transformer/src/VersionedModelTransformer.ts @@ -1,255 +1,218 @@ -import { Transformer, TransformerContext, InvalidDirectiveError, TransformerContractError, gql } from "graphql-transformer-core"; +import { Transformer, TransformerContext, InvalidDirectiveError, TransformerContractError, gql } from 'graphql-transformer-core'; +import { valueFromASTUntyped, ArgumentNode, ObjectTypeDefinitionNode, DirectiveNode, Kind } from 'graphql'; +import { printBlock, compoundExpression, set, ref, qref, obj, str, raw } from 'graphql-mapping-template'; import { - valueFromASTUntyped, - ArgumentNode, - ObjectTypeDefinitionNode, - DirectiveNode, - Kind -} from "graphql"; -import { printBlock, compoundExpression, set, ref, qref, obj, str, raw } from 'graphql-mapping-template' -import { - ResourceConstants, - ModelResourceIDs, - ResolverResourceIDs, - makeInputValueDefinition, - makeNonNullType, - makeNamedType, - getBaseType, - makeField -} from "graphql-transformer-common"; + ResourceConstants, + ModelResourceIDs, + ResolverResourceIDs, + makeInputValueDefinition, + makeNonNullType, + makeNamedType, + getBaseType, + makeField, +} from 'graphql-transformer-common'; export class VersionedModelTransformer extends Transformer { + constructor() { + super( + 'VersionedModelTransformer', + // TODO: Allow version attribute selection. Could be `@version on FIELD_DEFINITION` + gql` + directive @versioned(versionField: String = "version", versionInput: String = "expectedVersion") on OBJECT + ` + ); + } - constructor() { - super( - 'VersionedModelTransformer', - // TODO: Allow version attribute selection. Could be `@version on FIELD_DEFINITION` - gql`directive @versioned(versionField: String = "version", versionInput: String = "expectedVersion") on OBJECT` - ) + /** + * When a type is annotated with @versioned enable conflict resolution for the type. + * + * Usage: + * + * type Post @model @versioned(versionField: "version", versionInput: "expectedVersion") { + * id: ID! + * title: String + * version: Int! + * } + * + * Enabling conflict resolution automatically manages a "version" attribute in + * the @model type's DynamoDB table and injects a conditional expression into + * the types mutations that actually perform the conflict resolutions by + * checking the "version" attribute in the table with the "expectedVersion" passed + * by the user. + */ + public object = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext): void => { + // @versioned may only be used on types that are also @model + const modelDirective = def.directives.find(dir => dir.name.value === 'model'); + if (!modelDirective) { + throw new InvalidDirectiveError('Types annotated with @versioned must also be annotated with @model.'); } - /** - * When a type is annotated with @versioned enable conflict resolution for the type. - * - * Usage: - * - * type Post @model @versioned(versionField: "version", versionInput: "expectedVersion") { - * id: ID! - * title: String - * version: Int! - * } - * - * Enabling conflict resolution automatically manages a "version" attribute in - * the @model type's DynamoDB table and injects a conditional expression into - * the types mutations that actually perform the conflict resolutions by - * checking the "version" attribute in the table with the "expectedVersion" passed - * by the user. - */ - public object = (def: ObjectTypeDefinitionNode, directive: DirectiveNode, ctx: TransformerContext): void => { - // @versioned may only be used on types that are also @model - const modelDirective = def.directives.find((dir) => dir.name.value === 'model') - if (!modelDirective) { - throw new InvalidDirectiveError('Types annotated with @versioned must also be annotated with @model.') - } - - const isArg = (s: string) => (arg: ArgumentNode) => arg.name.value === s - const getArg = (arg: string, dflt?: any) => { - const argument = directive.arguments.find(isArg(arg)) - return argument ? valueFromASTUntyped(argument.value) : dflt - } + const isArg = (s: string) => (arg: ArgumentNode) => arg.name.value === s; + const getArg = (arg: string, dflt?: any) => { + const argument = directive.arguments.find(isArg(arg)); + return argument ? valueFromASTUntyped(argument.value) : dflt; + }; - const versionField = getArg('versionField', "version") - const versionInput = getArg('versionInput', "expectedVersion") - const typeName = def.name.value + const versionField = getArg('versionField', 'version'); + const versionInput = getArg('versionInput', 'expectedVersion'); + const typeName = def.name.value; - // Make the necessary changes to the context - this.augmentCreateMutation(ctx, typeName, versionField, versionInput) - this.augmentUpdateMutation(ctx, typeName, versionField, versionInput) - this.augmentDeleteMutation(ctx, typeName, versionField, versionInput) - this.stripCreateInputVersionedField(ctx, typeName, versionField) - this.addVersionedInputToDeleteInput(ctx, typeName, versionInput) - this.addVersionedInputToUpdateInput(ctx, typeName, versionInput) - this.enforceVersionedFieldOnType(ctx, typeName, versionField) - } + // Make the necessary changes to the context + this.augmentCreateMutation(ctx, typeName, versionField, versionInput); + this.augmentUpdateMutation(ctx, typeName, versionField, versionInput); + this.augmentDeleteMutation(ctx, typeName, versionField, versionInput); + this.stripCreateInputVersionedField(ctx, typeName, versionField); + this.addVersionedInputToDeleteInput(ctx, typeName, versionInput); + this.addVersionedInputToUpdateInput(ctx, typeName, versionInput); + this.enforceVersionedFieldOnType(ctx, typeName, versionField); + }; - /** - * Set the "version" to 1. - * @param ctx - * @param versionField - * @param versionInput - */ - private augmentCreateMutation(ctx: TransformerContext, typeName: string, versionField: string, versionInput: string) { - const snippet = printBlock(`Setting "${versionField}" to 1`)( - qref(`$ctx.args.input.put("${versionField}", 1)`) - ) - const mutationResolverLogicalId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName) - const resolver = ctx.getResource(mutationResolverLogicalId) - if (resolver) { - resolver.Properties.RequestMappingTemplate = snippet + '\n\n' + resolver.Properties.RequestMappingTemplate - ctx.setResource(mutationResolverLogicalId, resolver) - } + /** + * Set the "version" to 1. + * @param ctx + * @param versionField + * @param versionInput + */ + private augmentCreateMutation(ctx: TransformerContext, typeName: string, versionField: string, versionInput: string) { + const snippet = printBlock(`Setting "${versionField}" to 1`)(qref(`$ctx.args.input.put("${versionField}", 1)`)); + const mutationResolverLogicalId = ResolverResourceIDs.DynamoDBCreateResolverResourceID(typeName); + const resolver = ctx.getResource(mutationResolverLogicalId); + if (resolver) { + resolver.Properties.RequestMappingTemplate = snippet + '\n\n' + resolver.Properties.RequestMappingTemplate; + ctx.setResource(mutationResolverLogicalId, resolver); } + } - /** - * Prefix the update operation with a conditional expression that checks - * the object versions. - * @param ctx - * @param versionField - * @param versionInput - */ - private augmentDeleteMutation(ctx: TransformerContext, typeName: string, versionField: string, versionInput: string) { - const mutationResolverLogicalId = ResolverResourceIDs.DynamoDBDeleteResolverResourceID(typeName) - const snippet = printBlock(`Inject @versioned condition.`)( - compoundExpression([ - set(ref(ResourceConstants.SNIPPETS.VersionedCondition), obj({ - expression: str(`#${versionField} = :${versionInput}`), - expressionValues: obj({ - [`:${versionInput}`]: raw(`$util.dynamodb.toDynamoDB($ctx.args.input.${versionInput})`) - }), - expressionNames: obj({ - [`#${versionField}`]: str(`${versionField}`) - }) - })), - qref(`$ctx.args.input.remove("${versionInput}")`) - ]) - ) - const resolver = ctx.getResource(mutationResolverLogicalId) - if (resolver) { - resolver.Properties.RequestMappingTemplate = snippet + '\n\n' + resolver.Properties.RequestMappingTemplate - ctx.setResource(mutationResolverLogicalId, resolver) - } + /** + * Prefix the update operation with a conditional expression that checks + * the object versions. + * @param ctx + * @param versionField + * @param versionInput + */ + private augmentDeleteMutation(ctx: TransformerContext, typeName: string, versionField: string, versionInput: string) { + const mutationResolverLogicalId = ResolverResourceIDs.DynamoDBDeleteResolverResourceID(typeName); + const snippet = printBlock(`Inject @versioned condition.`)( + compoundExpression([ + set( + ref(ResourceConstants.SNIPPETS.VersionedCondition), + obj({ + expression: str(`#${versionField} = :${versionInput}`), + expressionValues: obj({ + [`:${versionInput}`]: raw(`$util.dynamodb.toDynamoDB($ctx.args.input.${versionInput})`), + }), + expressionNames: obj({ + [`#${versionField}`]: str(`${versionField}`), + }), + }) + ), + qref(`$ctx.args.input.remove("${versionInput}")`), + ]) + ); + const resolver = ctx.getResource(mutationResolverLogicalId); + if (resolver) { + resolver.Properties.RequestMappingTemplate = snippet + '\n\n' + resolver.Properties.RequestMappingTemplate; + ctx.setResource(mutationResolverLogicalId, resolver); } + } - private augmentUpdateMutation(ctx: TransformerContext, typeName: string, versionField: string, versionInput: string) { - const mutationResolverLogicalId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(typeName) - const snippet = printBlock(`Inject @versioned condition.`)( - compoundExpression([ - set(ref(ResourceConstants.SNIPPETS.VersionedCondition), obj({ - expression: str(`#${versionField} = :${versionInput}`), - expressionValues: obj({ - [`:${versionInput}`]: raw(`$util.dynamodb.toDynamoDB($ctx.args.input.${versionInput})`) - }), - expressionNames: obj({ - [`#${versionField}`]: str(`${versionField}`) - }) - })), - set(ref('newVersion'), raw(`$ctx.args.input.${versionInput} + 1`)), - qref(`$ctx.args.input.put("${versionField}", $newVersion)`), - qref(`$ctx.args.input.remove("${versionInput}")`) - ]) - ) - const resolver = ctx.getResource(mutationResolverLogicalId) - if (resolver) { - resolver.Properties.RequestMappingTemplate = snippet + '\n\n' + resolver.Properties.RequestMappingTemplate - ctx.setResource(mutationResolverLogicalId, resolver) - } + private augmentUpdateMutation(ctx: TransformerContext, typeName: string, versionField: string, versionInput: string) { + const mutationResolverLogicalId = ResolverResourceIDs.DynamoDBUpdateResolverResourceID(typeName); + const snippet = printBlock(`Inject @versioned condition.`)( + compoundExpression([ + set( + ref(ResourceConstants.SNIPPETS.VersionedCondition), + obj({ + expression: str(`#${versionField} = :${versionInput}`), + expressionValues: obj({ + [`:${versionInput}`]: raw(`$util.dynamodb.toDynamoDB($ctx.args.input.${versionInput})`), + }), + expressionNames: obj({ + [`#${versionField}`]: str(`${versionField}`), + }), + }) + ), + set(ref('newVersion'), raw(`$ctx.args.input.${versionInput} + 1`)), + qref(`$ctx.args.input.put("${versionField}", $newVersion)`), + qref(`$ctx.args.input.remove("${versionInput}")`), + ]) + ); + const resolver = ctx.getResource(mutationResolverLogicalId); + if (resolver) { + resolver.Properties.RequestMappingTemplate = snippet + '\n\n' + resolver.Properties.RequestMappingTemplate; + ctx.setResource(mutationResolverLogicalId, resolver); } + } - private stripCreateInputVersionedField( - ctx: TransformerContext, - typeName: string, - versionField: string, - ) { - const createInputName = ModelResourceIDs.ModelCreateInputObjectName(typeName) - const input = ctx.getType(createInputName) - if (input && input.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { - const updatedFields = input.fields.filter(f => f.name.value !== versionField) - if (updatedFields.length === 0) { - throw new InvalidDirectiveError( - `After stripping away version field "${versionField}", \ + private stripCreateInputVersionedField(ctx: TransformerContext, typeName: string, versionField: string) { + const createInputName = ModelResourceIDs.ModelCreateInputObjectName(typeName); + const input = ctx.getType(createInputName); + if (input && input.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { + const updatedFields = input.fields.filter(f => f.name.value !== versionField); + if (updatedFields.length === 0) { + throw new InvalidDirectiveError( + `After stripping away version field "${versionField}", \ the create input for type "${typeName}" cannot be created \ with 0 fields. Add another field to type "${typeName}" to continue.` - ) - } - const updatedInput = { - ...input, - fields: updatedFields - } - ctx.putType(updatedInput) - } + ); + } + const updatedInput = { + ...input, + fields: updatedFields, + }; + ctx.putType(updatedInput); } + } - private addVersionedInputToUpdateInput( - ctx: TransformerContext, - typeName: string, - versionInput: string, - ) { - return this.addVersionedInputToInput( - ctx, - ModelResourceIDs.ModelUpdateInputObjectName(typeName), - versionInput - ) - } + private addVersionedInputToUpdateInput(ctx: TransformerContext, typeName: string, versionInput: string) { + return this.addVersionedInputToInput(ctx, ModelResourceIDs.ModelUpdateInputObjectName(typeName), versionInput); + } - private addVersionedInputToDeleteInput( - ctx: TransformerContext, - typeName: string, - versionInput: string, - ) { - return this.addVersionedInputToInput( - ctx, - ModelResourceIDs.ModelDeleteInputObjectName(typeName), - versionInput - ) - } + private addVersionedInputToDeleteInput(ctx: TransformerContext, typeName: string, versionInput: string) { + return this.addVersionedInputToInput(ctx, ModelResourceIDs.ModelDeleteInputObjectName(typeName), versionInput); + } - private addVersionedInputToInput( - ctx: TransformerContext, - inputName: string, - versionInput: string, - ) { - const input = ctx.getType(inputName) - if (input && input.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { - const updatedFields = [ - ...input.fields, - makeInputValueDefinition(versionInput, makeNonNullType(makeNamedType("Int"))) - ] - const updatedInput = { - ...input, - fields: updatedFields - } - ctx.putType(updatedInput) - } + private addVersionedInputToInput(ctx: TransformerContext, inputName: string, versionInput: string) { + const input = ctx.getType(inputName); + if (input && input.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { + const updatedFields = [...input.fields, makeInputValueDefinition(versionInput, makeNonNullType(makeNamedType('Int')))]; + const updatedInput = { + ...input, + fields: updatedFields, + }; + ctx.putType(updatedInput); } + } - private enforceVersionedFieldOnType( - ctx: TransformerContext, - typeName: string, - versionField: string, - ) { - const type = ctx.getType(typeName) - if (type && type.kind === Kind.OBJECT_TYPE_DEFINITION) { - let updatedFields = type.fields - const versionFieldImpl = type.fields.find(f => f.name.value === versionField) - let updatedField = versionFieldImpl - if (versionFieldImpl) { - const baseType = getBaseType(versionFieldImpl.type) - if (baseType === 'Int' || baseType === 'BigInt') { - // ok. - if (versionFieldImpl.type.kind !== Kind.NON_NULL_TYPE) { - updatedField = { - ...updatedField, - type: makeNonNullType(versionFieldImpl.type), - } - updatedFields = updatedFields.map( - f => f.name.value === versionField ? updatedField : f - ) - } - } else { - throw new TransformerContractError(`The versionField "${versionField}" is required to be of type "Int" or "BigInt".`) - } - } else { - updatedField = makeField(versionField, [], makeNonNullType(makeNamedType('Int'))) - updatedFields = [ - ...updatedFields, - updatedField - ] - } - const updatedType = { - ...type, - fields: updatedFields - } - ctx.putType(updatedType) + private enforceVersionedFieldOnType(ctx: TransformerContext, typeName: string, versionField: string) { + const type = ctx.getType(typeName); + if (type && type.kind === Kind.OBJECT_TYPE_DEFINITION) { + let updatedFields = type.fields; + const versionFieldImpl = type.fields.find(f => f.name.value === versionField); + let updatedField = versionFieldImpl; + if (versionFieldImpl) { + const baseType = getBaseType(versionFieldImpl.type); + if (baseType === 'Int' || baseType === 'BigInt') { + // ok. + if (versionFieldImpl.type.kind !== Kind.NON_NULL_TYPE) { + updatedField = { + ...updatedField, + type: makeNonNullType(versionFieldImpl.type), + }; + updatedFields = updatedFields.map(f => (f.name.value === versionField ? updatedField : f)); + } + } else { + throw new TransformerContractError(`The versionField "${versionField}" is required to be of type "Int" or "BigInt".`); } + } else { + updatedField = makeField(versionField, [], makeNonNullType(makeNamedType('Int'))); + updatedFields = [...updatedFields, updatedField]; + } + const updatedType = { + ...type, + fields: updatedFields, + }; + ctx.putType(updatedType); } + } } diff --git a/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts b/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts index 4566337c95..5b08b67aee 100644 --- a/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts +++ b/packages/graphql-versioned-transformer/src/__tests__/VersionedModelTransformer.test.ts @@ -1,49 +1,42 @@ -import { - ObjectTypeDefinitionNode, parse, DocumentNode, - Kind, InputObjectTypeDefinitionNode -} from 'graphql' -import GraphQLTransform from 'graphql-transformer-core' -import { ResourceConstants } from 'graphql-transformer-common' -import { VersionedModelTransformer } from '../VersionedModelTransformer' -import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' +import { ObjectTypeDefinitionNode, parse, DocumentNode, Kind, InputObjectTypeDefinitionNode } from 'graphql'; +import GraphQLTransform from 'graphql-transformer-core'; +import { ResourceConstants } from 'graphql-transformer-common'; +import { VersionedModelTransformer } from '../VersionedModelTransformer'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; const getInputType = (schemaDoc: DocumentNode) => (name: string): InputObjectTypeDefinitionNode => - schemaDoc.definitions.find(d => d.kind !== Kind.SCHEMA_DEFINITION ? d.name.value === name : false) as InputObjectTypeDefinitionNode -const getInputField = (input: InputObjectTypeDefinitionNode, field: string) => input.fields.find(f => f.name.value === field) + schemaDoc.definitions.find(d => (d.kind !== Kind.SCHEMA_DEFINITION ? d.name.value === name : false)) as InputObjectTypeDefinitionNode; +const getInputField = (input: InputObjectTypeDefinitionNode, field: string) => input.fields.find(f => f.name.value === field); const getType = (schemaDoc: DocumentNode) => (name: string): ObjectTypeDefinitionNode => - schemaDoc.definitions.find(d => d.kind !== Kind.SCHEMA_DEFINITION ? d.name.value === name : false) as ObjectTypeDefinitionNode -const getField = (input: ObjectTypeDefinitionNode, field: string) => input.fields.find(f => f.name.value === field) + schemaDoc.definitions.find(d => (d.kind !== Kind.SCHEMA_DEFINITION ? d.name.value === name : false)) as ObjectTypeDefinitionNode; +const getField = (input: ObjectTypeDefinitionNode, field: string) => input.fields.find(f => f.name.value === field); test('Test VersionedModelTransformer validation happy case', () => { - const validSchema = ` + const validSchema = ` type Post @model @versioned { id: ID! title: String! createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new VersionedModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - // tslint:disable-next-line - const schemaDoc = parse(out.schema) + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new VersionedModelTransformer()], + }); + const out = transformer.transform(validSchema); + // tslint:disable-next-line + const schemaDoc = parse(out.schema); - expect(out).toBeDefined() - expect(getField(getType(schemaDoc)('Post'), 'version')).toBeDefined() - expect(getInputField(getInputType(schemaDoc)('CreatePostInput'), 'version')).toBeUndefined() - expect(getInputField(getInputType(schemaDoc)('UpdatePostInput'), 'expectedVersion')).toBeDefined() - expect(getInputField(getInputType(schemaDoc)('DeletePostInput'), 'expectedVersion')).toBeDefined() - // Use e2e tests to test resolver logic. + expect(out).toBeDefined(); + expect(getField(getType(schemaDoc)('Post'), 'version')).toBeDefined(); + expect(getInputField(getInputType(schemaDoc)('CreatePostInput'), 'version')).toBeUndefined(); + expect(getInputField(getInputType(schemaDoc)('UpdatePostInput'), 'expectedVersion')).toBeDefined(); + expect(getInputField(getInputType(schemaDoc)('DeletePostInput'), 'expectedVersion')).toBeDefined(); + // Use e2e tests to test resolver logic. }); - test('Test VersionedModelTransformer validation fails when provided version field of wrong type.', () => { - const validSchema = ` + const validSchema = ` type Post @model @versioned { id: ID! title: String! @@ -51,22 +44,19 @@ test('Test VersionedModelTransformer validation fails when provided version fiel createdAt: String updatedAt: String } - ` - try { - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new VersionedModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - } catch (e) { - expect(e.name).toEqual('TransformerContractError') - } + `; + try { + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new VersionedModelTransformer()], + }); + const out = transformer.transform(validSchema); + } catch (e) { + expect(e.name).toEqual('TransformerContractError'); + } }); test('Test VersionedModelTransformer version field replaced by non-null if provided as nullable.', () => { - const validSchema = ` + const validSchema = ` type Post @model @versioned { id: ID! title: String! @@ -74,17 +64,14 @@ test('Test VersionedModelTransformer version field replaced by non-null if provi createdAt: String updatedAt: String } - ` - const transformer = new GraphQLTransform({ - transformers: [ - new DynamoDBModelTransformer(), - new VersionedModelTransformer() - ] - }) - const out = transformer.transform(validSchema); - const sdl = out.schema - const schemaDoc = parse(sdl) - const versionField = getField(getType(schemaDoc)('Post'), 'version') - expect(versionField).toBeDefined() - expect(versionField.type.kind).toEqual(Kind.NON_NULL_TYPE) -}); \ No newline at end of file + `; + const transformer = new GraphQLTransform({ + transformers: [new DynamoDBModelTransformer(), new VersionedModelTransformer()], + }); + const out = transformer.transform(validSchema); + const sdl = out.schema; + const schemaDoc = parse(sdl); + const versionField = getField(getType(schemaDoc)('Post'), 'version'); + expect(versionField).toBeDefined(); + expect(versionField.type.kind).toEqual(Kind.NON_NULL_TYPE); +}); diff --git a/packages/graphql-versioned-transformer/src/index.ts b/packages/graphql-versioned-transformer/src/index.ts index 1268579433..c08d588cea 100644 --- a/packages/graphql-versioned-transformer/src/index.ts +++ b/packages/graphql-versioned-transformer/src/index.ts @@ -1,3 +1,3 @@ -import { VersionedModelTransformer } from './VersionedModelTransformer' -export * from './VersionedModelTransformer' -export default VersionedModelTransformer +import { VersionedModelTransformer } from './VersionedModelTransformer'; +export * from './VersionedModelTransformer'; +export default VersionedModelTransformer;