From d9388ab4be8d97a190f580f96db55c95607d850f Mon Sep 17 00:00:00 2001 From: Marc VandenBerg Date: Tue, 2 Aug 2022 15:15:51 -0400 Subject: [PATCH] Feat: Single Source Read (#573) * chore: no-op change to trigger pipelines (#545) * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.1 - amplify-category-api-dynamodb-simulator@2.4.1 - amplify-category-api-e2e-core@4.0.1 - amplify-category-api-e2e-tests@3.10.1 - @aws-amplify/graphql-auth-transformer@0.11.0 - @aws-amplify/graphql-default-value-transformer@0.5.27 - @aws-amplify/graphql-function-transformer@0.7.21 - @aws-amplify/graphql-http-transformer@0.8.21 - @aws-amplify/graphql-index-transformer@0.12.0 - @aws-amplify/graphql-maps-to-transformer@1.1.19 - amplify-category-api-graphql-migration-tests@2.3.1 - @aws-amplify/graphql-model-transformer@0.14.5 - @aws-amplify/graphql-predictions-transformer@0.6.21 - @aws-amplify/graphql-relational-transformer@0.10.0 - @aws-amplify/graphql-schema-test-library@1.1.0 - @aws-amplify/graphql-searchable-transformer@0.14.5 - @aws-amplify/graphql-transformer-core@0.17.5 - @aws-amplify/graphql-transformer-interfaces@1.14.4 - @aws-amplify/graphql-transformer-migrator@1.4.0 - amplify-category-api-migration-tests@5.0.1 - amplify-category-api-util-mock@5.1.0 - graphql-auth-transformer@7.2.38 - graphql-connection-transformer@5.2.38 - graphql-dynamodb-transformer@7.2.38 - graphql-elasticsearch-transformer@5.2.38 - graphql-function-transformer@3.3.29 - graphql-http-transformer@5.2.38 - graphql-key-transformer@3.2.38 - graphql-predictions-transformer@3.2.38 - graphql-transformer-core@7.6.1 - amplify-category-api-graphql-transformers-e2e-tests@8.1.0 - graphql-versioned-transformer@5.2.38 * Added Windows compatible translation of "hoist-cli" script (#543) * fix(graphql): handle begin and end vtl keywords in index name (#510) * fix(graphql): handle begin and end vtl keywords in index name * updated comments * chore: cleanup stale s3 buckets, and remove references to amplify-app which we no longer leverage in our test suite (#556) * feature: add support for default generation of query and gsi names for @index directive behind a feature flag * fix(amplify-graphql-auth-transformer, amplify-graphql-relational-transformer, amplify-graphql-transformer-core): support custom primary key with relational directives (#462) * fix(amplify-graphql-auth-transformer, amplify-graphql-relational-transformer, amplify-graphql-transformer-core): support custom primary key with relational directives * fix: fixed refactor incorrect hanlding for filter vs connection input names * fix: merge build errors * fix: merge caused unit test failures * fix: merge caused e2e failures * fix: merge caused e2e failures * addressing PR feedback * fix: corrected allowed fields for relational fields with custom PK and restricted auth * fix: fixed imports and e2e feature flag initialization * Feat: Single Source Read This adds the capability for but does not enable single source reading on GraphQL schemas This is intended for GraphQL Transformer V2 without support for V1 This also required pulling apart some of the logic in schema transformation so I can actually instantiate the transformer elsewhere * Nit: early return statement for ease of reading * test(graphql): fix failing datastore modelgen tests (#575) * test(graphql): fix failing datastore modelgen tests * moved the amplify-app to e2e tests package * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.2 - amplify-category-api-e2e-core@4.0.2 - amplify-category-api-e2e-tests@3.10.2 - @aws-amplify/graphql-auth-transformer@0.11.1 - @aws-amplify/graphql-default-value-transformer@0.5.28 - @aws-amplify/graphql-function-transformer@0.7.22 - @aws-amplify/graphql-http-transformer@0.8.22 - @aws-amplify/graphql-index-transformer@0.12.1 - @aws-amplify/graphql-maps-to-transformer@1.1.20 - amplify-category-api-graphql-migration-tests@2.3.2 - @aws-amplify/graphql-model-transformer@0.14.6 - @aws-amplify/graphql-predictions-transformer@0.6.22 - @aws-amplify/graphql-relational-transformer@0.10.1 - @aws-amplify/graphql-schema-test-library@1.1.1 - @aws-amplify/graphql-searchable-transformer@0.14.6 - @aws-amplify/graphql-transformer-core@0.17.6 - @aws-amplify/graphql-transformer-migrator@1.4.1 - amplify-category-api-migration-tests@5.0.2 - amplify-category-api-util-mock@5.1.1 - graphql-auth-transformer@7.2.39 - graphql-connection-transformer@5.2.39 - graphql-dynamodb-transformer@7.2.39 - graphql-elasticsearch-transformer@5.2.39 - graphql-function-transformer@3.3.30 - graphql-http-transformer@5.2.39 - graphql-key-transformer@3.2.39 - graphql-predictions-transformer@3.2.39 - graphql-relational-schema-transformer@2.21.9 - graphql-transformer-common@4.23.3 - graphql-transformer-core@7.6.2 - amplify-category-api-graphql-transformers-e2e-tests@8.1.1 - graphql-versioned-transformer@5.2.39 * fix: timestamp field sort in search queries (#581) * fix: timestamp field sort in search queries * address CRs * chore: change flag name for PK properties (#593) * chore: add build target to refresh lockfile, and refreshing lockfile * chore: improve error messaging if an invalid boolean sort key is provided to @index * feat(graphql): add runtime filtering support for subscriptions (#551) * feat(graphql): subscription runtime filtering * added dynamic groups support * update unit and e2e tests * added more test cases for auth precedence and different operators * fix enum list type on subscription filter * update model transformer screenshots * add test for enum field type * updated error message on a test * chore: remove unused dependencies based on depcheck report * chore: merge release to main (#616) * chore: no-op change to trigger pipelines (#545) * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.1 - amplify-category-api-dynamodb-simulator@2.4.1 - amplify-category-api-e2e-core@4.0.1 - amplify-category-api-e2e-tests@3.10.1 - @aws-amplify/graphql-auth-transformer@0.11.0 - @aws-amplify/graphql-default-value-transformer@0.5.27 - @aws-amplify/graphql-function-transformer@0.7.21 - @aws-amplify/graphql-http-transformer@0.8.21 - @aws-amplify/graphql-index-transformer@0.12.0 - @aws-amplify/graphql-maps-to-transformer@1.1.19 - amplify-category-api-graphql-migration-tests@2.3.1 - @aws-amplify/graphql-model-transformer@0.14.5 - @aws-amplify/graphql-predictions-transformer@0.6.21 - @aws-amplify/graphql-relational-transformer@0.10.0 - @aws-amplify/graphql-schema-test-library@1.1.0 - @aws-amplify/graphql-searchable-transformer@0.14.5 - @aws-amplify/graphql-transformer-core@0.17.5 - @aws-amplify/graphql-transformer-interfaces@1.14.4 - @aws-amplify/graphql-transformer-migrator@1.4.0 - amplify-category-api-migration-tests@5.0.1 - amplify-category-api-util-mock@5.1.0 - graphql-auth-transformer@7.2.38 - graphql-connection-transformer@5.2.38 - graphql-dynamodb-transformer@7.2.38 - graphql-elasticsearch-transformer@5.2.38 - graphql-function-transformer@3.3.29 - graphql-http-transformer@5.2.38 - graphql-key-transformer@3.2.38 - graphql-predictions-transformer@3.2.38 - graphql-transformer-core@7.6.1 - amplify-category-api-graphql-transformers-e2e-tests@8.1.0 - graphql-versioned-transformer@5.2.38 * API Category Release (#587) * chore: no-op change to trigger pipelines (#545) * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.1 - amplify-category-api-dynamodb-simulator@2.4.1 - amplify-category-api-e2e-core@4.0.1 - amplify-category-api-e2e-tests@3.10.1 - @aws-amplify/graphql-auth-transformer@0.11.0 - @aws-amplify/graphql-default-value-transformer@0.5.27 - @aws-amplify/graphql-function-transformer@0.7.21 - @aws-amplify/graphql-http-transformer@0.8.21 - @aws-amplify/graphql-index-transformer@0.12.0 - @aws-amplify/graphql-maps-to-transformer@1.1.19 - amplify-category-api-graphql-migration-tests@2.3.1 - @aws-amplify/graphql-model-transformer@0.14.5 - @aws-amplify/graphql-predictions-transformer@0.6.21 - @aws-amplify/graphql-relational-transformer@0.10.0 - @aws-amplify/graphql-schema-test-library@1.1.0 - @aws-amplify/graphql-searchable-transformer@0.14.5 - @aws-amplify/graphql-transformer-core@0.17.5 - @aws-amplify/graphql-transformer-interfaces@1.14.4 - @aws-amplify/graphql-transformer-migrator@1.4.0 - amplify-category-api-migration-tests@5.0.1 - amplify-category-api-util-mock@5.1.0 - graphql-auth-transformer@7.2.38 - graphql-connection-transformer@5.2.38 - graphql-dynamodb-transformer@7.2.38 - graphql-elasticsearch-transformer@5.2.38 - graphql-function-transformer@3.3.29 - graphql-http-transformer@5.2.38 - graphql-key-transformer@3.2.38 - graphql-predictions-transformer@3.2.38 - graphql-transformer-core@7.6.1 - amplify-category-api-graphql-transformers-e2e-tests@8.1.0 - graphql-versioned-transformer@5.2.38 * Added Windows compatible translation of "hoist-cli" script (#543) * fix(graphql): handle begin and end vtl keywords in index name (#510) * fix(graphql): handle begin and end vtl keywords in index name * updated comments * chore: cleanup stale s3 buckets, and remove references to amplify-app which we no longer leverage in our test suite (#556) * feature: add support for default generation of query and gsi names for @index directive behind a feature flag * fix(amplify-graphql-auth-transformer, amplify-graphql-relational-transformer, amplify-graphql-transformer-core): support custom primary key with relational directives (#462) * fix(amplify-graphql-auth-transformer, amplify-graphql-relational-transformer, amplify-graphql-transformer-core): support custom primary key with relational directives * fix: fixed refactor incorrect hanlding for filter vs connection input names * fix: merge build errors * fix: merge caused unit test failures * fix: merge caused e2e failures * fix: merge caused e2e failures * addressing PR feedback * fix: corrected allowed fields for relational fields with custom PK and restricted auth * fix: fixed imports and e2e feature flag initialization * test(graphql): fix failing datastore modelgen tests (#575) * test(graphql): fix failing datastore modelgen tests * moved the amplify-app to e2e tests package Co-authored-by: Al Harris <91494052+alharris-at@users.noreply.github.com> Co-authored-by: amplify-data-ci Co-authored-by: naedx <1711099+naedx@users.noreply.github.com> Co-authored-by: Alexander Harris Co-authored-by: Pavel Lazar <85319655+lazpavel@users.noreply.github.com> * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.2 - amplify-category-api-e2e-core@4.0.2 - amplify-category-api-e2e-tests@3.10.2 - @aws-amplify/graphql-auth-transformer@0.11.1 - @aws-amplify/graphql-default-value-transformer@0.5.28 - @aws-amplify/graphql-function-transformer@0.7.22 - @aws-amplify/graphql-http-transformer@0.8.22 - @aws-amplify/graphql-index-transformer@0.12.1 - @aws-amplify/graphql-maps-to-transformer@1.1.20 - amplify-category-api-graphql-migration-tests@2.3.2 - @aws-amplify/graphql-model-transformer@0.14.6 - @aws-amplify/graphql-predictions-transformer@0.6.22 - @aws-amplify/graphql-relational-transformer@0.10.1 - @aws-amplify/graphql-schema-test-library@1.1.1 - @aws-amplify/graphql-searchable-transformer@0.14.6 - @aws-amplify/graphql-transformer-core@0.17.6 - @aws-amplify/graphql-transformer-migrator@1.4.1 - amplify-category-api-migration-tests@5.0.2 - amplify-category-api-util-mock@5.1.1 - graphql-auth-transformer@7.2.39 - graphql-connection-transformer@5.2.39 - graphql-dynamodb-transformer@7.2.39 - graphql-elasticsearch-transformer@5.2.39 - graphql-function-transformer@3.3.30 - graphql-http-transformer@5.2.39 - graphql-key-transformer@3.2.39 - graphql-predictions-transformer@3.2.39 - graphql-relational-schema-transformer@2.21.9 - graphql-transformer-common@4.23.3 - graphql-transformer-core@7.6.2 - amplify-category-api-graphql-transformers-e2e-tests@8.1.1 - graphql-versioned-transformer@5.2.39 * fix conflict * fix conflict Co-authored-by: Al Harris <91494052+alharris-at@users.noreply.github.com> Co-authored-by: amplify-data-ci Co-authored-by: Christopher Sundersingh <83315412+sundersc@users.noreply.github.com> Co-authored-by: naedx <1711099+naedx@users.noreply.github.com> Co-authored-by: Alexander Harris Co-authored-by: Pavel Lazar <85319655+lazpavel@users.noreply.github.com> * Update linting rules * Added Windows compatible translation of "hoist-cli" script (#543) * chore: cleanup stale s3 buckets, and remove references to amplify-app which we no longer leverage in our test suite (#556) * Feat: Single Source Read This adds the capability for but does not enable single source reading on GraphQL schemas This is intended for GraphQL Transformer V2 without support for V1 This also required pulling apart some of the logic in schema transformation so I can actually instantiate the transformer elsewhere * Nit: early return statement for ease of reading * Consolidate if statements * chore: merge release to main (#616) * chore: no-op change to trigger pipelines (#545) * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.1 - amplify-category-api-dynamodb-simulator@2.4.1 - amplify-category-api-e2e-core@4.0.1 - amplify-category-api-e2e-tests@3.10.1 - @aws-amplify/graphql-auth-transformer@0.11.0 - @aws-amplify/graphql-default-value-transformer@0.5.27 - @aws-amplify/graphql-function-transformer@0.7.21 - @aws-amplify/graphql-http-transformer@0.8.21 - @aws-amplify/graphql-index-transformer@0.12.0 - @aws-amplify/graphql-maps-to-transformer@1.1.19 - amplify-category-api-graphql-migration-tests@2.3.1 - @aws-amplify/graphql-model-transformer@0.14.5 - @aws-amplify/graphql-predictions-transformer@0.6.21 - @aws-amplify/graphql-relational-transformer@0.10.0 - @aws-amplify/graphql-schema-test-library@1.1.0 - @aws-amplify/graphql-searchable-transformer@0.14.5 - @aws-amplify/graphql-transformer-core@0.17.5 - @aws-amplify/graphql-transformer-interfaces@1.14.4 - @aws-amplify/graphql-transformer-migrator@1.4.0 - amplify-category-api-migration-tests@5.0.1 - amplify-category-api-util-mock@5.1.0 - graphql-auth-transformer@7.2.38 - graphql-connection-transformer@5.2.38 - graphql-dynamodb-transformer@7.2.38 - graphql-elasticsearch-transformer@5.2.38 - graphql-function-transformer@3.3.29 - graphql-http-transformer@5.2.38 - graphql-key-transformer@3.2.38 - graphql-predictions-transformer@3.2.38 - graphql-transformer-core@7.6.1 - amplify-category-api-graphql-transformers-e2e-tests@8.1.0 - graphql-versioned-transformer@5.2.38 * API Category Release (#587) * chore: no-op change to trigger pipelines (#545) * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.1 - amplify-category-api-dynamodb-simulator@2.4.1 - amplify-category-api-e2e-core@4.0.1 - amplify-category-api-e2e-tests@3.10.1 - @aws-amplify/graphql-auth-transformer@0.11.0 - @aws-amplify/graphql-default-value-transformer@0.5.27 - @aws-amplify/graphql-function-transformer@0.7.21 - @aws-amplify/graphql-http-transformer@0.8.21 - @aws-amplify/graphql-index-transformer@0.12.0 - @aws-amplify/graphql-maps-to-transformer@1.1.19 - amplify-category-api-graphql-migration-tests@2.3.1 - @aws-amplify/graphql-model-transformer@0.14.5 - @aws-amplify/graphql-predictions-transformer@0.6.21 - @aws-amplify/graphql-relational-transformer@0.10.0 - @aws-amplify/graphql-schema-test-library@1.1.0 - @aws-amplify/graphql-searchable-transformer@0.14.5 - @aws-amplify/graphql-transformer-core@0.17.5 - @aws-amplify/graphql-transformer-interfaces@1.14.4 - @aws-amplify/graphql-transformer-migrator@1.4.0 - amplify-category-api-migration-tests@5.0.1 - amplify-category-api-util-mock@5.1.0 - graphql-auth-transformer@7.2.38 - graphql-connection-transformer@5.2.38 - graphql-dynamodb-transformer@7.2.38 - graphql-elasticsearch-transformer@5.2.38 - graphql-function-transformer@3.3.29 - graphql-http-transformer@5.2.38 - graphql-key-transformer@3.2.38 - graphql-predictions-transformer@3.2.38 - graphql-transformer-core@7.6.1 - amplify-category-api-graphql-transformers-e2e-tests@8.1.0 - graphql-versioned-transformer@5.2.38 * Added Windows compatible translation of "hoist-cli" script (#543) * fix(graphql): handle begin and end vtl keywords in index name (#510) * fix(graphql): handle begin and end vtl keywords in index name * updated comments * chore: cleanup stale s3 buckets, and remove references to amplify-app which we no longer leverage in our test suite (#556) * feature: add support for default generation of query and gsi names for @index directive behind a feature flag * fix(amplify-graphql-auth-transformer, amplify-graphql-relational-transformer, amplify-graphql-transformer-core): support custom primary key with relational directives (#462) * fix(amplify-graphql-auth-transformer, amplify-graphql-relational-transformer, amplify-graphql-transformer-core): support custom primary key with relational directives * fix: fixed refactor incorrect hanlding for filter vs connection input names * fix: merge build errors * fix: merge caused unit test failures * fix: merge caused e2e failures * fix: merge caused e2e failures * addressing PR feedback * fix: corrected allowed fields for relational fields with custom PK and restricted auth * fix: fixed imports and e2e feature flag initialization * test(graphql): fix failing datastore modelgen tests (#575) * test(graphql): fix failing datastore modelgen tests * moved the amplify-app to e2e tests package Co-authored-by: Al Harris <91494052+alharris-at@users.noreply.github.com> Co-authored-by: amplify-data-ci Co-authored-by: naedx <1711099+naedx@users.noreply.github.com> Co-authored-by: Alexander Harris Co-authored-by: Pavel Lazar <85319655+lazpavel@users.noreply.github.com> * chore(release): Publish [ci skip] - @aws-amplify/amplify-category-api@3.1.2 - amplify-category-api-e2e-core@4.0.2 - amplify-category-api-e2e-tests@3.10.2 - @aws-amplify/graphql-auth-transformer@0.11.1 - @aws-amplify/graphql-default-value-transformer@0.5.28 - @aws-amplify/graphql-function-transformer@0.7.22 - @aws-amplify/graphql-http-transformer@0.8.22 - @aws-amplify/graphql-index-transformer@0.12.1 - @aws-amplify/graphql-maps-to-transformer@1.1.20 - amplify-category-api-graphql-migration-tests@2.3.2 - @aws-amplify/graphql-model-transformer@0.14.6 - @aws-amplify/graphql-predictions-transformer@0.6.22 - @aws-amplify/graphql-relational-transformer@0.10.1 - @aws-amplify/graphql-schema-test-library@1.1.1 - @aws-amplify/graphql-searchable-transformer@0.14.6 - @aws-amplify/graphql-transformer-core@0.17.6 - @aws-amplify/graphql-transformer-migrator@1.4.1 - amplify-category-api-migration-tests@5.0.2 - amplify-category-api-util-mock@5.1.1 - graphql-auth-transformer@7.2.39 - graphql-connection-transformer@5.2.39 - graphql-dynamodb-transformer@7.2.39 - graphql-elasticsearch-transformer@5.2.39 - graphql-function-transformer@3.3.30 - graphql-http-transformer@5.2.39 - graphql-key-transformer@3.2.39 - graphql-predictions-transformer@3.2.39 - graphql-relational-schema-transformer@2.21.9 - graphql-transformer-common@4.23.3 - graphql-transformer-core@7.6.2 - amplify-category-api-graphql-transformers-e2e-tests@8.1.1 - graphql-versioned-transformer@5.2.39 * fix conflict * fix conflict Co-authored-by: Al Harris <91494052+alharris-at@users.noreply.github.com> Co-authored-by: amplify-data-ci Co-authored-by: Christopher Sundersingh <83315412+sundersc@users.noreply.github.com> Co-authored-by: naedx <1711099+naedx@users.noreply.github.com> Co-authored-by: Alexander Harris Co-authored-by: Pavel Lazar <85319655+lazpavel@users.noreply.github.com> * Added Windows compatible translation of "hoist-cli" script (#543) * chore: cleanup stale s3 buckets, and remove references to amplify-app which we no longer leverage in our test suite (#556) * Feat: Single Source Read This adds the capability for but does not enable single source reading on GraphQL schemas This is intended for GraphQL Transformer V2 without support for V1 This also required pulling apart some of the logic in schema transformation so I can actually instantiate the transformer elsewhere * Nit: early return statement for ease of reading * Consolidate if statements * Nits: fixing some code quality requests * Add explicit error for no schema found Co-authored-by: Al Harris <91494052+alharris-at@users.noreply.github.com> Co-authored-by: amplify-data-ci Co-authored-by: naedx <1711099+naedx@users.noreply.github.com> Co-authored-by: Christopher Sundersingh <83315412+sundersc@users.noreply.github.com> Co-authored-by: Alexander Harris Co-authored-by: Pavel Lazar <85319655+lazpavel@users.noreply.github.com> Co-authored-by: Zeyu Li --- .../src/category-utils/context-util.ts | 56 ++++ .../src/category-utils/schema-reader.ts | 91 ++++++ .../src/graphql-transformer/constants.ts | 6 + .../transform-graphql-schema-v2.ts | 267 ++-------------- .../transformer-factory.ts | 37 +++ .../transformer-options-types.ts | 60 ++++ .../transformer-options-v2.ts | 302 ++++++++++++++++++ .../src/index.ts | 8 +- 8 files changed, 590 insertions(+), 237 deletions(-) create mode 100644 packages/amplify-category-api/src/category-utils/context-util.ts create mode 100644 packages/amplify-category-api/src/category-utils/schema-reader.ts create mode 100644 packages/amplify-category-api/src/graphql-transformer/constants.ts create mode 100644 packages/amplify-category-api/src/graphql-transformer/transformer-options-types.ts create mode 100644 packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts diff --git a/packages/amplify-category-api/src/category-utils/context-util.ts b/packages/amplify-category-api/src/category-utils/context-util.ts new file mode 100644 index 0000000000..a4a65646c9 --- /dev/null +++ b/packages/amplify-category-api/src/category-utils/context-util.ts @@ -0,0 +1,56 @@ +import { + $TSAny, + $TSContext, + AmplifyCategories, + pathManager, +} from 'amplify-cli-core'; +import path from 'path'; +import { PROVIDER_NAME } from '../graphql-transformer/constants'; + +/** + * ContextUtil + * Some values are calculated on the basis of the context and options that come + * from the Amplify CLI, this class/singleton help calculate and cache those values + * for reference + */ +export class ContextUtil { + private resourceDir: string; + + /** + * Get the resource directory as used by the API category for GraphQL + * @param context the context from the CLI + * @param options the options from the CLI + */ + getResourceDir = async ( + context: $TSContext, + options: $TSAny, + ): Promise => { + if (this.resourceDir) { + return this.resourceDir; + } + let { resourceDir } = options; + const backEndDir = pathManager.getBackendDirPath(); + const { resourcesToBeCreated, resourcesToBeUpdated } = await context.amplify.getResourceStatus(AmplifyCategories.API); + const resources = resourcesToBeCreated.concat(resourcesToBeUpdated); + if (!resourceDir) { + // There can only be one appsync resource + if (!resources.length) { + // No appsync resource to update/add + return undefined; + } + if (resources.length > 0) { + const resource = resources[0]; + if (resource.providerPlugin !== PROVIDER_NAME) { + return undefined; + } + const { category } = resource; + const { resourceName } = resource; + resourceDir = path.normalize(path.join(backEndDir, category, resourceName)); + } + } + this.resourceDir = resourceDir; + return resourceDir; + } +} + +export const contextUtil = new ContextUtil(); diff --git a/packages/amplify-category-api/src/category-utils/schema-reader.ts b/packages/amplify-category-api/src/category-utils/schema-reader.ts new file mode 100644 index 0000000000..b18404a7d8 --- /dev/null +++ b/packages/amplify-category-api/src/category-utils/schema-reader.ts @@ -0,0 +1,91 @@ +import * as fs from 'fs-extra'; +import path from 'path'; +import { + DocumentNode, + parse, +} from 'graphql'; +import { + $TSAny, + $TSContext, + ApiCategoryFacade, +} from 'amplify-cli-core'; +import { buildGraphQLTransformV2 } from '../graphql-transformer/transformer-factory'; +import { + SCHEMA_DIR_NAME, + SCHEMA_FILENAME, +} from '../graphql-transformer/constants'; +import { generateTransformerOptions } from '../graphql-transformer/transformer-options-v2'; +import { contextUtil } from './context-util'; + +/** + * SchemaReader is a utility point to consolidate and abstract GraphQL Schema reading + * The readSchema method provides a flag to read the un-processed (original) schema + * if desired, but by default the intent of the SchemaReader is to use the preProcess + * utility of the V2 transformer + */ +export class SchemaReader { + private schemaPath: string; + private schemaDocument: DocumentNode; + private preProcessedSchemaDocument: DocumentNode; + + getSchemaPath = async ( + resourceDir: string, + ): Promise => { + if (this.schemaPath) { + return this.schemaPath; + } + const schemaFilePath = path.normalize(path.join(resourceDir, SCHEMA_FILENAME)); + const schemaDirPath = path.normalize(path.join(resourceDir, SCHEMA_DIR_NAME)); + + if (fs.pathExistsSync(schemaFilePath)) { + this.schemaPath = schemaFilePath; + } else if (fs.pathExistsSync(schemaDirPath)) { + this.schemaPath = schemaDirPath; + } else { + throw new Error(`No schema found, your graphql schema should be in either ${schemaFilePath} or ${schemaDirPath}`); + } + return this.schemaPath; + }; + + invalidateCachedSchema = (): void => { + this.schemaPath = null; + this.schemaDocument = null; + this.preProcessedSchemaDocument = null; + }; + + readSchema = async ( + context: $TSContext, + options: $TSAny, + usePreProcessing = true, + ): Promise => { + const preProcessSchema = usePreProcessing && (await ApiCategoryFacade.getTransformerVersion(context) === 2); + if (!this.schemaDocument) { + const fileContentsList = new Array>(); + const resourceDir = await contextUtil.getResourceDir(context, options); + const schemaPath = await this.getSchemaPath(resourceDir); + + const stats = fs.statSync(schemaPath); + if (stats.isDirectory()) { + fs.readdirSync(schemaPath).forEach((fileName) => { + fileContentsList.push(fs.readFile(path.join(schemaPath, fileName))); + }); + } else { + fileContentsList.push(fs.readFile(schemaPath)); + } + + const bufferList = await Promise.all(fileContentsList); + const fullSchema = bufferList.map((buff) => buff.toString()).join('\n'); + this.schemaDocument = parse(fullSchema); + } + + if (preProcessSchema && !this.preProcessedSchemaDocument) { + const transformerOptions = await generateTransformerOptions(context, options); + const transform = await buildGraphQLTransformV2(transformerOptions); + this.preProcessedSchemaDocument = transform.preProcessSchema(this.schemaDocument); + } + + return preProcessSchema ? this.preProcessedSchemaDocument : this.schemaDocument; + }; +} + +export const schemaReader = new SchemaReader(); diff --git a/packages/amplify-category-api/src/graphql-transformer/constants.ts b/packages/amplify-category-api/src/graphql-transformer/constants.ts new file mode 100644 index 0000000000..00e0894b57 --- /dev/null +++ b/packages/amplify-category-api/src/graphql-transformer/constants.ts @@ -0,0 +1,6 @@ +export const DESTRUCTIVE_UPDATES_FLAG = 'allow-destructive-graphql-schema-updates'; +export const PROVIDER_NAME = 'awscloudformation'; +export const PARAMETERS_FILENAME = 'parameters.json'; +export const ROOT_APPSYNC_S3_KEY = 'amplify-appsync-files'; +export const SCHEMA_FILENAME = 'schema.graphql'; +export const SCHEMA_DIR_NAME = 'schema'; diff --git a/packages/amplify-category-api/src/graphql-transformer/transform-graphql-schema-v2.ts b/packages/amplify-category-api/src/graphql-transformer/transform-graphql-schema-v2.ts index 4064ee9791..0dd1963870 100644 --- a/packages/amplify-category-api/src/graphql-transformer/transform-graphql-schema-v2.ts +++ b/packages/amplify-category-api/src/graphql-transformer/transform-graphql-schema-v2.ts @@ -2,34 +2,23 @@ import { collectDirectivesByTypeNames, DeploymentResources, GraphQLTransform, - ResolverConfig, - TransformerProjectConfig, } from '@aws-amplify/graphql-transformer-core'; -import { Template } from '@aws-amplify/graphql-transformer-core/lib/config/project-config'; -import { OverrideConfig } from '@aws-amplify/graphql-transformer-core/lib/transformation/types'; -import { AppSyncAuthConfiguration, TransformerPluginProvider } from '@aws-amplify/graphql-transformer-interfaces'; +import { AppSyncAuthConfiguration } from '@aws-amplify/graphql-transformer-interfaces'; import { - $TSAny, $TSContext, - $TSMeta, $TSObject, AmplifyCategories, AmplifySupportedService, ApiCategoryFacade, - CloudformationProviderFacade, getGraphQLTransformerAuthDocLink, JSONUtilities, pathManager, - stateManager, } from 'amplify-cli-core'; import { printer } from 'amplify-prompts'; import fs from 'fs-extra'; import { ResourceConstants } from 'graphql-transformer-common'; import { - DiffRule, - getSanityCheckRules, loadProject, - ProjectRule, sanityCheckProject, } from 'graphql-transformer-core'; import _ from 'lodash'; @@ -38,24 +27,29 @@ import path from 'path'; import { searchablePushChecks } from './api-utils'; import { AmplifyCLIFeatureFlagAdapter } from './amplify-cli-feature-flag-adapter'; import { isAuthModeUpdated } from './auth-mode-compare'; -import { schemaHasSandboxModeEnabled, showGlobalSandboxModeWarning, showSandboxModePrompts } from './sandbox-mode-helpers'; +import { + schemaHasSandboxModeEnabled, + showGlobalSandboxModeWarning, + showSandboxModePrompts, +} from './sandbox-mode-helpers'; import { parseUserDefinedSlots } from './user-defined-slots'; import { - getAdminRoles, getIdentityPoolId, mergeUserConfigWithTransformOutput, writeDeploymentToDisk, + mergeUserConfigWithTransformOutput, + writeDeploymentToDisk, } from './utils'; -import { getTransformerFactory } from './transformer-factory'; - -const PARAMETERS_FILENAME = 'parameters.json'; -const SCHEMA_FILENAME = 'schema.graphql'; -const SCHEMA_DIR_NAME = 'schema'; -const ROOT_APPSYNC_S3_KEY = 'amplify-appsync-files'; -const DESTRUCTIVE_UPDATES_FLAG = 'allow-destructive-graphql-schema-updates'; -const PROVIDER_NAME = 'awscloudformation'; - -type SanityCheckRules = { - diffRules: DiffRule[]; - projectRules: ProjectRule[]; -}; +import { + generateTransformerOptions, +} from './transformer-options-v2'; +import { + PARAMETERS_FILENAME, + SCHEMA_DIR_NAME, + SCHEMA_FILENAME, +} from './constants'; +import { + TransformerFactoryArgs, + TransformerProjectOptions, +} from './transformer-options-types'; +import { contextUtil } from '../category-utils/context-util'; const warnOnAuth = (map: $TSObject, docLink: string): void => { const unAuthModelTypes = Object.keys(map).filter(type => !map[type].includes('auth') && map[type].includes('model')); @@ -74,15 +68,18 @@ const warnOnAuth = (map: $TSObject, docLink: string): void => { * Transform GraphQL Schema */ export const transformGraphQLSchemaV2 = async (context: $TSContext, options): Promise => { - let resourceName: string; const backEndDir = pathManager.getBackendDirPath(); const flags = context.parameters.options; if (flags['no-gql-override']) { return undefined; } - let { resourceDir, parameters } = options; + let { parameters } = options; const { forceCompile } = options; + const resourceDir = await contextUtil.getResourceDir(context, options); + if (!resourceDir) { + return undefined; + } // Compilation during the push step const { resourcesToBeCreated, resourcesToBeUpdated, allResources } = await context.amplify.getResourceStatus(AmplifyCategories.API); @@ -105,38 +102,6 @@ export const transformGraphQLSchemaV2 = async (context: $TSContext, options): Pr } resources = resources.filter(resource => resource.service === 'AppSync'); - if (!resourceDir) { - // There can only be one appsync resource - if (resources.length > 0) { - const resource = resources[0]; - if (resource.providerPlugin !== PROVIDER_NAME) { - return undefined; - } - const { category } = resource; - ({ resourceName } = resource); - resourceDir = path.normalize(path.join(backEndDir, category, resourceName)); - } else { - // No appsync resource to update/add - return undefined; - } - } - - let previouslyDeployedBackendDir = options.cloudBackendDirectory; - if (!previouslyDeployedBackendDir) { - if (resources.length > 0) { - const resource = resources[0]; - if (resource.providerPlugin !== PROVIDER_NAME) { - return undefined; - } - const { category } = resource; - resourceName = resource.resourceName; - const cloudBackendRootDir = pathManager.getCurrentCloudBackendDirPath(); - /* eslint-disable */ - previouslyDeployedBackendDir = path.normalize(path.join(cloudBackendRootDir, category, resourceName)); - /* eslint-enable */ - } - } - const parametersFilePath = path.join(resourceDir, PARAMETERS_FILENAME); if (!parameters && fs.existsSync(parametersFilePath)) { @@ -183,29 +148,9 @@ export const transformGraphQLSchemaV2 = async (context: $TSContext, options): Pr } } - // for auth transformer we get any admin roles and a cognito identity pool to check for - // potential authenticated roles outside of the provided authRole - const adminRoles = await getAdminRoles(context, resourceName); - const identityPoolId = await getIdentityPoolId(context); - - // for the predictions directive get storage config - const s3Resource = s3ResourceAlreadyExists(); - const storageConfig = s3Resource ? getBucketName(s3Resource) : undefined; - const buildDir = path.normalize(path.join(resourceDir, 'build')); const schemaFilePath = path.normalize(path.join(resourceDir, SCHEMA_FILENAME)); const schemaDirPath = path.normalize(path.join(resourceDir, SCHEMA_DIR_NAME)); - let deploymentRootKey = await getPreviousDeploymentRootKey(previouslyDeployedBackendDir); - if (!deploymentRootKey) { - const deploymentSubKey = await CloudformationProviderFacade.hashDirectory(context, resourceDir); - deploymentRootKey = `${ROOT_APPSYNC_S3_KEY}/${deploymentSubKey}`; - } - const projectBucket = options.dryRun ? 'fake-bucket' : getProjectBucket(); - const buildParameters = { - ...parameters, - S3DeploymentBucket: projectBucket, - S3DeploymentRootKey: deploymentRootKey, - }; // If it is a dry run, don't create the build folder as it could make a follow-up command // to not to trigger a build, hence a corrupt deployment. @@ -215,9 +160,6 @@ export const transformGraphQLSchemaV2 = async (context: $TSContext, options): Pr const project = await loadProject(resourceDir); - const lastDeployedProjectConfig = fs.existsSync(previouslyDeployedBackendDir) - ? await loadProject(previouslyDeployedBackendDir) - : undefined; const transformerVersion = await ApiCategoryFacade.getTransformerVersion(context); const docLink = getGraphQLTransformerAuthDocLink(transformerVersion); const sandboxModeEnabled = schemaHasSandboxModeEnabled(project.schema, docLink); @@ -234,66 +176,16 @@ export const transformGraphQLSchemaV2 = async (context: $TSContext, options): Pr searchablePushChecks(context, directiveMap.types, parameters[ResourceConstants.PARAMETERS.AppSyncApiName]); - const transformerListFactory = await getTransformerFactory(context, resourceDir); - if (sandboxModeEnabled && options.promptApiKeyCreation) { const apiKeyConfig = await showSandboxModePrompts(context); if (apiKeyConfig) authConfig.additionalAuthenticationProviders.push(apiKeyConfig); } - let searchableTransformerFlag = false; - - if (directiveMap.directives.includes('searchable')) { - searchableTransformerFlag = true; - } - - // construct sanityCheckRules - const ff = new AmplifyCLIFeatureFlagAdapter(); - const isNewAppSyncAPI: boolean = resourcesToBeCreated.some(resource => resource.service === 'AppSync'); - const allowDestructiveUpdates = context?.input?.options?.[DESTRUCTIVE_UPDATES_FLAG] || context?.input?.options?.force; - const sanityCheckRules = getSanityCheckRules(isNewAppSyncAPI, ff, allowDestructiveUpdates); - let resolverConfig = {}; - if (!_.isEmpty(resources)) { - resolverConfig = await context.amplify.invokePluginMethod( - context, - AmplifyCategories.API, - AmplifySupportedService.APPSYNC, - 'getResolverConfig', - [context, resources[0].resourceName], - ); - } - - /** - * if Auth is not migrated , we need to fetch resolver Config from transformer.conf.json - * since above function will return empty object - */ - if (_.isEmpty(resolverConfig)) { - resolverConfig = project.config.ResolverConfig; + const buildConfig = await generateTransformerOptions(context, options); + if (!buildConfig) { + return undefined; } - const buildConfig: ProjectOptions = { - ...options, - buildParameters, - projectDirectory: resourceDir, - transformersFactory: transformerListFactory, - transformersFactoryArgs: { - addSearchableTransformer: searchableTransformerFlag, - storageConfig, - authConfig, - adminRoles, - identityPoolId, - }, - rootStackFileName: 'cloudformation-template.json', - currentCloudBackendDirectory: previouslyDeployedBackendDir, - minify: options.minify, - projectConfig: project, - lastDeployedProjectConfig, - authConfig, - sandboxModeEnabled, - sanityCheckRules, - resolverConfig, - }; - const transformerOutput = await buildAPIProject(context, buildConfig); printer.success(`GraphQL schema compiled successfully.\n\nEdit your schema at ${schemaFilePath} or \ @@ -309,109 +201,12 @@ place .graphql files in a directory at ${schemaDirPath}`); return transformerOutput; }; -const getProjectBucket = (): string => { - const meta: $TSMeta = stateManager.getMeta(undefined, { throwIfNotExist: false }); - const projectBucket = meta?.providers ? meta.providers[PROVIDER_NAME].DeploymentBucketName : ''; - return projectBucket; -}; - -const getPreviousDeploymentRootKey = async (previouslyDeployedBackendDir: string): Promise => { - // this is the function - let parameters; - try { - const parametersPath = path.join(previouslyDeployedBackendDir, `build/${PARAMETERS_FILENAME}`); - const parametersExists = fs.existsSync(parametersPath); - if (parametersExists) { - const parametersString = await fs.readFile(parametersPath); - parameters = JSON.parse(parametersString.toString()); - } - return parameters.S3DeploymentRootKey; - } catch (err) { - return undefined; - } -}; - -/** - * Check if storage exists in the project if not return undefined - */ -const s3ResourceAlreadyExists = (): string | undefined => { - try { - let resourceName: string; - const amplifyMeta: $TSMeta = stateManager.getMeta(undefined, { throwIfNotExist: false }); - if (amplifyMeta?.[AmplifyCategories.STORAGE]) { - const categoryResources = amplifyMeta[AmplifyCategories.STORAGE]; - Object.keys(categoryResources).forEach(resource => { - if (categoryResources[resource].service === AmplifySupportedService.S3) { - resourceName = resource; - } - }); - } - return resourceName; - } catch (error) { - if (error.name === 'UndeterminedEnvironmentError') { - return undefined; - } - throw error; - } -}; - -const getBucketName = (s3ResourceName: string): { bucketName: string } => { - const amplifyMeta = stateManager.getMeta(); - const stackName = amplifyMeta.providers.awscloudformation.StackName; - const s3ResourcePath = pathManager.getResourceDirectoryPath(undefined, AmplifyCategories.STORAGE, s3ResourceName); - const cliInputsPath = path.join(s3ResourcePath, 'cli-inputs.json'); - let bucketParameters: $TSObject; - // get bucketParameters 1st from cli-inputs , if not present, then parameters.json - if (fs.existsSync(cliInputsPath)) { - bucketParameters = JSONUtilities.readJson(cliInputsPath); - } else { - bucketParameters = stateManager.getResourceParametersJson(undefined, AmplifyCategories.STORAGE, s3ResourceName); - } - const bucketName = stackName.startsWith('amplify-') - ? `${bucketParameters.bucketName}\${hash}-\${env}` - : `${bucketParameters.bucketName}${s3ResourceName}-\${env}`; - return { bucketName }; -}; - -type TransformerFactoryArgs = { - addSearchableTransformer: boolean; - authConfig: $TSAny; - storageConfig?: $TSAny; - adminRoles?: Array; - identityPoolId?: string; -}; - -/** - * ProjectOptions Type Definition - */ -type ProjectOptions = { - buildParameters: { - S3DeploymentBucket: string; - S3DeploymentRootKey: string; - }; - projectDirectory: string; - transformersFactory: (options: T) => Promise; - transformersFactoryArgs: T; - rootStackFileName: 'cloudformation-template.json'; - currentCloudBackendDirectory?: string; - minify: boolean; - lastDeployedProjectConfig?: TransformerProjectConfig; - projectConfig: TransformerProjectConfig; - resolverConfig?: ResolverConfig; - dryRun?: boolean; - authConfig?: AppSyncAuthConfiguration; - stacks: Record; - sandboxModeEnabled?: boolean; - sanityCheckRules: SanityCheckRules; - overrideConfig: OverrideConfig; -}; - /** * buildAPIProject */ const buildAPIProject = async ( context: $TSContext, - opts: ProjectOptions, + opts: TransformerProjectOptions, ): Promise => { const schema = opts.projectConfig.schema.toString(); // Skip building the project if the schema is blank @@ -441,7 +236,7 @@ const buildAPIProject = async ( return builtProject; }; -const _buildProject = async (opts: ProjectOptions): Promise => { +const _buildProject = async (opts: TransformerProjectOptions): Promise => { const userProjectConfig = opts.projectConfig; const stackMapping = userProjectConfig.config.StackMapping; const userDefinedSlots = { diff --git a/packages/amplify-category-api/src/graphql-transformer/transformer-factory.ts b/packages/amplify-category-api/src/graphql-transformer/transformer-factory.ts index f817b2f688..5cb6fe8fca 100644 --- a/packages/amplify-category-api/src/graphql-transformer/transformer-factory.ts +++ b/packages/amplify-category-api/src/graphql-transformer/transformer-factory.ts @@ -45,6 +45,10 @@ import { import importFrom from 'import-from'; import importGlobal from 'import-global'; import path from 'path'; +import { GraphQLTransform } from '@aws-amplify/graphql-transformer-core'; +import { parseUserDefinedSlots } from './user-defined-slots'; +import { AmplifyCLIFeatureFlagAdapter } from './amplify-cli-feature-flag-adapter'; +import { TransformerProjectOptions } from './transformer-options-types'; const PROVIDER_NAME = 'awscloudformation'; @@ -231,3 +235,36 @@ const importTransformerModule = (transformerName: string) => { throw error; } }; + +/** + * Use the options generated from 'transformer-options' to create a V2 GraphQL Transform instance + * @param opts The options produced by 'transformer-options' + * @returns GraphQLTransform A brand new instance of the GraphQL Transform + */ +export const buildGraphQLTransformV2 = async (opts: TransformerProjectOptions): Promise => { + const userProjectConfig = opts.projectConfig; + const stackMapping = userProjectConfig.config.StackMapping; + const userDefinedSlots = { + ...parseUserDefinedSlots(userProjectConfig.pipelineFunctions), + ...parseUserDefinedSlots(userProjectConfig.resolvers), + }; + + // 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 = await opts.transformersFactory(opts.transformersFactoryArgs); + const transform = new GraphQLTransform({ + transformers, + stackMapping, + transformConfig: userProjectConfig.config, + authConfig: opts.authConfig, + buildParameters: opts.buildParameters, + stacks: opts.projectConfig.stacks || {}, + featureFlags: new AmplifyCLIFeatureFlagAdapter(), + sandboxModeEnabled: opts.sandboxModeEnabled, + userDefinedSlots, + resolverConfig: opts.resolverConfig, + overrideConfig: opts.overrideConfig, + }); + + return transform; +}; diff --git a/packages/amplify-category-api/src/graphql-transformer/transformer-options-types.ts b/packages/amplify-category-api/src/graphql-transformer/transformer-options-types.ts new file mode 100644 index 0000000000..f9f0d59ca5 --- /dev/null +++ b/packages/amplify-category-api/src/graphql-transformer/transformer-options-types.ts @@ -0,0 +1,60 @@ +/** + * ProjectOptions Type Definition + */ +import { + AppSyncAuthConfiguration, + TransformerPluginProvider, +} from '@aws-amplify/graphql-transformer-interfaces'; +import { + OverrideConfig, + ResolverConfig, + TransformerProjectConfig, +} from '@aws-amplify/graphql-transformer-core'; +import Template from '@aws-amplify/graphql-transformer-core/lib/transformation/types'; +import { + DiffRule, + ProjectRule, +} from 'graphql-transformer-core'; +import { $TSAny } from 'amplify-cli-core'; + +/** + * Transformer Options used to create a GraphQL Transform and compile a GQL API + */ +export type TransformerProjectOptions = { + buildParameters: { + S3DeploymentBucket: string; + S3DeploymentRootKey: string; + }; + projectDirectory: string; + transformersFactory: (options: T) => Promise; + transformersFactoryArgs: T; + rootStackFileName: 'cloudformation-template.json'; + currentCloudBackendDirectory?: string; + minify: boolean; + lastDeployedProjectConfig?: TransformerProjectConfig; + projectConfig: TransformerProjectConfig; + resolverConfig?: ResolverConfig; + dryRun?: boolean; + authConfig?: AppSyncAuthConfiguration; + stacks: Record; + sandboxModeEnabled?: boolean; + sanityCheckRules: SanityCheckRules; + overrideConfig: OverrideConfig; +}; + +type SanityCheckRules = { + diffRules: DiffRule[]; + projectRules: ProjectRule[]; +}; + +/** + * Arguments passed into a TransformerFactory + * Used to determine how to create a new GraphQLTransform + */ +export type TransformerFactoryArgs = { + addSearchableTransformer: boolean; + authConfig: $TSAny; + storageConfig?: $TSAny; + adminRoles?: Array; + identityPoolId?: string; +}; diff --git a/packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts b/packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts new file mode 100644 index 0000000000..a7e1e9103c --- /dev/null +++ b/packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts @@ -0,0 +1,302 @@ +import { + $TSAny, + $TSContext, $TSMeta, $TSObject, + AmplifyCategories, + AmplifySupportedService, ApiCategoryFacade, + CloudformationProviderFacade, getGraphQLTransformerAuthDocLink, + JSONUtilities, + pathManager, + stateManager, +} from 'amplify-cli-core'; +import { AppSyncAuthConfiguration } from '@aws-amplify/graphql-transformer-interfaces'; +import { + collectDirectivesByTypeNames, +} from '@aws-amplify/graphql-transformer-core'; +import { + getSanityCheckRules, + loadProject, +} from 'graphql-transformer-core'; +import path from 'path'; +import fs from 'fs-extra'; +import { ResourceConstants } from 'graphql-transformer-common'; +import _ from 'lodash'; +import { getAdminRoles, getIdentityPoolId } from './utils'; +import { schemaHasSandboxModeEnabled, showSandboxModePrompts } from './sandbox-mode-helpers'; +import { getTransformerFactory } from './transformer-factory'; +import { AmplifyCLIFeatureFlagAdapter } from './amplify-cli-feature-flag-adapter'; +import { + DESTRUCTIVE_UPDATES_FLAG, + PARAMETERS_FILENAME, + PROVIDER_NAME, + ROOT_APPSYNC_S3_KEY, +} from './constants'; +import { + TransformerFactoryArgs, + TransformerProjectOptions, +} from './transformer-options-types'; +import { contextUtil } from '../category-utils/context-util'; + +export const APPSYNC_RESOURCE_SERVICE = 'AppSync'; + +/** + * Use current context to generate the Transformer options for generating + * a GraphQL Transformer V2 object + * @param context The $TSContext from the Amplify CLI + * @param options The $TSAny options config coming from the Amplify CLI + */ +export const generateTransformerOptions = async ( + context: $TSContext, + options: $TSAny, +): Promise> => { + let resourceName: string; + const backEndDir = pathManager.getBackendDirPath(); + const flags = context.parameters.options; + if (flags['no-gql-override']) { + return undefined; + } + + let { parameters } = options; + const { forceCompile } = options; + + // Compilation during the push step + const { resourcesToBeCreated, resourcesToBeUpdated, allResources } = await context.amplify.getResourceStatus(AmplifyCategories.API); + let resources = resourcesToBeCreated.concat(resourcesToBeUpdated); + + // When build folder is missing include the API + // to be compiled without the backend/api//build + // cloud formation push will fail even if there is no changes in the GraphQL API + // https://github.com/aws-amplify/amplify-console/issues/10 + const resourceNeedCompile = allResources + .filter((r) => !resources.includes(r)) + .filter((r) => { + const buildDir = path.normalize(path.join(backEndDir, AmplifyCategories.API, r.resourceName, 'build')); + return !fs.existsSync(buildDir); + }); + resources = resources.concat(resourceNeedCompile); + + if (forceCompile) { + resources = resources.concat(allResources); + } + resources = resources.filter((resource) => resource.service === APPSYNC_RESOURCE_SERVICE); + + const resourceDir = await contextUtil.getResourceDir(context, options); + + let previouslyDeployedBackendDir = options.cloudBackendDirectory; + if (!previouslyDeployedBackendDir) { + if (resources.length > 0) { + const resource = resources[0]; + if (resource.providerPlugin !== PROVIDER_NAME) { + return undefined; + } + const { category } = resource; + resourceName = resource.resourceName; + const cloudBackendRootDir = pathManager.getCurrentCloudBackendDirPath(); + /* eslint-disable */ + previouslyDeployedBackendDir = path.normalize(path.join(cloudBackendRootDir, category, resourceName)); + /* eslint-enable */ + } + } + + const parametersFilePath = path.join(resourceDir, PARAMETERS_FILENAME); + + if (!parameters && fs.existsSync(parametersFilePath)) { + try { + parameters = JSONUtilities.readJson(parametersFilePath); + + // OpenSearch Instance type support for x.y.search types + if (parameters[ResourceConstants.PARAMETERS.OpenSearchInstanceType]) { + parameters[ResourceConstants.PARAMETERS.OpenSearchInstanceType] = parameters[ResourceConstants.PARAMETERS.OpenSearchInstanceType] + .replace('.search', '.elasticsearch'); + } + } catch (e) { + parameters = {}; + } + } + + let { authConfig }: { authConfig: AppSyncAuthConfiguration } = options; + + if (_.isEmpty(authConfig) && !_.isEmpty(resources)) { + authConfig = await context.amplify.invokePluginMethod( + context, + AmplifyCategories.API, + AmplifySupportedService.APPSYNC, + 'getAuthConfig', + [context, resources[0].resourceName], + ); + // handle case where auth project is not migrated , if Auth not migrated above function will return empty Object + if (_.isEmpty(authConfig)) { + // + // If we don't have an authConfig from the caller, use it from the + // already read resources[0], which is an AppSync API. + // + if (resources[0].output.securityType) { + // Convert to multi-auth format if needed. + authConfig = { + defaultAuthentication: { + authenticationType: resources[0].output.securityType, + }, + additionalAuthenticationProviders: [], + }; + } else { + ({ authConfig } = resources[0].output); + } + } + } + + // for auth transformer we get any admin roles and a cognito identity pool to check for + // potential authenticated roles outside of the provided authRole + const adminRoles = await getAdminRoles(context, resourceName); + const identityPoolId = await getIdentityPoolId(context); + + // for the predictions directive get storage config + const s3Resource = s3ResourceAlreadyExists(); + const storageConfig = s3Resource ? getBucketName(s3Resource) : undefined; + + let deploymentRootKey = await getPreviousDeploymentRootKey(previouslyDeployedBackendDir); + if (!deploymentRootKey) { + const deploymentSubKey = await CloudformationProviderFacade.hashDirectory(context, resourceDir); + deploymentRootKey = `${ROOT_APPSYNC_S3_KEY}/${deploymentSubKey}`; + } + const projectBucket = options.dryRun ? 'fake-bucket' : getProjectBucket(); + const buildParameters = { + ...parameters, + S3DeploymentBucket: projectBucket, + S3DeploymentRootKey: deploymentRootKey, + }; + + const project = await loadProject(resourceDir); + + const lastDeployedProjectConfig = fs.existsSync(previouslyDeployedBackendDir) + ? await loadProject(previouslyDeployedBackendDir) + : undefined; + const transformerVersion = await ApiCategoryFacade.getTransformerVersion(context); + const docLink = getGraphQLTransformerAuthDocLink(transformerVersion); + const sandboxModeEnabled = schemaHasSandboxModeEnabled(project.schema, docLink); + const directiveMap = collectDirectivesByTypeNames(project.schema); + + const transformerListFactory = await getTransformerFactory(context, resourceDir); + + if (sandboxModeEnabled && options.promptApiKeyCreation) { + const apiKeyConfig = await showSandboxModePrompts(context); + if (apiKeyConfig) authConfig.additionalAuthenticationProviders.push(apiKeyConfig); + } + + let searchableTransformerFlag = false; + + if (directiveMap.directives.includes('searchable')) { + searchableTransformerFlag = true; + } + + // construct sanityCheckRules + const ff = new AmplifyCLIFeatureFlagAdapter(); + const isNewAppSyncAPI: boolean = resourcesToBeCreated.some(resource => resource.service === 'AppSync'); + const allowDestructiveUpdates = context?.input?.options?.[DESTRUCTIVE_UPDATES_FLAG] || context?.input?.options?.force; + const sanityCheckRules = getSanityCheckRules(isNewAppSyncAPI, ff, allowDestructiveUpdates); + let resolverConfig = {}; + if (!_.isEmpty(resources)) { + resolverConfig = await context.amplify.invokePluginMethod( + context, + AmplifyCategories.API, + AmplifySupportedService.APPSYNC, + 'getResolverConfig', + [context, resources[0].resourceName], + ); + } + + /** + * if Auth is not migrated , we need to fetch resolver Config from transformer.conf.json + * since above function will return empty object + */ + if (_.isEmpty(resolverConfig)) { + resolverConfig = project.config.ResolverConfig; + } + + const buildConfig: TransformerProjectOptions = { + ...options, + buildParameters, + projectDirectory: resourceDir, + transformersFactory: transformerListFactory, + transformersFactoryArgs: { + addSearchableTransformer: searchableTransformerFlag, + storageConfig, + authConfig, + adminRoles, + identityPoolId, + }, + rootStackFileName: 'cloudformation-template.json', + currentCloudBackendDirectory: previouslyDeployedBackendDir, + minify: options.minify, + projectConfig: project, + lastDeployedProjectConfig, + authConfig, + sandboxModeEnabled, + sanityCheckRules, + resolverConfig, + }; + + return buildConfig; +}; + +const getBucketName = (s3ResourceName: string): { bucketName: string } => { + const amplifyMeta = stateManager.getMeta(); + const stackName = amplifyMeta.providers.awscloudformation.StackName; + const s3ResourcePath = pathManager.getResourceDirectoryPath(undefined, AmplifyCategories.STORAGE, s3ResourceName); + const cliInputsPath = path.join(s3ResourcePath, 'cli-inputs.json'); + let bucketParameters: $TSObject; + // get bucketParameters 1st from cli-inputs , if not present, then parameters.json + if (fs.existsSync(cliInputsPath)) { + bucketParameters = JSONUtilities.readJson(cliInputsPath); + } else { + bucketParameters = stateManager.getResourceParametersJson(undefined, AmplifyCategories.STORAGE, s3ResourceName); + } + const bucketName = stackName.startsWith('amplify-') + ? `${bucketParameters.bucketName}\${hash}-\${env}` + : `${bucketParameters.bucketName}${s3ResourceName}-\${env}`; + return { bucketName }; +}; + +const getPreviousDeploymentRootKey = async (previouslyDeployedBackendDir: string): Promise => { + // this is the function + let parameters; + try { + const parametersPath = path.join(previouslyDeployedBackendDir, `build/${PARAMETERS_FILENAME}`); + const parametersExists = fs.existsSync(parametersPath); + if (parametersExists) { + const parametersString = await fs.readFile(parametersPath); + parameters = JSON.parse(parametersString.toString()); + } + return parameters.S3DeploymentRootKey; + } catch (err) { + return undefined; + } +}; + +const getProjectBucket = (): string => { + const meta: $TSMeta = stateManager.getMeta(undefined, { throwIfNotExist: false }); + const projectBucket = meta?.providers ? meta.providers[PROVIDER_NAME].DeploymentBucketName : ''; + return projectBucket; +}; + +/** + * Check if storage exists in the project if not return undefined + */ +const s3ResourceAlreadyExists = (): string | undefined => { + try { + let resourceName: string; + const amplifyMeta: $TSMeta = stateManager.getMeta(undefined, { throwIfNotExist: false }); + if (amplifyMeta?.[AmplifyCategories.STORAGE]) { + const categoryResources = amplifyMeta[AmplifyCategories.STORAGE]; + Object.keys(categoryResources).forEach(resource => { + if (categoryResources[resource].service === AmplifySupportedService.S3) { + resourceName = resource; + } + }); + } + return resourceName; + } catch (error) { + if (error.name === 'UndeterminedEnvironmentError') { + return undefined; + } + throw error; + } +}; diff --git a/packages/amplify-graphql-transformer-core/src/index.ts b/packages/amplify-graphql-transformer-core/src/index.ts index 4650353641..3cfd741779 100644 --- a/packages/amplify-graphql-transformer-core/src/index.ts +++ b/packages/amplify-graphql-transformer-core/src/index.ts @@ -1,7 +1,13 @@ import { print } from 'graphql'; import { EXTRA_DIRECTIVES_DOCUMENT } from './transformation/validation'; + export { GraphQLTransform, GraphQLTransformOptions, SyncUtils } from './transformation'; -export { DeploymentResources, UserDefinedSlot, UserDefinedResolver } from './transformation/types'; +export { + DeploymentResources, + UserDefinedSlot, + UserDefinedResolver, + OverrideConfig, +} from './transformation/types'; export { validateModelSchema } from './transformation/validation'; export { ConflictDetectionType,