diff --git a/packages/amplify-data-construct/API.md b/packages/amplify-data-construct/API.md index 234d78c273..e5bb1c947c 100644 --- a/packages/amplify-data-construct/API.md +++ b/packages/amplify-data-construct/API.md @@ -31,6 +31,7 @@ import { IAmplifyGraphqlDefinition as IAmplifyDataDefinition } from '@aws-amplif import { IBackendOutputEntry } from '@aws-amplify/graphql-api-construct'; import { IBackendOutputStorageStrategy } from '@aws-amplify/graphql-api-construct'; import { IdentityPoolAuthorizationConfig } from '@aws-amplify/graphql-api-construct'; +import { ImportedAmplifyDynamoDbModelDataSourceStrategy } from '@aws-amplify/graphql-api-construct'; import { LambdaAuthorizationConfig } from '@aws-amplify/graphql-api-construct'; import { ModelDataSourceStrategy } from '@aws-amplify/graphql-api-construct'; import { ModelDataSourceStrategyDbType } from '@aws-amplify/graphql-api-construct'; @@ -114,6 +115,8 @@ export { IBackendOutputStorageStrategy } export { IdentityPoolAuthorizationConfig } +export { ImportedAmplifyDynamoDbModelDataSourceStrategy } + export { LambdaAuthorizationConfig } export { ModelDataSourceStrategy } diff --git a/packages/amplify-data-construct/src/index.ts b/packages/amplify-data-construct/src/index.ts index 80807b4dff..70e36fec9a 100644 --- a/packages/amplify-data-construct/src/index.ts +++ b/packages/amplify-data-construct/src/index.ts @@ -26,6 +26,7 @@ import { IBackendOutputEntry, IBackendOutputStorageStrategy, IdentityPoolAuthorizationConfig, + ImportedAmplifyDynamoDbModelDataSourceStrategy, LambdaAuthorizationConfig, ModelDataSourceStrategy, ModelDataSourceStrategyDbType, @@ -84,6 +85,7 @@ export { IBackendOutputEntry, IBackendOutputStorageStrategy, IdentityPoolAuthorizationConfig, + ImportedAmplifyDynamoDbModelDataSourceStrategy, LambdaAuthorizationConfig, ModelDataSourceStrategy, ModelDataSourceStrategyDbType, diff --git a/packages/amplify-graphql-api-construct/.jsii b/packages/amplify-graphql-api-construct/.jsii index 9db3a2e6aa..7673e2e319 100644 --- a/packages/amplify-graphql-api-construct/.jsii +++ b/packages/amplify-graphql-api-construct/.jsii @@ -3677,7 +3677,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 38 + "line": 39 }, "name": "AmplifyDynamoDbModelDataSourceStrategy", "properties": [ @@ -3689,7 +3689,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 39 + "line": 40 }, "name": "dbType", "type": { @@ -3704,7 +3704,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 40 + "line": 41 }, "name": "provisionStrategy", "type": { @@ -4977,7 +4977,7 @@ { "abstract": true, "docs": { - "remarks": "If no outputStorageStrategey is provided a default strategy will be used.", + "remarks": "If no outputStorageStrategy is provided a default strategy will be used.", "stability": "stable", "summary": "Strategy to store construct outputs." }, @@ -5346,6 +5346,9 @@ { "fqn": "@aws-amplify/graphql-api-construct.AmplifyDynamoDbModelDataSourceStrategy" }, + { + "fqn": "@aws-amplify/graphql-api-construct.ImportedAmplifyDynamoDbModelDataSourceStrategy" + }, { "fqn": "@aws-amplify/graphql-api-construct.SQLLambdaModelDataSourceStrategy" } @@ -5398,6 +5401,9 @@ { "fqn": "@aws-amplify/graphql-api-construct.AmplifyDynamoDbModelDataSourceStrategy" }, + { + "fqn": "@aws-amplify/graphql-api-construct.ImportedAmplifyDynamoDbModelDataSourceStrategy" + }, { "fqn": "@aws-amplify/graphql-api-construct.SQLLambdaModelDataSourceStrategy" } @@ -5793,7 +5799,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 235 + "line": 256 }, "name": "CustomSqlDataSourceStrategy", "properties": [ @@ -5806,7 +5812,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 240 + "line": 261 }, "name": "fieldName", "type": { @@ -5822,7 +5828,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 243 + "line": 264 }, "name": "strategy", "type": { @@ -5838,7 +5844,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 237 + "line": 258 }, "name": "typeName", "type": { @@ -5941,7 +5947,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 30 + "line": 31 }, "name": "DefaultDynamoDbModelDataSourceStrategy", "properties": [ @@ -5953,7 +5959,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 31 + "line": 32 }, "name": "dbType", "type": { @@ -5968,7 +5974,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 32 + "line": 33 }, "name": "provisionStrategy", "type": { @@ -6266,6 +6272,9 @@ { "fqn": "@aws-amplify/graphql-api-construct.AmplifyDynamoDbModelDataSourceStrategy" }, + { + "fqn": "@aws-amplify/graphql-api-construct.ImportedAmplifyDynamoDbModelDataSourceStrategy" + }, { "fqn": "@aws-amplify/graphql-api-construct.SQLLambdaModelDataSourceStrategy" } @@ -6547,6 +6556,70 @@ ], "symbolId": "src/types:IdentityPoolAuthorizationConfig" }, + "@aws-amplify/graphql-api-construct.ImportedAmplifyDynamoDbModelDataSourceStrategy": { + "assembly": "@aws-amplify/graphql-api-construct", + "datatype": true, + "docs": { + "remarks": "Tables can be imported only if they meet the following criteria.\n1. The imported table must have been created with through an Amplify Gen 1 project.\n2. The imported table must be in the same account and region as this construct.\n3. The imported table properties must match the corresponding table properties specified in this construct.\n (AttributeDefinitions, KeySchema, GlobalSecondaryIndexes, BillingModeSummary, ProvisionedThroughput, StreamSpecification, SSEDescription, DeletionProtectionEnabled)\n\nThe imported tables will follow the auth rules defined in this construct.\nThe auth rules of the source Gen 1 project will not apply to the API created by this construct.\nEnsure the correct auth rules have been set to prevent data exposure.", + "stability": "stable", + "summary": "Use custom resource type 'Custom::ImportedAmplifyDynamoDBTable' to manage an imported table." + }, + "fqn": "@aws-amplify/graphql-api-construct.ImportedAmplifyDynamoDbModelDataSourceStrategy", + "kind": "interface", + "locationInModule": { + "filename": "src/model-datasource-strategy-types.ts", + "line": 58 + }, + "name": "ImportedAmplifyDynamoDbModelDataSourceStrategy", + "properties": [ + { + "abstract": true, + "docs": { + "stability": "stable" + }, + "immutable": true, + "locationInModule": { + "filename": "src/model-datasource-strategy-types.ts", + "line": 59 + }, + "name": "dbType", + "type": { + "primitive": "string" + } + }, + { + "abstract": true, + "docs": { + "stability": "stable" + }, + "immutable": true, + "locationInModule": { + "filename": "src/model-datasource-strategy-types.ts", + "line": 60 + }, + "name": "provisionStrategy", + "type": { + "primitive": "string" + } + }, + { + "abstract": true, + "docs": { + "stability": "stable" + }, + "immutable": true, + "locationInModule": { + "filename": "src/model-datasource-strategy-types.ts", + "line": 61 + }, + "name": "tableName", + "type": { + "primitive": "string" + } + } + ], + "symbolId": "src/model-datasource-strategy-types:ImportedAmplifyDynamoDbModelDataSourceStrategy" + }, "@aws-amplify/graphql-api-construct.LambdaAuthorizationConfig": { "assembly": "@aws-amplify/graphql-api-construct", "datatype": true, @@ -7086,7 +7159,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 101 + "line": 122 }, "name": "ProvisionedConcurrencyConfig", "properties": [ @@ -7100,7 +7173,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 103 + "line": 124 }, "name": "provisionedConcurrentExecutions", "type": { @@ -7225,7 +7298,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 46 + "line": 67 }, "name": "SQLLambdaModelDataSourceStrategy", "properties": [ @@ -7238,7 +7311,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 61 + "line": 82 }, "name": "dbConnectionConfig", "type": { @@ -7266,7 +7339,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 56 + "line": 77 }, "name": "dbType", "type": { @@ -7283,7 +7356,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 51 + "line": 72 }, "name": "name", "type": { @@ -7300,7 +7373,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 72 + "line": 93 }, "name": "customSqlStatements", "optional": true, @@ -7322,7 +7395,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 77 + "line": 98 }, "name": "sqlLambdaProvisionedConcurrencyConfig", "optional": true, @@ -7339,7 +7412,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 66 + "line": 87 }, "name": "vpcConfiguration", "optional": true, @@ -7522,7 +7595,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 176 + "line": 197 }, "name": "SqlModelDataSourceSecretsManagerDbConnectionConfig", "properties": [ @@ -7535,7 +7608,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 190 + "line": 211 }, "name": "databaseName", "type": { @@ -7551,7 +7624,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 193 + "line": 214 }, "name": "hostname", "type": { @@ -7567,7 +7640,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 187 + "line": 208 }, "name": "port", "type": { @@ -7584,7 +7657,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 178 + "line": 199 }, "name": "secretArn", "type": { @@ -7601,7 +7674,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 184 + "line": 205 }, "name": "keyArn", "optional": true, @@ -7618,7 +7691,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 198 + "line": 219 }, "name": "sslCertConfig", "optional": true, @@ -7641,7 +7714,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 206 + "line": 227 }, "name": "SqlModelDataSourceSsmDbConnectionConfig", "properties": [ @@ -7654,7 +7727,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 222 + "line": 243 }, "name": "databaseNameSsmPath", "type": { @@ -7671,7 +7744,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 210 + "line": 231 }, "name": "hostnameSsmPath", "type": { @@ -7687,7 +7760,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 219 + "line": 240 }, "name": "passwordSsmPath", "type": { @@ -7703,7 +7776,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 213 + "line": 234 }, "name": "portSsmPath", "type": { @@ -7719,7 +7792,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 216 + "line": 237 }, "name": "usernameSsmPath", "type": { @@ -7735,7 +7808,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 227 + "line": 248 }, "name": "sslCertConfig", "optional": true, @@ -7757,7 +7830,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 155 + "line": 176 }, "name": "SqlModelDataSourceSsmDbConnectionStringConfig", "properties": [ @@ -7771,7 +7844,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 162 + "line": 183 }, "name": "connectionUriSsmPath", "type": { @@ -7801,7 +7874,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 167 + "line": 188 }, "name": "sslCertConfig", "optional": true, @@ -7824,7 +7897,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 134 + "line": 155 }, "name": "SslCertConfig", "symbolId": "src/model-datasource-strategy-types:SslCertConfig" @@ -7842,7 +7915,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 136 + "line": 157 }, "name": "SslCertSsmPathConfig", "properties": [ @@ -7856,7 +7929,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 148 + "line": 169 }, "name": "ssmPath", "type": { @@ -7927,7 +8000,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 111 + "line": 132 }, "name": "SubnetAvailabilityZone", "properties": [ @@ -7940,7 +8013,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 116 + "line": 137 }, "name": "availabilityZone", "type": { @@ -7956,7 +8029,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 113 + "line": 134 }, "name": "subnetId", "type": { @@ -8385,7 +8458,7 @@ "kind": "interface", "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 87 + "line": 108 }, "name": "VpcConfig", "properties": [ @@ -8398,7 +8471,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 92 + "line": 113 }, "name": "securityGroupIds", "type": { @@ -8419,7 +8492,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 95 + "line": 116 }, "name": "subnetAvailabilityZoneConfig", "type": { @@ -8440,7 +8513,7 @@ "immutable": true, "locationInModule": { "filename": "src/model-datasource-strategy-types.ts", - "line": 89 + "line": 110 }, "name": "vpcId", "type": { @@ -8452,5 +8525,5 @@ } }, "version": "1.11.1", - "fingerprint": "fLRs0exr9MTBPSkZ5TNAF8Jli5TTVfukC9TLuSg2Wyw=" + "fingerprint": "WTPIPh83dMH/R4G0mvGyEhYBPrWOizqj9RhGpB/9Z4Y=" } \ No newline at end of file diff --git a/packages/amplify-graphql-api-construct/API.md b/packages/amplify-graphql-api-construct/API.md index 65c2f02b70..062f8fc285 100644 --- a/packages/amplify-graphql-api-construct/API.md +++ b/packages/amplify-graphql-api-construct/API.md @@ -276,6 +276,16 @@ export interface IdentityPoolAuthorizationConfig { readonly unauthenticatedUserRole: IRole; } +// @public +export interface ImportedAmplifyDynamoDbModelDataSourceStrategy { + // (undocumented) + readonly dbType: 'DYNAMODB'; + // (undocumented) + readonly provisionStrategy: 'IMPORTED_AMPLIFY_TABLE'; + // (undocumented) + readonly tableName: string; +} + // @public export interface LambdaAuthorizationConfig { readonly function: IFunction; @@ -283,7 +293,7 @@ export interface LambdaAuthorizationConfig { } // @public -export type ModelDataSourceStrategy = DefaultDynamoDbModelDataSourceStrategy | AmplifyDynamoDbModelDataSourceStrategy | SQLLambdaModelDataSourceStrategy; +export type ModelDataSourceStrategy = DefaultDynamoDbModelDataSourceStrategy | AmplifyDynamoDbModelDataSourceStrategy | ImportedAmplifyDynamoDbModelDataSourceStrategy | SQLLambdaModelDataSourceStrategy; // @public export type ModelDataSourceStrategyDbType = 'DYNAMODB' | ModelDataSourceStrategySqlDbType; diff --git a/packages/amplify-graphql-api-construct/package.json b/packages/amplify-graphql-api-construct/package.json index d4ec2f083f..7020ba7d54 100644 --- a/packages/amplify-graphql-api-construct/package.json +++ b/packages/amplify-graphql-api-construct/package.json @@ -164,7 +164,7 @@ "global": { "branches": 90, "functions": 90, - "lines": 60 + "lines": 59 } }, "coverageReporters": [ diff --git a/packages/amplify-graphql-api-construct/src/model-datasource-strategy-types.ts b/packages/amplify-graphql-api-construct/src/model-datasource-strategy-types.ts index fb350f7a7a..1e3e2270cc 100644 --- a/packages/amplify-graphql-api-construct/src/model-datasource-strategy-types.ts +++ b/packages/amplify-graphql-api-construct/src/model-datasource-strategy-types.ts @@ -12,6 +12,7 @@ export type ModelDataSourceStrategy = | DefaultDynamoDbModelDataSourceStrategy | AmplifyDynamoDbModelDataSourceStrategy + | ImportedAmplifyDynamoDbModelDataSourceStrategy | SQLLambdaModelDataSourceStrategy; /** @@ -40,6 +41,26 @@ export interface AmplifyDynamoDbModelDataSourceStrategy { readonly provisionStrategy: 'AMPLIFY_TABLE'; } +// TODO: decide final naming before merging to main +/** + * Use custom resource type 'Custom::ImportedAmplifyDynamoDBTable' to manage an imported table. + * + * Tables can be imported only if they meet the following criteria. + * 1. The imported table must have been created with through an Amplify Gen 1 project. + * 2. The imported table must be in the same account and region as this construct. + * 3. The imported table properties must match the corresponding table properties specified in this construct. + * (AttributeDefinitions, KeySchema, GlobalSecondaryIndexes, BillingModeSummary, ProvisionedThroughput, StreamSpecification, SSEDescription, DeletionProtectionEnabled) + * + * The imported tables will follow the auth rules defined in this construct. + * The auth rules of the source Gen 1 project will not apply to the API created by this construct. + * Ensure the correct auth rules have been set to prevent data exposure. + */ +export interface ImportedAmplifyDynamoDbModelDataSourceStrategy { + readonly dbType: 'DYNAMODB'; + readonly provisionStrategy: 'IMPORTED_AMPLIFY_TABLE'; + readonly tableName: string; +} + /** * A strategy that creates a Lambda to connect to a pre-existing SQL table to resolve model data. */ diff --git a/packages/amplify-graphql-api-construct/src/types.ts b/packages/amplify-graphql-api-construct/src/types.ts index 966f1a4668..8287565fc4 100644 --- a/packages/amplify-graphql-api-construct/src/types.ts +++ b/packages/amplify-graphql-api-construct/src/types.ts @@ -743,7 +743,7 @@ export interface AmplifyGraphqlApiProps { readonly translationBehavior?: PartialTranslationBehavior; /** - * Strategy to store construct outputs. If no outputStorageStrategey is provided a default strategy will be used. + * Strategy to store construct outputs. If no outputStorageStrategy is provided a default strategy will be used. */ readonly outputStorageStrategy?: IBackendOutputStorageStrategy; diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/amplify-dynamodb-table-generator.test.ts.snap b/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/amplify-dynamodb-table-generator.test.ts.snap index 4ae24865a1..52263383fb 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/amplify-dynamodb-table-generator.test.ts.snap +++ b/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/amplify-dynamodb-table-generator.test.ts.snap @@ -16,19 +16,29 @@ Object { "dynamodb:UpdateTimeToLive", ], "Effect": "Allow", - "Resource": Object { - "Fn::Sub": Array [ - "arn:aws:dynamodb:\${AWS::Region}:\${AWS::AccountId}:table/*-\${apiId}-\${envName}", - Object { - "apiId": Object { - "Ref": "referencetotransformerrootstackGraphQLAPI20497F53ApiId", + "Resource": Array [ + Object { + "Fn::Sub": Array [ + "arn:aws:dynamodb:\${AWS::Region}:\${AWS::AccountId}:table/*-\${apiId}-\${envName}", + Object { + "apiId": Object { + "Ref": "referencetotransformerrootstackGraphQLAPI20497F53ApiId", + }, + "envName": Object { + "Ref": "referencetotransformerrootstackenv10C5A902Ref", + }, }, - "envName": Object { - "Ref": "referencetotransformerrootstackenv10C5A902Ref", + ], + }, + Object { + "Fn::Sub": Array [ + "arn:aws:dynamodb:\${AWS::Region}:\${AWS::AccountId}:table/\${tableName}", + Object { + "tableName": "Author-myApiId-myEnv", }, - }, - ], - }, + ], + }, + ], }, ], "Version": "2012-10-17", diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-construct.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-construct.test.ts index 80a3864dc0..96b62cf785 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-construct.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-construct.test.ts @@ -1,5 +1,9 @@ import * as cdk from 'aws-cdk-lib'; -import { AmplifyDynamoDBTable, CUSTOM_DDB_CFN_TYPE } from '../resources/amplify-dynamodb-table/amplify-dynamodb-table-construct'; +import { + AmplifyDynamoDBTable, + CUSTOM_DDB_CFN_TYPE, + CUSTOM_IMPORTED_DDB_CFN_TYPE, +} from '../resources/amplify-dynamodb-table/amplify-dynamodb-table-construct'; import { AttributeType, StreamViewType, TableEncryption } from 'aws-cdk-lib/aws-dynamodb'; import { Template } from 'aws-cdk-lib/assertions'; @@ -147,4 +151,23 @@ describe('Amplify DynamoDB Table Construct Tests', () => { replaceTableUponGsiUpdate: false, }); }); + it('render the imported amplify dynamodb table in correct form', () => { + const stack = new cdk.Stack(); + new AmplifyDynamoDBTable(stack, 'MockTable', { + customResourceServiceToken: 'mockResourceServiceToken', + tableName: 'mockTableName', + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + isImported: true, + }); + const template = Template.fromStack(stack); + // The correct template should be generated with default input + template.hasResourceProperties(CUSTOM_IMPORTED_DDB_CFN_TYPE, { + ServiceToken: 'mockResourceServiceToken', + tableName: 'mockTableName', + isImported: true, + }); + }); }); diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-generator.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-generator.test.ts index 3713ebc702..59a90cb82b 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-generator.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/amplify-dynamodb-table-generator.test.ts @@ -6,7 +6,7 @@ import { } from '@aws-amplify/graphql-transformer-core'; import { parse } from 'graphql'; import { ModelTransformer } from '../graphql-model-transformer'; -import { CUSTOM_DDB_CFN_TYPE } from '../resources/amplify-dynamodb-table/amplify-dynamodb-table-construct'; +import { CUSTOM_DDB_CFN_TYPE, CUSTOM_IMPORTED_DDB_CFN_TYPE } from '../resources/amplify-dynamodb-table/amplify-dynamodb-table-construct'; import { ITERATIVE_TABLE_STACK_NAME } from '../resources/amplify-dynamodb-table/amplify-dynamo-model-resource-generator'; describe('ModelTransformer:', () => { @@ -20,6 +20,10 @@ describe('ModelTransformer:', () => { id: ID! content: String } + type Author @model { + id: ID! + name: String + } `; const out = testTransform({ schema: validSchema, @@ -27,6 +31,11 @@ describe('ModelTransformer:', () => { dataSourceStrategies: { Comment: DDB_DEFAULT_DATASOURCE_STRATEGY, Post: DDB_AMPLIFY_MANAGED_DATASOURCE_STRATEGY, + Author: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: 'Author-myApiId-myEnv', + }, }, }); expect(out).toBeDefined(); @@ -53,12 +62,22 @@ describe('ModelTransformer:', () => { const commentTable = commentStack.Resources?.CommentTable; expect(commentTable).toBeDefined(); expect(commentTable.Type).toBe('AWS::DynamoDB::Table'); + // Author table resource should be generated within the imported amplify DynamoDB table + const authorStack = out.stacks['Author']; + expect(authorStack).toBeDefined(); + const authorTable = authorStack.Resources?.AuthorTable; + expect(authorTable).toBeDefined(); + expect(authorTable.Type).toBe(CUSTOM_IMPORTED_DDB_CFN_TYPE); + expect(authorTable.UpdateReplacePolicy).toBe('Retain'); + expect(authorTable.DeletionPolicy).toBe('Retain'); + expect(authorTable.Properties.isImported).toBe(true); + expect(authorTable.Properties.tableName).toBe('Author-myApiId-myEnv'); validateModelSchema(parse(out.schema)); // Outputs should contain a reference to the Arn to the entry point (onEventHandler) // of the provider for the AmplifyTableManager custom resource. // If any of these assertions should fail, it is likely caused by a change in the custom resource provider - /** {@link Provider} */ // that caused the entry point ARN to change. + /** {@link Provider} */ // !! This will result in broken redeployments !! // Friends don't let friends mutate custom resource entry point ARNs. const outputs = amplifyTableManagerStack.Outputs!; @@ -87,4 +106,46 @@ describe('ModelTransformer:', () => { const onEventHandlerLambda = amplifyTableManagerResources![onEventHandlerResourceName]; expect(onEventHandlerLambda).toBeDefined(); }); + + it('should throw error when tableName is not set for imported table strategy', async () => { + const validSchema = ` + type Post @model { + id: ID! + title: String! + } + `; + const transformOption = { + schema: validSchema, + transformers: [new ModelTransformer()], + dataSourceStrategies: { + Post: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + }, + }, + }; + // @ts-expect-error tableName is required on dataSourceStrategies.Post + expect(() => testTransform(transformOption)).toThrow('No resource generator assigned for Post with dbType DYNAMODB'); + }); + + it('should throw error when tableName is empty for imported table strategy', async () => { + const validSchema = ` + type Post @model { + id: ID! + title: String! + } + `; + const transformOption = { + schema: validSchema, + transformers: [new ModelTransformer()], + dataSourceStrategies: { + Post: { + dbType: 'DYNAMODB' as const, + provisionStrategy: 'IMPORTED_AMPLIFY_TABLE' as const, + tableName: '', + }, + }, + }; + expect(() => testTransform(transformOption)).toThrow('No resource generator assigned for Post with dbType DYNAMODB'); + }); }); diff --git a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts index 2b48fa7238..3345c33db1 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts @@ -80,6 +80,7 @@ import { DynamoModelResourceGenerator } from './resources/dynamo-model-resource- import { RdsModelResourceGenerator } from './resources/rds-model-resource-generator'; import { ModelTransformerOptions } from './types'; import { AmplifyDynamoModelResourceGenerator } from './resources/amplify-dynamodb-table/amplify-dynamo-model-resource-generator'; +import { isImportedAmplifyDynamoDbModelDataSourceStrategy } from '@aws-amplify/graphql-transformer-core'; /** * Nullable @@ -114,7 +115,8 @@ export class ModelTransformer extends TransformerModelBase implements Transforme this.options = this.getOptions(options); this.resourceGeneratorMap.set(DDB_DB_TYPE, new DynamoModelResourceGenerator()); this.resourceGeneratorMap.set(SQL_LAMBDA_GENERATOR, new RdsModelResourceGenerator()); - this.resourceGeneratorMap.set(ITERATIVE_TABLE_GENERATOR, new AmplifyDynamoModelResourceGenerator()); + const amplifyTableDynamoModelResourceGenerator = new AmplifyDynamoModelResourceGenerator(); + this.resourceGeneratorMap.set(ITERATIVE_TABLE_GENERATOR, amplifyTableDynamoModelResourceGenerator); } before = (ctx: TransformerBeforeStepContextProvider): void => { @@ -133,7 +135,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme this.resourceGeneratorMap.get(SQL_LAMBDA_GENERATOR)?.enableGenerator(); this.resourceGeneratorMap.get(SQL_LAMBDA_GENERATOR)?.enableUnprovisioned(); } - if (strategies.some(isAmplifyDynamoDbModelDataSourceStrategy)) { + if (strategies.some(isAmplifyDynamoDbModelDataSourceStrategy) || strategies.some(isImportedAmplifyDynamoDbModelDataSourceStrategy)) { this.resourceGeneratorMap.get(ITERATIVE_TABLE_GENERATOR)?.enableGenerator(); this.resourceGeneratorMap.get(ITERATIVE_TABLE_GENERATOR)?.enableProvisioned(); } @@ -871,6 +873,8 @@ export class ModelTransformer extends TransformerModelBase implements Transforme generator = this.resourceGeneratorMap.get(ITERATIVE_TABLE_GENERATOR); } else if (isSqlStrategy(strategy)) { generator = this.resourceGeneratorMap.get(SQL_LAMBDA_GENERATOR); + } else if (isImportedAmplifyDynamoDbModelDataSourceStrategy(strategy)) { + generator = this.resourceGeneratorMap.get(ITERATIVE_TABLE_GENERATOR); } if (!generator) { diff --git a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamo-model-resource-generator.ts b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamo-model-resource-generator.ts index 4bbfb46cbb..a5c7b3e57b 100644 --- a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamo-model-resource-generator.ts +++ b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamo-model-resource-generator.ts @@ -2,7 +2,7 @@ import * as cdk from 'aws-cdk-lib'; import { TransformerContextProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { ModelResourceIDs, ResourceConstants } from 'graphql-transformer-common'; import { ObjectTypeDefinitionNode } from 'graphql'; -import { setResourceName } from '@aws-amplify/graphql-transformer-core'; +import { setResourceName, isImportedAmplifyDynamoDbModelDataSourceStrategy } from '@aws-amplify/graphql-transformer-core'; import { AttributeType, StreamViewType, TableEncryption } from 'aws-cdk-lib/aws-dynamodb'; import { Construct } from 'constructs'; import { Duration, aws_iam, aws_lambda } from 'aws-cdk-lib'; @@ -53,6 +53,10 @@ export class AmplifyDynamoModelResourceGenerator extends DynamoModelResourceGene { exclude: ['*.ts'] }, ); + const importedTableNames = Object.values(context.dataSourceStrategies) + .filter(isImportedAmplifyDynamoDbModelDataSourceStrategy) + .map((strategy) => strategy.tableName); + // PolicyDocument that grants access to Create/Update/Delete relevant DynamoDB tables const lambdaPolicyDocument = new aws_iam.PolicyDocument({ statements: [ @@ -73,6 +77,11 @@ export class AmplifyDynamoModelResourceGenerator extends DynamoModelResourceGene apiId: context.api.apiId, envName: context.synthParameters.amplifyEnvironmentName, }), + ...importedTableNames.map((tableName) => + cdk.Fn.sub('arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tableName}', { + tableName, + }), + ), ], }), ], @@ -149,7 +158,9 @@ export class AmplifyDynamoModelResourceGenerator extends DynamoModelResourceGene protected createModelTable(scope: Construct, def: ObjectTypeDefinitionNode, context: TransformerContextProvider): void { const modelName = def!.name.value; const tableLogicalName = ModelResourceIDs.ModelTableResourceID(modelName); - const tableName = context.resourceHelper.generateTableName(modelName); + const strategy = context.dataSourceStrategies[modelName]; + const isTableImported = isImportedAmplifyDynamoDbModelDataSourceStrategy(strategy); + const tableName = isTableImported ? strategy.tableName : context.resourceHelper.generateTableName(modelName); // Add parameters. const { readIops, writeIops, billingMode, pointInTimeRecovery, enableSSE } = this.createDynamoDBParameters(scope, true); @@ -168,7 +179,7 @@ export class AmplifyDynamoModelResourceGenerator extends DynamoModelResourceGene expression: cdk.Fn.conditionEquals(pointInTimeRecovery, 'true'), }); - const removalPolicy = this.options.EnableDeletionProtection ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY; + const removalPolicy = isTableImported || this.options.EnableDeletionProtection ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY; // TODO: The attribute of encryption and TTL should be added const table = new AmplifyDynamoDBTable(scope, `${tableLogicalName}`, { @@ -184,6 +195,7 @@ export class AmplifyDynamoModelResourceGenerator extends DynamoModelResourceGene encryption: TableEncryption.DEFAULT, removalPolicy, ...(context.isProjectUsingDataStore() ? { timeToLiveAttribute: '_ttl' } : undefined), + ...(isTableImported ? { isImported: true } : undefined), }); setResourceName(table, { name: modelName, setOnDefaultChild: false }); diff --git a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamodb-table-construct/index.ts b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamodb-table-construct/index.ts index 53d238e660..0f15177a60 100644 --- a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamodb-table-construct/index.ts +++ b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-dynamodb-table-construct/index.ts @@ -23,11 +23,13 @@ const RANGE_KEY_TYPE = 'RANGE'; const MAX_LOCAL_SECONDARY_INDEX_COUNT = 5; export const CUSTOM_DDB_CFN_TYPE = 'Custom::AmplifyDynamoDBTable'; +export const CUSTOM_IMPORTED_DDB_CFN_TYPE = 'Custom::ImportedAmplifyDynamoDBTable'; export interface AmplifyDynamoDBTableProps extends TableProps { customResourceServiceToken: string; allowDestructiveGraphqlSchemaUpdates?: boolean; replaceTableUponGsiUpdate?: boolean; + isImported?: boolean; } export class AmplifyDynamoDBTable extends Resource { public readonly encryptionKey?: kms.IKey; @@ -71,7 +73,7 @@ export class AmplifyDynamoDBTable extends Resource { // Refer https://docs.aws.amazon.com/cdk/api/v2/docs/constructs.Node.html#defaultchild this.table = new CustomResource(this, 'Default', { serviceToken: this.customResourceServiceToken, - resourceType: CUSTOM_DDB_CFN_TYPE, + resourceType: props.isImported ? CUSTOM_IMPORTED_DDB_CFN_TYPE : CUSTOM_DDB_CFN_TYPE, properties: { tableName: this.tableName, attributeDefinitions: this.attributeDefinitions, @@ -95,6 +97,7 @@ export class AmplifyDynamoDBTable extends Resource { deletionProtectionEnabled: props.deletionProtection, allowDestructiveGraphqlSchemaUpdates: props.allowDestructiveGraphqlSchemaUpdates ?? false, replaceTableUponGsiUpdate: props.replaceTableUponGsiUpdate ?? false, + isImported: props.isImported, }, removalPolicy: props.removalPolicy, }); diff --git a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-manager-lambda/amplify-table-manager-handler.ts b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-manager-lambda/amplify-table-manager-handler.ts index 4d7db4140a..42a2762bd7 100644 --- a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-manager-lambda/amplify-table-manager-handler.ts +++ b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-manager-lambda/amplify-table-manager-handler.ts @@ -130,6 +130,9 @@ const processOnEvent = async (event: AWSCDKAsyncCustomResource.OnEventRequest): let result; switch (event.RequestType) { case 'Create': + if (tableDef.isImported) { + return importTable(tableDef); + } console.log('Initiating CREATE event'); const createTableInput = toCreateTableInput(tableDef); console.log('Create Table Params: ', createTableInput); @@ -946,6 +949,7 @@ const convertStringToBooleanOrNumber = (obj: Record): Record( * @returns void */ const sleep = async (milliseconds: number): Promise => new Promise((resolve) => setTimeout(resolve, milliseconds)); + +/** + * Imports an existing DDB table. + * @param tableDef Tabel definition of imported table. + */ +const importTable = async (tableDef: CustomDDB.Input): Promise => { + // TODO: Add import validation + console.log('Initiating table import process'); + console.log('Fetching current table state'); + console.log(`Table name: ${tableDef.tableName}`); + const describeTableResult = await ddbClient.describeTable({ TableName: tableDef.tableName }); + if (!describeTableResult.Table) { + throw new Error(`Could not find ${tableDef.tableName} to update`); + } + log('Current table state: ', describeTableResult); + const result = { + PhysicalResourceId: describeTableResult.Table.TableName, + Data: { + TableArn: describeTableResult.Table.TableArn, + TableStreamArn: describeTableResult.Table.LatestStreamArn, + TableName: describeTableResult.Table.TableName, + }, + }; + console.log('Returning result: ', result); + return result; +}; + // #endregion Helpers diff --git a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-types.d.ts b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-types.d.ts index 3d2a664dab..9582be441e 100644 --- a/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-types.d.ts +++ b/packages/amplify-graphql-model-transformer/src/resources/amplify-dynamodb-table/amplify-table-types.d.ts @@ -19,6 +19,10 @@ export type Input = CfnTableProps & { * Determines if a table is in sandbox mode. When enabled along with 'allowDestructiveGraphqlSchemaUpdates' , the table will be replaced when GSI updates are detected. This setting is disabled by default. */ replaceTableUponGsiUpdate?: boolean; + /** + * Determines if a table is imported or not. The table will be imported when this value is set to true. + */ + isImported?: boolean; }; /** diff --git a/packages/amplify-graphql-transformer-core/API.md b/packages/amplify-graphql-transformer-core/API.md index a15a768737..73cef13d6e 100644 --- a/packages/amplify-graphql-transformer-core/API.md +++ b/packages/amplify-graphql-transformer-core/API.md @@ -38,6 +38,7 @@ import { GraphQLAPIProvider } from '@aws-amplify/graphql-transformer-interfaces' import { GraphQLError } from 'graphql'; import * as iam from 'aws-cdk-lib/aws-iam'; import { IGrantable } from 'aws-cdk-lib/aws-iam'; +import { ImportedAmplifyDynamoDbModelDataSourceStrategy } from '@aws-amplify/graphql-transformer-interfaces'; import { InlineMappingTemplateProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { InputObjectTypeDefinitionNode } from 'graphql'; import { InputObjectTypeExtensionNode } from 'graphql'; @@ -438,6 +439,9 @@ export const isDynamoDbModel: (ctx: DataSourceStrategiesProvider, typename: stri // @public (undocumented) export const isDynamoDbType: (dbType: ModelDataSourceStrategyDbType) => dbType is "DYNAMODB"; +// @public (undocumented) +export const isImportedAmplifyDynamoDbModelDataSourceStrategy: (strategy: ModelDataSourceStrategy) => strategy is ImportedAmplifyDynamoDbModelDataSourceStrategy; + // @public (undocumented) function isLambdaSyncConfig(syncConfig: SyncConfig): syncConfig is SyncConfigLambda; diff --git a/packages/amplify-graphql-transformer-core/src/index.ts b/packages/amplify-graphql-transformer-core/src/index.ts index 6f4705670c..a593885867 100644 --- a/packages/amplify-graphql-transformer-core/src/index.ts +++ b/packages/amplify-graphql-transformer-core/src/index.ts @@ -56,6 +56,7 @@ export { getSubscriptionFilterInputName, getTable, getType, + isImportedAmplifyDynamoDbModelDataSourceStrategy, isAmplifyDynamoDbModelDataSourceStrategy, isBuiltInGraphqlNode, isDefaultDynamoDbModelDataSourceStrategy, diff --git a/packages/amplify-graphql-transformer-core/src/utils/model-datasource-strategy-utils.ts b/packages/amplify-graphql-transformer-core/src/utils/model-datasource-strategy-utils.ts index 5e460a0b7d..f3dd6fc7dc 100644 --- a/packages/amplify-graphql-transformer-core/src/utils/model-datasource-strategy-utils.ts +++ b/packages/amplify-graphql-transformer-core/src/utils/model-datasource-strategy-utils.ts @@ -2,6 +2,7 @@ import { AmplifyDynamoDbModelDataSourceStrategy, DataSourceStrategiesProvider, DefaultDynamoDbModelDataSourceStrategy, + ImportedAmplifyDynamoDbModelDataSourceStrategy, ModelDataSourceStrategy, ModelDataSourceStrategyDbType, ModelDataSourceStrategySqlDbType, @@ -60,6 +61,21 @@ export const getModelDataSourceStrategy = (ctx: DataSourceStrategiesProvider, ty return strategy; }; +/** + * Type predicate that returns true if `obj` is a AmplifyDynamoDbModelDataSourceStrategy + */ +export const isImportedAmplifyDynamoDbModelDataSourceStrategy = ( + strategy: ModelDataSourceStrategy, +): strategy is ImportedAmplifyDynamoDbModelDataSourceStrategy => { + return ( + isDynamoDbType(strategy.dbType) && + typeof (strategy as any)['provisionStrategy'] === 'string' && + (strategy as any)['provisionStrategy'] === 'IMPORTED_AMPLIFY_TABLE' && + typeof (strategy as any)['tableName'] === 'string' && + (strategy as any)['tableName'] !== '' + ); +}; + /** * Type predicate that returns true if `obj` is a AmplifyDynamoDbModelDataSourceStrategy */ diff --git a/packages/amplify-graphql-transformer-interfaces/API.md b/packages/amplify-graphql-transformer-interfaces/API.md index e2c537d77f..0efd2ca01f 100644 --- a/packages/amplify-graphql-transformer-interfaces/API.md +++ b/packages/amplify-graphql-transformer-interfaces/API.md @@ -215,6 +215,16 @@ export interface GraphQLAPIProvider extends IConstruct { readonly name: string; } +// @public (undocumented) +export interface ImportedAmplifyDynamoDbModelDataSourceStrategy { + // (undocumented) + readonly dbType: 'DYNAMODB'; + // (undocumented) + readonly provisionStrategy: 'IMPORTED_AMPLIFY_TABLE'; + // (undocumented) + readonly tableName: string; +} + // @public (undocumented) export interface InlineMappingTemplateProvider { // (undocumented) @@ -252,7 +262,7 @@ export enum MappingTemplateType { } // @public (undocumented) -export type ModelDataSourceStrategy = DefaultDynamoDbModelDataSourceStrategy | AmplifyDynamoDbModelDataSourceStrategy | SQLLambdaModelDataSourceStrategy; +export type ModelDataSourceStrategy = DefaultDynamoDbModelDataSourceStrategy | AmplifyDynamoDbModelDataSourceStrategy | ImportedAmplifyDynamoDbModelDataSourceStrategy | SQLLambdaModelDataSourceStrategy; // @public (undocumented) export interface ModelDataSourceStrategyBase { diff --git a/packages/amplify-graphql-transformer-interfaces/src/model-datasource/types.ts b/packages/amplify-graphql-transformer-interfaces/src/model-datasource/types.ts index 207dfd1b96..e0ea0d285a 100644 --- a/packages/amplify-graphql-transformer-interfaces/src/model-datasource/types.ts +++ b/packages/amplify-graphql-transformer-interfaces/src/model-datasource/types.ts @@ -9,6 +9,7 @@ export type ModelDataSourceStrategy = | DefaultDynamoDbModelDataSourceStrategy | AmplifyDynamoDbModelDataSourceStrategy + | ImportedAmplifyDynamoDbModelDataSourceStrategy | SQLLambdaModelDataSourceStrategy; export interface ModelDataSourceStrategyBase { @@ -41,6 +42,15 @@ export interface AmplifyDynamoDbModelDataSourceStrategy extends ModelDataSourceS readonly provisionStrategy: 'AMPLIFY_TABLE'; } +/** + * Use custom resource type 'Custom::ImportedAmplifyDynamoDBTable' to provision table. + */ +export interface ImportedAmplifyDynamoDbModelDataSourceStrategy { + readonly dbType: 'DYNAMODB'; + readonly provisionStrategy: 'IMPORTED_AMPLIFY_TABLE'; + readonly tableName: string; +} + /** * A strategy that creates a Lambda to connect to a pre-existing SQL table to resolve model data. *