diff --git a/codebuild_specs/e2e_workflow.yml b/codebuild_specs/e2e_workflow.yml index a8b39f2807..a9184391b8 100644 --- a/codebuild_specs/e2e_workflow.yml +++ b/codebuild_specs/e2e_workflow.yml @@ -1119,59 +1119,70 @@ batch: depend-on: - publish_to_local_registry - identifier: >- - amplify_table_5_add_resources_single_gsi_single_record_single_gsi_empty_table + amplify_table_5_add_resources_references_migration_many_to_many_migration buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_MEDIUM variables: TEST_SUITE: >- - src/__tests__/amplify-table-5.test.ts|src/__tests__/add-resources.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-empty-table.test.ts - CLI_REGION: ap-east-1 + src/__tests__/amplify-table-5.test.ts|src/__tests__/add-resources.test.ts|src/__tests__/migration/references-migration.test.ts|src/__tests__/migration/many-to-many-migration.test.ts + CLI_REGION: ap-south-1 depend-on: - publish_to_local_registry - identifier: >- - single_gsi_1k_records_single_gsi_10k_records_replace_2_gsis_update_attr_single_record_replace_2_gsis_update_attr_empty_table + base_migration_single_gsi_single_record_single_gsi_empty_table_single_gsi_1k_records buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_MEDIUM variables: TEST_SUITE: >- - src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-empty-table.test.ts + src/__tests__/migration/base-migration.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-1k-records.test.ts CLI_REGION: ap-northeast-1 depend-on: - publish_to_local_registry - identifier: >- - replace_2_gsis_update_attr_1k_records_replace_2_gsis_update_attr_10k_records_replace_2_gsis_single_record_replace_2_gsis_empty_ + single_gsi_10k_records_replace_2_gsis_update_attr_single_record_replace_2_gsis_update_attr_empty_table_replace_2_gsis_update_at buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_MEDIUM variables: TEST_SUITE: >- - src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-empty-table.test.ts + src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-1k-records.test.ts CLI_REGION: ap-northeast-2 depend-on: - publish_to_local_registry - identifier: >- - replace_2_gsis_1k_records_replace_2_gsis_10k_records_3_gsis_single_record_3_gsis_empty_table + replace_2_gsis_update_attr_10k_records_replace_2_gsis_single_record_replace_2_gsis_empty_table_replace_2_gsis_1k_records buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_MEDIUM variables: TEST_SUITE: >- - src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-empty-table.test.ts + src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-1k-records.test.ts CLI_REGION: ap-south-1 depend-on: - publish_to_local_registry - - identifier: 3_gsis_1k_records_3_gsis_10k_records + - identifier: >- + replace_2_gsis_10k_records_3_gsis_single_record_3_gsis_empty_table_3_gsis_1k_records buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_MEDIUM variables: TEST_SUITE: >- - src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-10k-records.test.ts + src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-1k-records.test.ts CLI_REGION: ap-southeast-1 depend-on: - publish_to_local_registry + - identifier: 3_gsis_10k_records + buildspec: codebuild_specs/run_cdk_tests.yml + env: + compute-type: BUILD_GENERAL1_MEDIUM + variables: + TEST_SUITE: >- + src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-10k-records.test.ts + CLI_REGION: ap-southeast-2 + depend-on: + - publish_to_local_registry - identifier: sql_pg_models buildspec: codebuild_specs/run_cdk_tests.yml env: @@ -1723,7 +1734,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/static-group-auth/static-group-auth-sqlprimary-sqlrelated-subscriptions-off.test.ts - CLI_REGION: eu-north-1 + CLI_REGION: eu-south-1 depend-on: - publish_to_local_registry - identifier: static_group_auth_sqlprimary_ddbrelated_subscriptions_off @@ -1733,7 +1744,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/static-group-auth/static-group-auth-sqlprimary-ddbrelated-subscriptions-off.test.ts - CLI_REGION: eu-south-1 + CLI_REGION: eu-west-1 depend-on: - publish_to_local_registry - identifier: static_group_auth_ddbprimary_sqlrelated_subscriptions_off @@ -1743,7 +1754,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/static-group-auth/static-group-auth-ddbprimary-sqlrelated-subscriptions-off.test.ts - CLI_REGION: eu-west-1 + CLI_REGION: eu-west-2 depend-on: - publish_to_local_registry - identifier: static_group_auth_ddbprimary_ddbrelated_subscriptions_off @@ -1753,7 +1764,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/static-group-auth/static-group-auth-ddbprimary-ddbrelated-subscriptions-off.test.ts - CLI_REGION: eu-west-2 + CLI_REGION: eu-west-3 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_sqlprimary_sqlrelated_subscriptions_off @@ -1763,7 +1774,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/dynamic-group-auth/dynamic-group-auth-sqlprimary-sqlrelated-subscriptions-off.test.ts - CLI_REGION: eu-west-3 + CLI_REGION: me-south-1 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_sqlprimary_ddbrelated_subscriptions_off @@ -1773,7 +1784,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/dynamic-group-auth/dynamic-group-auth-sqlprimary-ddbrelated-subscriptions-off.test.ts - CLI_REGION: me-south-1 + CLI_REGION: sa-east-1 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_ddbprimary_sqlrelated_subscriptions_off @@ -1783,7 +1794,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/dynamic-group-auth/dynamic-group-auth-ddbprimary-sqlrelated-subscriptions-off.test.ts - CLI_REGION: sa-east-1 + CLI_REGION: us-east-1 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_ddbprimary_ddbrelated_subscriptions_off @@ -1793,7 +1804,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/subscriptions-off/dynamic-group-auth/dynamic-group-auth-ddbprimary-ddbrelated-subscriptions-off.test.ts - CLI_REGION: us-east-1 + CLI_REGION: us-east-2 depend-on: - publish_to_local_registry - identifier: static_group_auth_sqlprimary_sqlrelated @@ -1803,7 +1814,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/static-group-auth/static-group-auth-sqlprimary-sqlrelated.test.ts - CLI_REGION: us-east-2 + CLI_REGION: us-west-1 depend-on: - publish_to_local_registry - identifier: static_group_auth_sqlprimary_ddbrelated @@ -1813,7 +1824,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/static-group-auth/static-group-auth-sqlprimary-ddbrelated.test.ts - CLI_REGION: us-west-1 + CLI_REGION: us-west-2 depend-on: - publish_to_local_registry - identifier: static_group_auth_ddbprimary_sqlrelated @@ -1823,7 +1834,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/static-group-auth/static-group-auth-ddbprimary-sqlrelated.test.ts - CLI_REGION: us-west-2 + CLI_REGION: ap-northeast-1 depend-on: - publish_to_local_registry - identifier: static_group_auth_ddbprimary_ddbrelated @@ -1833,7 +1844,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/static-group-auth/static-group-auth-ddbprimary-ddbrelated.test.ts - CLI_REGION: ap-northeast-1 + CLI_REGION: ap-northeast-2 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_sqlprimary_sqlrelated @@ -1843,7 +1854,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/dynamic-group-auth/dynamic-group-auth-sqlprimary-sqlrelated.test.ts - CLI_REGION: ap-northeast-2 + CLI_REGION: ap-south-1 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_sqlprimary_ddbrelated @@ -1853,7 +1864,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/dynamic-group-auth/dynamic-group-auth-sqlprimary-ddbrelated.test.ts - CLI_REGION: ap-south-1 + CLI_REGION: ap-southeast-1 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_ddbprimary_sqlrelated @@ -1863,7 +1874,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/dynamic-group-auth/dynamic-group-auth-ddbprimary-sqlrelated.test.ts - CLI_REGION: ap-southeast-1 + CLI_REGION: ap-southeast-2 depend-on: - publish_to_local_registry - identifier: dynamic_group_auth_ddbprimary_ddbrelated @@ -1873,7 +1884,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/group-auth/dynamic-group-auth/dynamic-group-auth-ddbprimary-ddbrelated.test.ts - CLI_REGION: ap-southeast-2 + CLI_REGION: ca-central-1 depend-on: - publish_to_local_registry - identifier: single_gsi_100k_records @@ -1882,7 +1893,7 @@ batch: compute-type: BUILD_GENERAL1_SMALL variables: TEST_SUITE: src/__tests__/deploy-velocity/single-gsi-100k-records.test.ts - CLI_REGION: ap-southeast-2 + CLI_REGION: ca-central-1 depend-on: - publish_to_local_registry - identifier: replace_2_gsis_update_attr_100k_records @@ -1892,7 +1903,7 @@ batch: variables: TEST_SUITE: >- src/__tests__/deploy-velocity/replace-2-gsis-update-attr-100k-records.test.ts - CLI_REGION: ca-central-1 + CLI_REGION: eu-central-1 depend-on: - publish_to_local_registry - identifier: replace_2_gsis_100k_records @@ -1901,7 +1912,7 @@ batch: compute-type: BUILD_GENERAL1_SMALL variables: TEST_SUITE: src/__tests__/deploy-velocity/replace-2-gsis-100k-records.test.ts - CLI_REGION: eu-central-1 + CLI_REGION: eu-north-1 depend-on: - publish_to_local_registry - identifier: 3_gsis_100k_records @@ -1910,7 +1921,7 @@ batch: compute-type: BUILD_GENERAL1_SMALL variables: TEST_SUITE: src/__tests__/deploy-velocity/3-gsis-100k-records.test.ts - CLI_REGION: eu-north-1 + CLI_REGION: eu-south-1 depend-on: - publish_to_local_registry - identifier: >- diff --git a/packages/amplify-e2e-tests/schemas/many-to-many.graphql b/packages/amplify-e2e-tests/schemas/many-to-many.graphql new file mode 100644 index 0000000000..e155c58b02 --- /dev/null +++ b/packages/amplify-e2e-tests/schemas/many-to-many.graphql @@ -0,0 +1,12 @@ +type Post @model @auth(rules: [{ allow: public }]) { + id: ID! + title: String! + content: String + tags: [Tag] @manyToMany(relationName: "PostTags") +} + +type Tag @model @auth(rules: [{ allow: public }]) { + id: ID! + label: String! + posts: [Post] @manyToMany(relationName: "PostTags") +} diff --git a/packages/amplify-e2e-tests/schemas/references.graphql b/packages/amplify-e2e-tests/schemas/references.graphql new file mode 100644 index 0000000000..f61e8ad9fc --- /dev/null +++ b/packages/amplify-e2e-tests/schemas/references.graphql @@ -0,0 +1,17 @@ +type Primary @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + relatedMany: [RelatedMany] @hasMany(references: "primaryId") + relatedOne: RelatedOne @hasOne(references: "primaryId") +} + +type RelatedMany @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + primaryId: String + primary: Primary @belongsTo(references: ["primaryId"]) +} + +type RelatedOne @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + primaryId: String + primary: Primary @belongsTo(references: ["primaryId"]) +} diff --git a/packages/amplify-e2e-tests/schemas/simple_model_public_auth.graphql b/packages/amplify-e2e-tests/schemas/simple_model_public_auth.graphql new file mode 100644 index 0000000000..3c5cb48eb8 --- /dev/null +++ b/packages/amplify-e2e-tests/schemas/simple_model_public_auth.graphql @@ -0,0 +1,4 @@ +type Todo @model @auth(rules: [{ allow: public }]) { + id: ID! + content: String +} diff --git a/packages/amplify-graphql-api-construct-tests/package.json b/packages/amplify-graphql-api-construct-tests/package.json index 42e3e75c6e..5a9dc8d785 100644 --- a/packages/amplify-graphql-api-construct-tests/package.json +++ b/packages/amplify-graphql-api-construct-tests/package.json @@ -26,6 +26,7 @@ "@aws-amplify/graphql-api-construct": "1.11.2", "@aws-cdk/aws-cognito-identitypool-alpha": "2.129.0-alpha.0", "@aws-sdk/client-cognito-identity-provider": "3.338.0", + "@aws-sdk/client-dynamodb": "3.338.0", "@aws-sdk/client-lambda": "3.338.0", "@aws-sdk/client-rds": "3.338.0", "@aws-sdk/client-ssm": "3.338.0", diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/base-migration.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/base-migration.test.ts new file mode 100644 index 0000000000..f4540a59c7 --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/base-migration.test.ts @@ -0,0 +1,175 @@ +import * as path from 'path'; +import { createNewProjectDir, deleteProjectDir, deleteProject } from 'amplify-category-api-e2e-core'; +import { initCDKProject, cdkDeploy, cdkDestroy, createGen1ProjectForMigration, deleteDDBTables } from '../../commands'; +import { graphql } from '../../graphql-request'; +import { TestDefinition, writeStackConfig, writeTestDefinitions } from '../../utils'; +import { DURATION_20_MINUTES } from '../../utils/duration-constants'; + +jest.setTimeout(DURATION_20_MINUTES); + +describe('Migration with basic schema', () => { + let gen1ProjRoot: string; + let gen2ProjRoot: string; + let gen1ProjFolderName: string; + let gen2ProjFolderName: string; + let dataSourceMapping: Record; + + beforeEach(async () => { + gen1ProjFolderName = 'basemigrationgen1'; + gen2ProjFolderName = 'basemigrationgen2'; + gen1ProjRoot = await createNewProjectDir(gen1ProjFolderName); + gen2ProjRoot = await createNewProjectDir(gen2ProjFolderName); + }); + + afterEach(async () => { + try { + await deleteProject(gen1ProjRoot); + } catch (_) { + /* No-op */ + } + try { + await cdkDestroy(gen2ProjRoot, '--all'); + } catch (_) { + /* No-op */ + } + + try { + // Tables are set to retain when migrating from gen 1 to gen 2 + // delete the tables to prevent resource leak after test is complete + await deleteDDBTables(Object.values(dataSourceMapping)); + } catch (_) { + /* No-op */ + } + + deleteProjectDir(gen1ProjRoot); + deleteProjectDir(gen2ProjRoot); + }); + + test('Migration with basic schema', async () => { + const { + GraphQLAPIEndpointOutput: gen1APIEndpoint, + GraphQLAPIKeyOutput: gen1APIKey, + DataSourceMappingOutput, + } = await createGen1ProjectForMigration(gen1ProjFolderName, gen1ProjRoot, 'simple_model_public_auth.graphql'); + dataSourceMapping = JSON.parse(DataSourceMappingOutput); + const templatePath = path.resolve(path.join(__dirname, '..', 'backends', 'configurable-stack')); + const name = await initCDKProject(gen2ProjRoot, templatePath); + const testDefinitions: Record = { + 'basic-schema-migration': { + schema: /* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + id: ID! + content: String + } + `, + strategy: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: dataSourceMapping.Todo, + }, + }, + }; + writeStackConfig(gen2ProjRoot, { prefix: gen2ProjFolderName }); + writeTestDefinitions(testDefinitions, gen2ProjRoot); + + const outputs = await cdkDeploy(gen2ProjRoot, '--all'); + const { awsAppsyncApiEndpoint: gen2APIEndpoint, awsAppsyncApiKey: gen2APIKey } = outputs[name]; + + const gen1Result = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + mutation CREATE_TODO { + createTodo(input: { content: "todo desc" }) { + id + content + } + } + `, + ); + // the create mutations are later verified with list queries + expect(gen1Result.statusCode).toEqual(200); + + const gen1Todo = gen1Result.body.data.createTodo; + + const gen2Result = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_TODO { + createTodo(input: { content: "todo desc" }) { + id + content + } + } + `, + ); + expect(gen2Result.statusCode).toEqual(200); + + const gen2Todo = gen2Result.body.data.createTodo; + + const gen1ListResult = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + query LIST_TODOS { + listTodos { + items { + id + content + } + } + } + `, + ); + + expect(gen1ListResult.statusCode).toEqual(200); + expect(gen1ListResult.body.data.listTodos.items.length).toEqual(2); + expect([gen1Todo.id, gen2Todo.id]).toContain(gen1ListResult.body.data.listTodos.items[0].id); + expect([gen1Todo.id, gen2Todo.id]).toContain(gen1ListResult.body.data.listTodos.items[1].id); + + const gen2ListResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + query LIST_TODOS { + listTodos { + items { + id + content + } + } + } + `, + ); + + expect(gen2ListResult.statusCode).toEqual(200); + expect(gen2ListResult.body.data.listTodos.items.length).toEqual(2); + expect([gen1Todo.id, gen2Todo.id]).toContain(gen2ListResult.body.data.listTodos.items[0].id); + expect([gen1Todo.id, gen2Todo.id]).toContain(gen2ListResult.body.data.listTodos.items[1].id); + + await deleteProject(gen1ProjRoot); + + // assert tables have not been deleted after deleting the gen 1 project + + const listResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + query LIST_TODOS { + listTodos { + items { + id + content + } + } + } + `, + ); + + expect(listResult.statusCode).toEqual(200); + expect(listResult.body.data.listTodos.items.length).toEqual(2); + expect([gen1Todo.id, gen2Todo.id]).toContain(listResult.body.data.listTodos.items[0].id); + expect([gen1Todo.id, gen2Todo.id]).toContain(listResult.body.data.listTodos.items[1].id); + }); +}); diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/many-to-many-migration.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/many-to-many-migration.test.ts new file mode 100644 index 0000000000..fe268a69fc --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/many-to-many-migration.test.ts @@ -0,0 +1,287 @@ +import * as path from 'path'; +import { createNewProjectDir, deleteProjectDir, deleteProject } from 'amplify-category-api-e2e-core'; +import { initCDKProject, cdkDeploy, cdkDestroy, createGen1ProjectForMigration, deleteDDBTables } from '../../commands'; +import { graphql } from '../../graphql-request'; +import { TestDefinition, writeStackConfig, writeTestDefinitions } from '../../utils'; +import { DURATION_20_MINUTES } from '../../utils/duration-constants'; + +jest.setTimeout(DURATION_20_MINUTES); + +describe('Many-to-many Migration', () => { + let gen1ProjRoot: string; + let gen2ProjRoot: string; + let gen1ProjFolderName: string; + let gen2ProjFolderName: string; + let dataSourceMapping: Record; + + beforeEach(async () => { + gen1ProjFolderName = 'mtmmigrationgen1'; + gen2ProjFolderName = 'mtmmigrationgen2'; + gen1ProjRoot = await createNewProjectDir(gen1ProjFolderName); + gen2ProjRoot = await createNewProjectDir(gen2ProjFolderName); + }); + + afterEach(async () => { + try { + await deleteProject(gen1ProjRoot); + } catch (_) { + /* No-op */ + } + try { + // await cdkDestroy(gen2ProjRoot, '--all'); + } catch (_) { + /* No-op */ + } + + try { + // Tables are set to retain when migrating from gen 1 to gen 2 + // delete the tables to prevent resource leak after test is complete + await deleteDDBTables(Object.values(dataSourceMapping)); + } catch (_) { + /* No-op */ + } + + deleteProjectDir(gen1ProjRoot); + // deleteProjectDir(gen2ProjRoot); + }); + + test('many-to-many migration', async () => { + const { + GraphQLAPIEndpointOutput: gen1APIEndpoint, + GraphQLAPIKeyOutput: gen1APIKey, + DataSourceMappingOutput, + } = await createGen1ProjectForMigration(gen1ProjFolderName, gen1ProjRoot, 'many-to-many.graphql'); + dataSourceMapping = JSON.parse(DataSourceMappingOutput); + const templatePath = path.resolve(path.join(__dirname, '..', 'backends', 'configurable-stack')); + const name = await initCDKProject(gen2ProjRoot, templatePath); + const testDefinitions: Record = { + post: { + schema: /* GraphQL */ ` + type Post @model @auth(rules: [{ allow: public }]) { + id: ID! + title: String! + content: String + tags: [PostTags] @hasMany(references: ["postId"], indexName: "byPost") + } + `, + strategy: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: dataSourceMapping.Post, + }, + }, + tag: { + schema: /* GraphQL */ ` + type Tag @model @auth(rules: [{ allow: public }]) { + id: ID! + label: String! + posts: [PostTags] @hasMany(references: ["tagId"], indexName: "byTag") + } + `, + strategy: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: dataSourceMapping.Tag, + }, + }, + postTags: { + schema: /* GraphQL */ ` + type PostTags @model @auth(rules: [{ allow: public }]) { + postId: ID @index(name: "byPost") + tagId: ID @index(name: "byTag") + post: Post @belongsTo(references: ["postId"]) + tag: Tag @belongsTo(references: ["tagId"]) + } + `, + strategy: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: dataSourceMapping.PostTags, + }, + }, + }; + writeStackConfig(gen2ProjRoot, { prefix: gen2ProjFolderName }); + writeTestDefinitions(testDefinitions, gen2ProjRoot); + const outputs = await cdkDeploy(gen2ProjRoot, '--all'); + const { awsAppsyncApiEndpoint: gen2APIEndpoint, awsAppsyncApiKey: gen2APIKey } = outputs[name]; + + const gen1PostResult = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + mutation CREATE_POST { + createPost(input: { title: "my post" }) { + id + title + } + } + `, + ); + // the create mutations are later verified with list queries + expect(gen1PostResult.statusCode).toEqual(200); + + const gen1Post = gen1PostResult.body.data.createPost; + + const gen1TagResult = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + mutation CREATE_TAG { + createTag(input: { label: "my tag" }) { + id + label + } + } + `, + ); + expect(gen1TagResult.statusCode).toEqual(200); + + const gen1Tag = gen1TagResult.body.data.createTag; + + const gen1PostTagsResult = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + mutation CREATE_TAG { + createPostTags(input: {tagId: "${gen1Tag.id}", postId: "${gen1Post.id}"}) { + id + postId + tagId + } + } + `, + ); + expect(gen1PostTagsResult.statusCode).toEqual(200); + + const gen1PostTags = gen1PostTagsResult.body.data.createPostTags; + + const gen2PostResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_POST { + createPost(input: { title: "my post" }) { + id + title + } + } + `, + ); + expect(gen2PostResult.statusCode).toEqual(200); + + const gen2Post = gen2PostResult.body.data.createPost; + + const gen2TagResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_TAG { + createTag(input: { label: "my tag" }) { + id + label + } + } + `, + ); + expect(gen2TagResult.statusCode).toEqual(200); + + const gen2Tag = gen2TagResult.body.data.createTag; + + const gen2PostTagsResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_POST_TAG { + createPostTags(input: {tagId: "${gen2Tag.id}", postId: "${gen2Post.id}"}) { + id + postId + tagId + } + } + `, + ); + expect(gen2PostTagsResult.statusCode).toEqual(200); + + const gen1ListResult = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + query LIST_POSTS { + listPosts { + items { + id + tags { + items { + id + } + } + } + } + } + `, + ); + + expect(gen1ListResult.statusCode).toEqual(200); + expect(gen1ListResult.body.data.listPosts.items.length).toEqual(2); + expect([gen1Post.id, gen2Post.id]).toContain(gen1ListResult.body.data.listPosts.items[0].id); + expect([gen1Post.id, gen2Post.id]).toContain(gen1ListResult.body.data.listPosts.items[1].id); + expect(gen1ListResult.body.data.listPosts.items[0].tags.items.length).toEqual(1); + expect(gen1ListResult.body.data.listPosts.items[1].tags.items.length).toEqual(1); + + const gen2ListResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + query LIST_POSTS { + listPosts { + items { + id + tags { + items { + id + } + } + } + } + } + `, + ); + + expect(gen2ListResult.statusCode).toEqual(200); + expect(gen2ListResult.body.data.listPosts.items.length).toEqual(2); + expect([gen1Post.id, gen2Post.id]).toContain(gen2ListResult.body.data.listPosts.items[0].id); + expect([gen1Post.id, gen2Post.id]).toContain(gen2ListResult.body.data.listPosts.items[1].id); + expect(gen2ListResult.body.data.listPosts.items[0].tags.items.length).toEqual(1); + expect(gen2ListResult.body.data.listPosts.items[1].tags.items.length).toEqual(1); + + await deleteProject(gen1ProjRoot); + + // assert tables have not been deleted after deleting the gen 1 project + + const listResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + query LIST_POSTS { + listPosts { + items { + id + tags { + items { + id + } + } + } + } + } + `, + ); + + expect(listResult.statusCode).toEqual(200); + expect(listResult.body.data.listPosts.items.length).toEqual(2); + expect([gen1Post.id, gen2Post.id]).toContain(listResult.body.data.listPosts.items[0].id); + expect([gen1Post.id, gen2Post.id]).toContain(listResult.body.data.listPosts.items[1].id); + expect(listResult.body.data.listPosts.items[0].tags.items.length).toEqual(1); + expect(listResult.body.data.listPosts.items[1].tags.items.length).toEqual(1); + }); +}); diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/references-migration.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/references-migration.test.ts new file mode 100644 index 0000000000..f726865e8e --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/migration/references-migration.test.ts @@ -0,0 +1,307 @@ +import * as path from 'path'; +import { createNewProjectDir, deleteProjectDir, deleteProject } from 'amplify-category-api-e2e-core'; +import { initCDKProject, cdkDeploy, cdkDestroy, createGen1ProjectForMigration, deleteDDBTables } from '../../commands'; +import { graphql } from '../../graphql-request'; +import { TestDefinition, writeStackConfig, writeTestDefinitions } from '../../utils'; +import { DURATION_20_MINUTES } from '../../utils/duration-constants'; + +jest.setTimeout(DURATION_20_MINUTES); + +describe('References Migration', () => { + let gen1ProjRoot: string; + let gen2ProjRoot: string; + let gen1ProjFolderName: string; + let gen2ProjFolderName: string; + let dataSourceMapping: Record; + + beforeEach(async () => { + gen1ProjFolderName = 'referencesgen1'; + gen2ProjFolderName = 'referencesgen2'; + gen1ProjRoot = await createNewProjectDir(gen1ProjFolderName); + gen2ProjRoot = await createNewProjectDir(gen2ProjFolderName); + }); + + afterEach(async () => { + try { + await deleteProject(gen1ProjRoot); + } catch (_) { + /* No-op */ + } + try { + await cdkDestroy(gen2ProjRoot, '--all'); + } catch (_) { + /* No-op */ + } + + try { + // Tables are set to retain when migrating from gen 1 to gen 2 + // delete the tables to prevent resource leak after test is complete + await deleteDDBTables(Object.values(dataSourceMapping)); + } catch (_) { + /* No-op */ + } + + deleteProjectDir(gen1ProjRoot); + deleteProjectDir(gen2ProjRoot); + }); + + test('references migration', async () => { + const { + GraphQLAPIEndpointOutput: gen1APIEndpoint, + GraphQLAPIKeyOutput: gen1APIKey, + DataSourceMappingOutput, + } = await createGen1ProjectForMigration(gen1ProjFolderName, gen1ProjRoot, 'references.graphql'); + dataSourceMapping = JSON.parse(DataSourceMappingOutput); + const templatePath = path.resolve(path.join(__dirname, '..', 'backends', 'configurable-stack')); + const name = await initCDKProject(gen2ProjRoot, templatePath); + const testDefinitions: Record = { + Primary: { + schema: /* GraphQL */ ` + type Primary @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + relatedMany: [RelatedMany] @hasMany(references: "primaryId") + relatedOne: RelatedOne @hasOne(references: "primaryId") + } + `, + strategy: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: dataSourceMapping.Primary, + }, + }, + RelatedMany: { + schema: /* GraphQL */ ` + type RelatedMany @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + primaryId: String + primary: Primary @belongsTo(references: ["primaryId"]) + } + `, + strategy: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: dataSourceMapping.RelatedMany, + }, + }, + RelatedOne: { + schema: /* GraphQL */ ` + type RelatedOne @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + primaryId: String + primary: Primary @belongsTo(references: ["primaryId"]) + } + `, + strategy: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: dataSourceMapping.RelatedOne, + }, + }, + }; + writeStackConfig(gen2ProjRoot, { prefix: gen2ProjFolderName }); + writeTestDefinitions(testDefinitions, gen2ProjRoot); + const outputs = await cdkDeploy(gen2ProjRoot, '--all'); + const { awsAppsyncApiEndpoint: gen2APIEndpoint, awsAppsyncApiKey: gen2APIKey } = outputs[name]; + + const gen1PrimaryResult = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + mutation CREATE_PRIMARY { + createPrimary(input: {}) { + id + } + } + `, + ); + // the create mutations are later verified with list queries + expect(gen1PrimaryResult.statusCode).toEqual(200); + + const gen1Primary = gen1PrimaryResult.body.data.createPrimary; + + const gen2PrimaryResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_PRIMARY { + createPrimary(input: {}) { + id + } + } + `, + ); + expect(gen2PrimaryResult.statusCode).toEqual(200); + + const gen2Primary = gen2PrimaryResult.body.data.createPrimary; + + const gen1RelatedOneResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_RELATED_ONE { + createRelatedOne(input: { primaryId: "${gen1Primary.id}" }) { + id + } + } + `, + ); + expect(gen1RelatedOneResult.statusCode).toEqual(200); + + const gen2RelatedOneResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_RELATED_ONE { + createRelatedOne(input: { primaryId: "${gen2Primary.id}" }) { + id + } + } + `, + ); + expect(gen2RelatedOneResult.statusCode).toEqual(200); + + const gen1RelatedManyResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_RELATED_MANY { + createRelatedMany(input: { primaryId: "${gen1Primary.id}" }) { + id + } + } + `, + ); + expect(gen1RelatedManyResult.statusCode).toEqual(200); + + const gen2RelatedManyResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + mutation CREATE_RELATED_MANY { + createRelatedMany(input: { primaryId: "${gen2Primary.id}" }) { + id + } + } + `, + ); + expect(gen2RelatedManyResult.statusCode).toEqual(200); + + const gen1ListResult = await graphql( + gen1APIEndpoint, + gen1APIKey, + /* GraphQL */ ` + query LIST_PRIMARY { + listPrimaries { + items { + id + relatedMany { + items { + id + primaryId + } + nextToken + } + relatedOne { + id + primaryId + primary { + id + } + } + } + nextToken + } + } + `, + ); + + expect(gen1ListResult.statusCode).toEqual(200); + expect(gen1ListResult.body.data.listPrimaries.items.length).toEqual(2); + expect([gen1Primary.id, gen2Primary.id]).toContain(gen1ListResult.body.data.listPrimaries.items[0].id); + expect([gen1Primary.id, gen2Primary.id]).toContain(gen1ListResult.body.data.listPrimaries.items[1].id); + expect(gen1ListResult.body.data.listPrimaries.items[0].relatedMany.items.length).toEqual(1); + expect(gen1ListResult.body.data.listPrimaries.items[1].relatedMany.items.length).toEqual(1); + expect(gen1ListResult.body.data.listPrimaries.items[0].relatedOne).toBeDefined(); + expect(gen1ListResult.body.data.listPrimaries.items[1].relatedOne).toBeDefined(); + + const gen2ListResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + query LIST_PRIMARY { + listPrimaries { + items { + id + relatedMany { + items { + id + primaryId + } + nextToken + } + relatedOne { + id + primaryId + primary { + id + } + } + } + nextToken + } + } + `, + ); + + expect(gen2ListResult.statusCode).toEqual(200); + expect(gen2ListResult.body.data.listPrimaries.items.length).toEqual(2); + expect([gen1Primary.id, gen2Primary.id]).toContain(gen2ListResult.body.data.listPrimaries.items[0].id); + expect([gen1Primary.id, gen2Primary.id]).toContain(gen2ListResult.body.data.listPrimaries.items[1].id); + expect(gen2ListResult.body.data.listPrimaries.items[0].relatedMany.items.length).toEqual(1); + expect(gen2ListResult.body.data.listPrimaries.items[1].relatedMany.items.length).toEqual(1); + expect(gen2ListResult.body.data.listPrimaries.items[0].relatedOne).toBeDefined(); + expect(gen2ListResult.body.data.listPrimaries.items[1].relatedOne).toBeDefined(); + + await deleteProject(gen1ProjRoot); + + // assert tables have not been deleted after deleting the gen 1 project + + const listResult = await graphql( + gen2APIEndpoint, + gen2APIKey, + /* GraphQL */ ` + query LIST_PRIMARY { + listPrimaries { + items { + id + relatedMany { + items { + id + primaryId + } + nextToken + } + relatedOne { + id + primaryId + primary { + id + } + } + } + nextToken + } + } + `, + ); + + expect(listResult.statusCode).toEqual(200); + expect(listResult.body.data.listPrimaries.items.length).toEqual(2); + expect([gen1Primary.id, gen2Primary.id]).toContain(listResult.body.data.listPrimaries.items[0].id); + expect([gen1Primary.id, gen2Primary.id]).toContain(listResult.body.data.listPrimaries.items[1].id); + expect(listResult.body.data.listPrimaries.items[0].relatedMany.items.length).toEqual(1); + expect(listResult.body.data.listPrimaries.items[1].relatedMany.items.length).toEqual(1); + expect(listResult.body.data.listPrimaries.items[0].relatedOne).toBeDefined(); + expect(listResult.body.data.listPrimaries.items[1].relatedOne).toBeDefined(); + }); +}); diff --git a/packages/amplify-graphql-api-construct-tests/src/commands.ts b/packages/amplify-graphql-api-construct-tests/src/commands.ts index ef86ae2249..88d2609985 100644 --- a/packages/amplify-graphql-api-construct-tests/src/commands.ts +++ b/packages/amplify-graphql-api-construct-tests/src/commands.ts @@ -1,7 +1,17 @@ import * as path from 'path'; import * as fs from 'fs'; import { copySync, moveSync, readFileSync, writeFileSync } from 'fs-extra'; -import { getScriptRunnerPath, sleep, nspawn as spawn } from 'amplify-category-api-e2e-core'; +import { + addApiWithoutSchema, + amplifyPush, + getProjectMeta, + getScriptRunnerPath, + initJSProjectWithProfile, + nspawn as spawn, + sleep, + updateApiSchema, +} from 'amplify-category-api-e2e-core'; +import { DynamoDBClient, DeleteTableCommand } from '@aws-sdk/client-dynamodb'; /** * Retrieve the path to the `npx` executable for interacting with the aws-cdk cli. @@ -160,3 +170,51 @@ export const updateCDKAppWithTemplate = (cwd: string, templatePath: string): voi copySync(templatePath, binDir, { overwrite: true }); moveSync(path.join(binDir, 'app.ts'), path.join(binDir, `${path.basename(cwd)}.ts`), { overwrite: true }); }; + +/** + * Helper function to create a gen 1 project with for migration. + * + * @param name project name + * @param projRoot project root directory + * @param schema schema file to use + */ +export const createGen1ProjectForMigration = async ( + name: string, + projRoot: string, + schema: string, +): Promise<{ + GraphQLAPIEndpointOutput: string; + GraphQLAPIKeyOutput: string; + DataSourceMappingOutput: string; +}> => { + await initJSProjectWithProfile(projRoot, { name }); + await addApiWithoutSchema(projRoot, { transformerVersion: 2 }); + await updateApiSchema(projRoot, name, schema); + await amplifyPush(projRoot); + + // TODO: can't use feature flag until released in CLI + // The test should do a second push after enabling the feature flag to start the migration + // Add the feature flag and push before merging to main. + // addFeatureFlag(projRoot, 'graphqltransformer', 'enableGen2Migration', true); + // await amplifyPush(projRoot); + + const meta = getProjectMeta(projRoot); + const { output } = meta.api[name]; + const { GraphQLAPIEndpointOutput, GraphQLAPIKeyOutput, DataSourceMappingOutput } = output; + + return { + GraphQLAPIEndpointOutput, + GraphQLAPIKeyOutput, + DataSourceMappingOutput, + }; +}; + +/** + * Helper function to delete DDB tables. + * Used to delete tables set to retain on delete. + * @param tableNames table names to delete + */ +export const deleteDDBTables = async (tableNames: string[]): Promise => { + const client = new DynamoDBClient({ region: process.env.CLI_REGION || 'us-west-2' }); + await Promise.allSettled(tableNames.map((tableName) => client.send(new DeleteTableCommand({ TableName: tableName })))); +}; diff --git a/yarn.lock b/yarn.lock index 613e4cc57e..0def0b1e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1120,6 +1120,51 @@ tslib "^2.0.0" uuid "^3.0.0" +"@aws-sdk/client-dynamodb@3.338.0": + version "3.338.0" + resolved "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.338.0.tgz#a63999ef6c764a8572ef76f8d4d375567cc295ed" + integrity sha512-m1tYFDdO2KxD9LvAEZEzixgfFpvNnXz5LHjCG/oVaGCPPMB91AyuyW4NkloCJ0gxmY4WqaHPvWMkwuJ4RJsU7Q== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.338.0" + "@aws-sdk/config-resolver" "3.338.0" + "@aws-sdk/credential-provider-node" "3.338.0" + "@aws-sdk/fetch-http-handler" "3.338.0" + "@aws-sdk/hash-node" "3.338.0" + "@aws-sdk/invalid-dependency" "3.338.0" + "@aws-sdk/middleware-content-length" "3.338.0" + "@aws-sdk/middleware-endpoint" "3.338.0" + "@aws-sdk/middleware-endpoint-discovery" "3.338.0" + "@aws-sdk/middleware-host-header" "3.338.0" + "@aws-sdk/middleware-logger" "3.338.0" + "@aws-sdk/middleware-recursion-detection" "3.338.0" + "@aws-sdk/middleware-retry" "3.338.0" + "@aws-sdk/middleware-serde" "3.338.0" + "@aws-sdk/middleware-signing" "3.338.0" + "@aws-sdk/middleware-stack" "3.338.0" + "@aws-sdk/middleware-user-agent" "3.338.0" + "@aws-sdk/node-config-provider" "3.338.0" + "@aws-sdk/node-http-handler" "3.338.0" + "@aws-sdk/smithy-client" "3.338.0" + "@aws-sdk/types" "3.338.0" + "@aws-sdk/url-parser" "3.338.0" + "@aws-sdk/util-base64" "3.310.0" + "@aws-sdk/util-body-length-browser" "3.310.0" + "@aws-sdk/util-body-length-node" "3.310.0" + "@aws-sdk/util-defaults-mode-browser" "3.338.0" + "@aws-sdk/util-defaults-mode-node" "3.338.0" + "@aws-sdk/util-endpoints" "3.338.0" + "@aws-sdk/util-retry" "3.338.0" + "@aws-sdk/util-user-agent-browser" "3.338.0" + "@aws-sdk/util-user-agent-node" "3.338.0" + "@aws-sdk/util-utf8" "3.310.0" + "@aws-sdk/util-waiter" "3.338.0" + "@smithy/protocol-http" "^1.0.1" + "@smithy/types" "^1.0.0" + tslib "^2.5.0" + uuid "^8.3.2" + "@aws-sdk/client-dynamodb@^3.431.0": version "3.431.0" resolved "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.431.0.tgz#b9537f7e134633d74e6f1f3fad5b9056d2eb31ad" @@ -3660,6 +3705,16 @@ "@aws-sdk/types" "3.6.1" tslib "^1.8.0" +"@aws-sdk/middleware-endpoint-discovery@3.338.0": + version "3.338.0" + resolved "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.338.0.tgz#e0389025e1528f0af37379a3014b29426de2dfce" + integrity sha512-LD2iQ7h65Q89Ef2rEo/tJqSsj6V4UpLjlbbnL7PkDSzOJNCIgRItH1nEXEHL6g94Kef5MQEzAKEg5W/H0ygJ3Q== + dependencies: + "@aws-sdk/endpoint-cache" "3.310.0" + "@aws-sdk/protocol-http" "3.338.0" + "@aws-sdk/types" "3.338.0" + tslib "^2.5.0" + "@aws-sdk/middleware-endpoint-discovery@3.430.0": version "3.430.0" resolved "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.430.0.tgz#1b742f33106fbe4c832122d803f807f2d2239031"