diff --git a/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-index-transformer.test.ts.snap b/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-index-transformer.test.ts.snap index d820407008..75cccbcfca 100644 --- a/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-index-transformer.test.ts.snap +++ b/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-index-transformer.test.ts.snap @@ -2137,7 +2137,9 @@ $util.qr($mergedValues.put(\\"__typename\\", \\"Test\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"PutItem\\", \\"attributeValues\\": $util.dynamodb.toMapValues($mergedValues), - \\"condition\\": $condition + \\"condition\\": $condition, + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $args.condition ) $util.qr($ctx.stash.conditions.add($args.condition)) @@ -2220,7 +2222,9 @@ $util.qr($ctx.stash.metadata.put(\\"modelObjectKey\\", { #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) #set( $DeleteRequest = { \\"version\\": \\"2018-05-29\\", - \\"operation\\": \\"DeleteItem\\" + \\"operation\\": \\"DeleteItem\\", + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $ctx.stash.metadata.modelObjectKey ) #set( $Key = $ctx.stash.metadata.modelObjectKey ) @@ -2429,7 +2433,9 @@ $util.qr($update.put(\\"expression\\", \\"$expression\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"UpdateItem\\", \\"key\\": $Key, - \\"update\\": $update + \\"update\\": $update, + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $Conditions ) #if( $keyConditionExprNames ) @@ -2849,7 +2855,9 @@ $util.qr($mergedValues.put(\\"__typename\\", \\"Item\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"PutItem\\", \\"attributeValues\\": $util.dynamodb.toMapValues($mergedValues), - \\"condition\\": $condition + \\"condition\\": $condition, + \\"customPartitionKey\\": \\"orderId\\", + \\"populateIndexFields\\": true } ) #if( $args.condition ) $util.qr($ctx.stash.conditions.add($args.condition)) @@ -2932,7 +2940,9 @@ $util.qr($ctx.stash.metadata.put(\\"modelObjectKey\\", { #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) #set( $DeleteRequest = { \\"version\\": \\"2018-05-29\\", - \\"operation\\": \\"DeleteItem\\" + \\"operation\\": \\"DeleteItem\\", + \\"customPartitionKey\\": \\"orderId\\", + \\"populateIndexFields\\": true } ) #if( $ctx.stash.metadata.modelObjectKey ) #set( $Key = $ctx.stash.metadata.modelObjectKey ) @@ -3148,6 +3158,8 @@ $util.qr($update.put(\\"expression\\", \\"$expression\\")) \\"operation\\": \\"UpdateItem\\", \\"key\\": $Key, \\"update\\": $update, + \\"customPartitionKey\\": \\"orderId\\", + \\"populateIndexFields\\": true, \\"_version\\": $util.defaultIfNull($args.input[\\"_version\\"], 0) } ) #if( $Conditions ) @@ -3911,14 +3923,12 @@ null { \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"Sync\\", - \\"filter\\": #if( $filter ) -$util.toJson($filter) - #else -null - #end, + \\"filter\\": #if( $filter ) $util.toJson($filter) #else null #end, \\"limit\\": $util.defaultIfNull($args.limit, 100), \\"lastSync\\": $util.toJson($util.defaultIfNull($args.lastSync, null)), - \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)) + \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)), + \\"basePartitionKey\\": #if( $filter && $$filter.orderId ) $filter.orderId.eq #else null #end, + \\"deltaIndexName\\": \\"deltaSyncGSI\\" } ## [End] Sync Request template. **", "Query.syncItems.res.vtl": "## [Start] ResponseTemplate. ** @@ -4660,11 +4670,7 @@ null { \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"Sync\\", - \\"filter\\": #if( $filter ) -$util.toJson($filter) - #else -null - #end, + \\"filter\\": #if( $filter ) $util.toJson($filter) #else null #end, \\"limit\\": $util.defaultIfNull($args.limit, 100), \\"lastSync\\": $util.toJson($util.defaultIfNull($args.lastSync, null)), \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)) @@ -5409,11 +5415,7 @@ null { \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"Sync\\", - \\"filter\\": #if( $filter ) -$util.toJson($filter) - #else -null - #end, + \\"filter\\": #if( $filter ) $util.toJson($filter) #else null #end, \\"limit\\": $util.defaultIfNull($args.limit, 100), \\"lastSync\\": $util.toJson($util.defaultIfNull($args.lastSync, null)), \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)) @@ -5539,7 +5541,9 @@ $util.qr($mergedValues.put(\\"__typename\\", \\"Item\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"PutItem\\", \\"attributeValues\\": $util.dynamodb.toMapValues($mergedValues), - \\"condition\\": $condition + \\"condition\\": $condition, + \\"customPartitionKey\\": \\"orderId\\", + \\"populateIndexFields\\": true } ) #if( $args.condition ) $util.qr($ctx.stash.conditions.add($args.condition)) @@ -5622,7 +5626,9 @@ $util.qr($ctx.stash.metadata.put(\\"modelObjectKey\\", { #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) #set( $DeleteRequest = { \\"version\\": \\"2018-05-29\\", - \\"operation\\": \\"DeleteItem\\" + \\"operation\\": \\"DeleteItem\\", + \\"customPartitionKey\\": \\"orderId\\", + \\"populateIndexFields\\": true } ) #if( $ctx.stash.metadata.modelObjectKey ) #set( $Key = $ctx.stash.metadata.modelObjectKey ) @@ -5836,7 +5842,9 @@ $util.qr($update.put(\\"expression\\", \\"$expression\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"UpdateItem\\", \\"key\\": $Key, - \\"update\\": $update + \\"update\\": $update, + \\"customPartitionKey\\": \\"orderId\\", + \\"populateIndexFields\\": true } ) #if( $Conditions ) #if( $keyConditionExprNames ) diff --git a/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-primary-key-transformer.test.ts.snap b/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-primary-key-transformer.test.ts.snap index 6a2998cf14..0da358eb72 100644 --- a/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-primary-key-transformer.test.ts.snap +++ b/packages/amplify-graphql-index-transformer/src/__tests__/__snapshots__/amplify-graphql-primary-key-transformer.test.ts.snap @@ -48,7 +48,9 @@ $util.qr($mergedValues.put(\\"__typename\\", \\"Test\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"PutItem\\", \\"attributeValues\\": $util.dynamodb.toMapValues($mergedValues), - \\"condition\\": $condition + \\"condition\\": $condition, + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $args.condition ) $util.qr($ctx.stash.conditions.add($args.condition)) @@ -131,7 +133,9 @@ $util.qr($ctx.stash.metadata.put(\\"modelObjectKey\\", { #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) #set( $DeleteRequest = { \\"version\\": \\"2018-05-29\\", - \\"operation\\": \\"DeleteItem\\" + \\"operation\\": \\"DeleteItem\\", + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $ctx.stash.metadata.modelObjectKey ) #set( $Key = $ctx.stash.metadata.modelObjectKey ) @@ -345,7 +349,9 @@ $util.qr($update.put(\\"expression\\", \\"$expression\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"UpdateItem\\", \\"key\\": $Key, - \\"update\\": $update + \\"update\\": $update, + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $Conditions ) #if( $keyConditionExprNames ) @@ -694,7 +700,9 @@ $util.qr($mergedValues.put(\\"__typename\\", \\"Test\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"PutItem\\", \\"attributeValues\\": $util.dynamodb.toMapValues($mergedValues), - \\"condition\\": $condition + \\"condition\\": $condition, + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $args.condition ) $util.qr($ctx.stash.conditions.add($args.condition)) @@ -777,7 +785,9 @@ $util.qr($ctx.stash.metadata.put(\\"modelObjectKey\\", { #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) #set( $DeleteRequest = { \\"version\\": \\"2018-05-29\\", - \\"operation\\": \\"DeleteItem\\" + \\"operation\\": \\"DeleteItem\\", + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $ctx.stash.metadata.modelObjectKey ) #set( $Key = $ctx.stash.metadata.modelObjectKey ) @@ -986,7 +996,9 @@ $util.qr($update.put(\\"expression\\", \\"$expression\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"UpdateItem\\", \\"key\\": $Key, - \\"update\\": $update + \\"update\\": $update, + \\"customPartitionKey\\": \\"email\\", + \\"populateIndexFields\\": true } ) #if( $Conditions ) #if( $keyConditionExprNames ) @@ -1837,7 +1849,9 @@ $util.qr($mergedValues.put(\\"__typename\\", \\"Test\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"PutItem\\", \\"attributeValues\\": $util.dynamodb.toMapValues($mergedValues), - \\"condition\\": $condition + \\"condition\\": $condition, + \\"customPartitionKey\\": \\"status\\", + \\"populateIndexFields\\": true } ) #if( $args.condition ) $util.qr($ctx.stash.conditions.add($args.condition)) @@ -1920,7 +1934,9 @@ $util.qr($ctx.stash.metadata.put(\\"modelObjectKey\\", { #set( $args = $util.defaultIfNull($ctx.stash.transformedArgs, $ctx.args) ) #set( $DeleteRequest = { \\"version\\": \\"2018-05-29\\", - \\"operation\\": \\"DeleteItem\\" + \\"operation\\": \\"DeleteItem\\", + \\"customPartitionKey\\": \\"status\\", + \\"populateIndexFields\\": true } ) #if( $ctx.stash.metadata.modelObjectKey ) #set( $Key = $ctx.stash.metadata.modelObjectKey ) @@ -2129,7 +2145,9 @@ $util.qr($update.put(\\"expression\\", \\"$expression\\")) \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"UpdateItem\\", \\"key\\": $Key, - \\"update\\": $update + \\"update\\": $update, + \\"customPartitionKey\\": \\"status\\", + \\"populateIndexFields\\": true } ) #if( $Conditions ) #if( $keyConditionExprNames ) diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/model-transformer.test.ts.snap b/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/model-transformer.test.ts.snap index 831bb57eac..e3a3677ca6 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/model-transformer.test.ts.snap +++ b/packages/amplify-graphql-model-transformer/src/__tests__/__snapshots__/model-transformer.test.ts.snap @@ -1216,11 +1216,7 @@ $util.toJson({}) { \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"Sync\\", - \\"filter\\": #if( $filter ) -$util.toJson($filter) - #else -null - #end, + \\"filter\\": #if( $filter ) $util.toJson($filter) #else null #end, \\"limit\\": $util.defaultIfNull($args.limit, 100), \\"lastSync\\": $util.toJson($util.defaultIfNull($args.lastSync, null)), \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)) @@ -1758,11 +1754,7 @@ $util.toJson({}) { \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"Sync\\", - \\"filter\\": #if( $filter ) -$util.toJson($filter) - #else -null - #end, + \\"filter\\": #if( $filter ) $util.toJson($filter) #else null #end, \\"limit\\": $util.defaultIfNull($args.limit, 100), \\"lastSync\\": $util.toJson($util.defaultIfNull($args.lastSync, null)), \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)) @@ -2300,11 +2292,7 @@ $util.toJson({}) { \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"Sync\\", - \\"filter\\": #if( $filter ) -$util.toJson($filter) - #else -null - #end, + \\"filter\\": #if( $filter ) $util.toJson($filter) #else null #end, \\"limit\\": $util.defaultIfNull($args.limit, 100), \\"lastSync\\": $util.toJson($util.defaultIfNull($args.lastSync, null)), \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)) @@ -4342,11 +4330,7 @@ exports[`ModelTransformer: the datastore table should be configured 1`] = ` { \\"version\\": \\"2018-05-29\\", \\"operation\\": \\"Sync\\", - \\"filter\\": #if( $filter ) -$util.toJson($filter) - #else -null - #end, + \\"filter\\": #if( $filter ) $util.toJson($filter) #else null #end, \\"limit\\": $util.defaultIfNull($args.limit, 100), \\"lastSync\\": $util.toJson($util.defaultIfNull($args.lastSync, null)), \\"nextToken\\": $util.toJson($util.defaultIfNull($args.nextToken, null)) diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts index 2dd58371e0..2dcf54b241 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -1143,8 +1143,34 @@ describe('ModelTransformer: ', () => { AttributeName: 'ds_sk', AttributeType: 'S', }, + { + AttributeName: 'gsi_ds_pk', + AttributeType: 'S', + }, + { + AttributeName: 'gsi_ds_sk', + AttributeType: 'S', + } ], BillingMode: 'PAY_PER_REQUEST', + GlobalSecondaryIndexes: [ + { + IndexName: "deltaSyncGSI", + KeySchema: [ + { + AttributeName: "gsi_ds_pk", + KeyType: "HASH" + }, + { + AttributeName: "gsi_ds_sk", + KeyType: "RANGE" + } + ], + Projection: { + ProjectionType: "ALL" + } + } + ], StreamSpecification: { StreamViewType: 'NEW_AND_OLD_IMAGES', }, 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 63099626bc..6eb7c34d4d 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts @@ -45,6 +45,7 @@ import { FieldDefinitionNode, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + ListValueNode, ObjectTypeDefinitionNode, } from 'graphql'; import { @@ -143,6 +144,8 @@ type ModelTransformerOptions = { SyncConfig?: SyncConfig; }; +const DEFAULT_ID_FIELD_NAME = 'id'; + /** * ModelTransformer */ @@ -504,13 +507,15 @@ export class ModelTransformer extends TransformerModelBase implements Transforme const dataSource = this.datasourceMap[type.name.value]; const resolverKey = `Update${generateResolverKey(typeName, fieldName)}`; if (!this.resolverMap[resolverKey]) { + const hasCustomPrimaryKey = this.hasCustomPrimaryKey(type); + const partitionKey = this.getPartitionKey(type); const resolver = ctx.resolvers.generateMutationResolver( typeName, fieldName, resolverLogicalId, dataSource, MappingTemplate.s3MappingTemplateFromString( - generateUpdateRequestTemplate(typeName, isSyncEnabled), + generateUpdateRequestTemplate(typeName, isSyncEnabled, hasCustomPrimaryKey, partitionKey), `${typeName}.${fieldName}.req.vtl`, ), MappingTemplate.s3MappingTemplateFromString( @@ -541,13 +546,15 @@ export class ModelTransformer extends TransformerModelBase implements Transforme const isSyncEnabled = ctx.isProjectUsingDataStore(); const dataSource = this.datasourceMap[type.name.value]; const resolverKey = `delete${generateResolverKey(typeName, fieldName)}`; + const hasCustomPrimaryKey = this.hasCustomPrimaryKey(type); + const partitionKey = this.getPartitionKey(type); if (!this.resolverMap[resolverKey]) { this.resolverMap[resolverKey] = ctx.resolvers.generateMutationResolver( typeName, fieldName, resolverLogicalId, dataSource, - MappingTemplate.s3MappingTemplateFromString(generateDeleteRequestTemplate(isSyncEnabled), `${typeName}.${fieldName}.req.vtl`), + MappingTemplate.s3MappingTemplateFromString(generateDeleteRequestTemplate(isSyncEnabled, hasCustomPrimaryKey, partitionKey), `${typeName}.${fieldName}.req.vtl`), MappingTemplate.s3MappingTemplateFromString( generateDefaultResponseMappingTemplate(isSyncEnabled, true), `${typeName}.${fieldName}.res.vtl`, @@ -625,12 +632,14 @@ export class ModelTransformer extends TransformerModelBase implements Transforme const dataSource = this.datasourceMap[type.name.value]; const resolverKey = `Sync${generateResolverKey(typeName, fieldName)}`; if (!this.resolverMap[resolverKey]) { + const hasCustomPrimaryKey = this.hasCustomPrimaryKey(type); + const partitionKeyName = this.getPartitionKeyName(type); this.resolverMap[resolverKey] = ctx.resolvers.generateQueryResolver( typeName, fieldName, resolverLogicalId, dataSource, - MappingTemplate.s3MappingTemplateFromString(generateSyncRequestTemplate(), `${typeName}.${fieldName}.req.vtl`), + MappingTemplate.s3MappingTemplateFromString(generateSyncRequestTemplate(hasCustomPrimaryKey, partitionKeyName), `${typeName}.${fieldName}.req.vtl`), MappingTemplate.s3MappingTemplateFromString( generateDefaultResponseMappingTemplate(isSyncEnabled), `${typeName}.${fieldName}.res.vtl`, @@ -879,12 +888,14 @@ export class ModelTransformer extends TransformerModelBase implements Transforme const resolverKey = `Create${generateResolverKey(typeName, fieldName)}`; const modelIndexFields = type.fields!.filter(field => field.directives?.some(it => it.name.value === 'index')).map(it => it.name.value); if (!this.resolverMap[resolverKey]) { + const hasCustomPrimaryKey = this.hasCustomPrimaryKey(type); + const partitionKey = this.getPartitionKey(type); const resolver = ctx.resolvers.generateMutationResolver( typeName, fieldName, resolverLogicalId, dataSource, - MappingTemplate.s3MappingTemplateFromString(generateCreateRequestTemplate(type.name.value, modelIndexFields), `${typeName}.${fieldName}.req.vtl`), + MappingTemplate.s3MappingTemplateFromString(generateCreateRequestTemplate(type.name.value, modelIndexFields, hasCustomPrimaryKey, partitionKey), `${typeName}.${fieldName}.req.vtl`), MappingTemplate.s3MappingTemplateFromString( generateDefaultResponseMappingTemplate(isSyncEnabled, true), `${typeName}.${fieldName}.res.vtl`, @@ -1391,4 +1402,56 @@ export class ModelTransformer extends TransformerModelBase implements Transforme EnableDeletionProtection: false, ...options, }); + + /** + * Returns true if the model contains a custom primary key. + * Custom Primary Key is a renamed partition key with at least one sort key. + * @param obj ObjectTypeDefinitionNode + * @returns a boolean + */ + private hasCustomPrimaryKey = (obj: ObjectTypeDefinitionNode): boolean => { + const primaryKeyField = obj.fields?.find(field => field.directives?.find(directive => directive.name.value === 'primaryKey')); + if (!primaryKeyField || primaryKeyField.name.value === DEFAULT_ID_FIELD_NAME) { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + const primaryKeyDirective = primaryKeyField.directives?.find(directive => directive.name.value === 'primaryKey')!; + const sortKeysArgument = primaryKeyDirective.arguments?.find(arg => arg.name.value === 'sortKeyFields'); + if (sortKeysArgument && sortKeysArgument.value?.kind == 'StringValue') { + return true; + } + if (!sortKeysArgument || (sortKeysArgument.value as ListValueNode)?.values?.length === 0) { + return false; + } + + return true; + } + + /** + * Returns partition key of the type + * Custom Primary Key is a renamed partition key with at least one sort key. + * @param obj ObjectTypeDefinitionNode + * @returns a string + */ + private getPartitionKey = (obj: ObjectTypeDefinitionNode): string => { + const primaryKeyField = obj.fields?.find(field => field.directives?.find(directive => directive.name.value === 'primaryKey')); + if (!primaryKeyField || primaryKeyField.name.value === DEFAULT_ID_FIELD_NAME) { + return DEFAULT_ID_FIELD_NAME; + } + return primaryKeyField.name.value; + } + + /** + * Returns the field name of the partition key. + * @param obj ObjectTypeDefinitionNode + * @returns a string + */ + private getPartitionKeyName = (obj: ObjectTypeDefinitionNode): string => { + const primaryKeyField = obj.fields?.find(field => field.directives?.find(directive => directive.name.value === 'primaryKey')); + if (!primaryKeyField) { + return DEFAULT_ID_FIELD_NAME; + } + return primaryKeyField.name.value; + } } diff --git a/packages/amplify-graphql-model-transformer/src/resolvers/mutation.ts b/packages/amplify-graphql-model-transformer/src/resolvers/mutation.ts index beed0fcedc..a3a74d07b8 100644 --- a/packages/amplify-graphql-model-transformer/src/resolvers/mutation.ts +++ b/packages/amplify-graphql-model-transformer/src/resolvers/mutation.ts @@ -26,7 +26,7 @@ import { generateConditionSlot } from './common'; * Generates VTL template in update mutation * @param modelName Name of the model */ -export const generateUpdateRequestTemplate = (modelName: string, isSyncEnabled: boolean): string => { +export const generateUpdateRequestTemplate = (modelName: string, isSyncEnabled: boolean, hasCustomPrimaryKey: boolean, partitionKey: string): string => { const objectKeyVariable = 'ctx.stash.metadata.modelObjectKey'; const keyFields: StringNode[] = [str('id')]; if (isSyncEnabled) { @@ -133,6 +133,10 @@ export const generateUpdateRequestTemplate = (modelName: string, isSyncEnabled: operation: str('UpdateItem'), key: ref('Key'), update: ref('update'), + ...(hasCustomPrimaryKey && { + customPartitionKey: str(`${partitionKey}`), + populateIndexFields: raw(`${hasCustomPrimaryKey}`), + }), ...(isSyncEnabled && { _version: ref('util.defaultIfNull($args.input["_version"], 0)') }), }), ), @@ -152,7 +156,7 @@ export const generateUpdateRequestTemplate = (modelName: string, isSyncEnabled: * Generates VTL template in create mutation * @param modelName Name of the model */ -export const generateCreateRequestTemplate = (modelName: string, modelIndexFields: string[]): string => { +export const generateCreateRequestTemplate = (modelName: string, modelIndexFields: string[], hasCustomPrimaryKey: boolean, partitionKey: string): string => { const statements: Expression[] = [ setArgs, // Generate conditions @@ -182,6 +186,10 @@ export const generateCreateRequestTemplate = (modelName: string, modelIndexField operation: str('PutItem'), attributeValues: methodCall(ref('util.dynamodb.toMapValues'), ref('mergedValues')), condition: ref('condition'), + ...(hasCustomPrimaryKey && { + customPartitionKey: str(`${partitionKey}`), + populateIndexFields: raw(`${hasCustomPrimaryKey}`), + }), }), ), @@ -256,7 +264,7 @@ export const generateCreateInitSlotTemplate = (modelConfig: ModelDirectiveConfig * Generates VTL template in delete mutation * */ -export const generateDeleteRequestTemplate = (isSyncEnabled: boolean): string => { +export const generateDeleteRequestTemplate = (isSyncEnabled: boolean, hasCustomPrimaryKey: boolean, partitionKey: string): string => { const statements: Expression[] = [ setArgs, set( @@ -264,6 +272,10 @@ export const generateDeleteRequestTemplate = (isSyncEnabled: boolean): string => obj({ version: str('2018-05-29'), operation: str('DeleteItem'), + ...(hasCustomPrimaryKey && { + customPartitionKey: str(`${partitionKey}`), + populateIndexFields: raw(`${hasCustomPrimaryKey}`), + }), }), ), ifElse( diff --git a/packages/amplify-graphql-model-transformer/src/resolvers/query.ts b/packages/amplify-graphql-model-transformer/src/resolvers/query.ts index 28276af07a..cb6f02bc8d 100644 --- a/packages/amplify-graphql-model-transformer/src/resolvers/query.ts +++ b/packages/amplify-graphql-model-transformer/src/resolvers/query.ts @@ -20,12 +20,14 @@ import { list, forEach, nul, + raw, } from 'graphql-mapping-template'; -import { ResourceConstants, setArgs } from 'graphql-transformer-common'; +import { ResourceConstants, setArgs, SyncResourceIDs } from 'graphql-transformer-common'; + const authFilter = ref('ctx.stash.authFilter'); /** - * Generate get query resolver template + * Generate get query resolver request template */ export const generateGetRequestTemplate = (): string => { const statements: Expression[] = [ @@ -74,6 +76,9 @@ export const generateGetRequestTemplate = (): string => { return printBlock('Get Request template')(compoundExpression(statements)); }; +/** + * Generates the Get query response template + */ export const generateGetResponseTemplate = (isSyncEnabled: boolean): string => { const statements = new Array(); if (isSyncEnabled) { @@ -96,13 +101,16 @@ export const generateGetResponseTemplate = (isSyncEnabled: boolean): string => { return printBlock('Get Response template')(compoundExpression(statements)); }; +/** + * Generates the List query request template + */ export const generateListRequestTemplate = (): string => { const requestVariable = 'ListRequest'; const modelQueryObj = 'ctx.stash.modelQueryExpression'; const indexNameVariable = 'ctx.stash.metadata.index'; const expression = compoundExpression([ setArgs, - set(ref('limit'), methodCall(ref(`util.defaultIfNull`), ref('args.limit'), int(100))), + set(ref('limit'), methodCall(ref('util.defaultIfNull'), ref('args.limit'), int(100))), set( ref(requestVariable), obj({ @@ -123,7 +131,7 @@ export const generateListRequestTemplate = (): string => { not(isNullOrEmpty(ref('filter'))), compoundExpression([ set( - ref(`filterExpression`), + ref('filterExpression'), methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('filter'))), ), iff( @@ -137,7 +145,7 @@ export const generateListRequestTemplate = (): string => { equals(methodCall(ref('filterExpression.expressionValues.size')), int(0)), qref(methodCall(ref('filterExpression.remove'), str('expressionValues'))), ), - set(ref(`${requestVariable}.filter`), ref(`filterExpression`)), + set(ref(`${requestVariable}.filter`), ref('filterExpression')), ]), ), ]), @@ -164,45 +172,58 @@ export const generateListRequestTemplate = (): string => { return printBlock('List Request')(expression); }; -export const generateSyncRequestTemplate = (): string => { - return printBlock('Sync Request template')( - compoundExpression([ - setArgs, - ifElse( - not(isNullOrEmpty(authFilter)), - compoundExpression([ - set(ref('filter'), authFilter), - iff(not(isNullOrEmpty(ref('args.filter'))), set(ref('filter'), obj({ and: list([ref('filter'), ref('args.filter')]) }))), - ]), - iff(not(isNullOrEmpty(ref('args.filter'))), set(ref('filter'), ref('args.filter'))), - ), - iff( - not(isNullOrEmpty(ref('filter'))), - compoundExpression([ - set( - ref(`filterExpression`), - methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('filter'))), - ), - iff( - not(methodCall(ref('util.isNullOrBlank'), ref('filterExpression.expression'))), - compoundExpression([ - iff( - equals(methodCall(ref('filterExpression.expressionValues.size')), int(0)), - qref(methodCall(ref('filterExpression.remove'), str('expressionValues'))), - ), - set(ref('filter'), ref('filterExpression')), - ]), - ), - ]), - ), - obj({ - version: str('2018-05-29'), - operation: str('Sync'), - filter: ifElse(ref('filter'), ref('util.toJson($filter)'), nul()), - limit: ref(`util.defaultIfNull($args.limit, ${ResourceConstants.DEFAULT_SYNC_QUERY_PAGE_LIMIT})`), - lastSync: ref('util.toJson($util.defaultIfNull($args.lastSync, null))'), - nextToken: ref('util.toJson($util.defaultIfNull($args.nextToken, null))'), +/** + * Generates the Sync query request template + */ +export const generateSyncRequestTemplate = (hasCustomPrimaryKey: boolean, partitionKeyName: string): string => printBlock('Sync Request template')( + compoundExpression([ + setArgs, + ifElse( + not(isNullOrEmpty(authFilter)), + compoundExpression([ + set(ref('filter'), authFilter), + iff(not(isNullOrEmpty(ref('args.filter'))), set(ref('filter'), obj({ and: list([ref('filter'), ref('args.filter')]) }))), + ]), + iff(not(isNullOrEmpty(ref('args.filter'))), set(ref('filter'), ref('args.filter'))), + ), + iff( + not(isNullOrEmpty(ref('filter'))), + compoundExpression([ + set( + ref('filterExpression'), + methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('filter'))), + ), + iff( + not(methodCall(ref('util.isNullOrBlank'), ref('filterExpression.expression'))), + compoundExpression([ + iff( + equals(methodCall(ref('filterExpression.expressionValues.size')), int(0)), + qref(methodCall(ref('filterExpression.remove'), str('expressionValues'))), + ), + set(ref('filter'), ref('filterExpression')), + ]), + ), + ]), + ), + obj({ + version: str('2018-05-29'), + operation: str('Sync'), + filter: ifElse(ref('filter'), ref('util.toJson($filter)'), nul(), true), + limit: ref(`util.defaultIfNull($args.limit, ${ResourceConstants.DEFAULT_SYNC_QUERY_PAGE_LIMIT})`), + lastSync: ref('util.toJson($util.defaultIfNull($args.lastSync, null))'), + nextToken: ref('util.toJson($util.defaultIfNull($args.nextToken, null))'), + ...(hasCustomPrimaryKey && { + basePartitionKey: ifElse( + and([ + ref('filter'), + ref(`$filter.${partitionKeyName}`), + ]), + raw(`$filter.${partitionKeyName}.eq`), + nul(), + true, + ), + deltaIndexName: str(SyncResourceIDs.syncGSIName), }), - ]), - ); -}; + }), + ]), +); diff --git a/packages/amplify-graphql-transformer-core/src/transformation/sync-utils.ts b/packages/amplify-graphql-transformer-core/src/transformation/sync-utils.ts index 26a61751f7..b929dd40ca 100644 --- a/packages/amplify-graphql-transformer-core/src/transformation/sync-utils.ts +++ b/packages/amplify-graphql-transformer-core/src/transformation/sync-utils.ts @@ -1,27 +1,34 @@ -import { AttributeType, BillingMode, StreamViewType, Table } from '@aws-cdk/aws-dynamodb'; +import { + AttributeType, BillingMode, StreamViewType, Table, +} from '@aws-cdk/aws-dynamodb'; import * as cdk from '@aws-cdk/core'; import * as iam from '@aws-cdk/aws-iam'; import { ResourceConstants, SyncResourceIDs } from 'graphql-transformer-common'; -import { TransformerContext } from '../transformer-context'; -import { ResolverConfig, SyncConfig, SyncConfigLambda } from '../config/transformer-config'; import { StackManagerProvider, TransformerContextProvider, TransformerSchemaVisitStepContextProvider, TransformerTransformSchemaStepContextProvider, } from '@aws-amplify/graphql-transformer-interfaces'; +// eslint-disable-next-line import/no-cycle +import { TransformerContext } from '../transformer-context'; +import { ResolverConfig, SyncConfig, SyncConfigLambda } from '../config/transformer-config'; type DeltaSyncConfig = { - DeltaSyncTableName: any; + DeltaSyncTableName: unknown; DeltaSyncTableTTL: number; BaseTableTTL: number; }; -export function createSyncTable(context: TransformerContext) { +/** + * Creates the SyncTable required for the data store + * @param context TransformerContext + */ +export const createSyncTable = (context: TransformerContext): void => { const stack = context.stackManager.getStackFor(SyncResourceIDs.syncTableName); const tableName = context.resourceHelper.generateTableName(SyncResourceIDs.syncTableName); // eslint-disable-next-line no-new - new Table(stack, SyncResourceIDs.syncDataSourceID, { + const table = new Table(stack, SyncResourceIDs.syncDataSourceID, { tableName, partitionKey: { name: SyncResourceIDs.syncPrimaryKey, @@ -37,10 +44,25 @@ export function createSyncTable(context: TransformerContext) { timeToLiveAttribute: '_ttl', }); + // Add the GSI for delta sync table required for the data store. + // This index is used for Custom Primary Key scenarios only. + // AppSync will not populate these fields if the model doesn't contain a custom primary key + table.addGlobalSecondaryIndex({ + indexName: SyncResourceIDs.syncGSIName, + partitionKey: { + name: SyncResourceIDs.syncGSIPartitionKey, + type: AttributeType.STRING, + }, + sortKey: { + name: SyncResourceIDs.syncGSISortKey, + type: AttributeType.STRING, + }, + }); + createSyncIAMRole(context, stack, tableName); -} +}; -function createSyncIAMRole(context: TransformerContext, stack: cdk.Stack, tableName: string) { +const createSyncIAMRole = (context: TransformerContext, stack: cdk.Stack, tableName: string): void => { const role = new iam.Role(stack, SyncResourceIDs.syncIAMRoleName, { roleName: context.resourceHelper.generateIAMRoleName(SyncResourceIDs.syncIAMRoleName), assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), @@ -75,28 +97,35 @@ function createSyncIAMRole(context: TransformerContext, stack: cdk.Stack, tableN ], }), ); -} - -export function syncDataSourceConfig(): DeltaSyncConfig { - return { - DeltaSyncTableName: joinWithEnv('-', [ - SyncResourceIDs.syncTableName, - cdk.Fn.getAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), - ]), - DeltaSyncTableTTL: 30, - BaseTableTTL: 43200, // 30 days - }; -} +}; -export function validateResolverConfigForType(ctx: TransformerSchemaVisitStepContextProvider, typeName: string): void { +/** + * + */ +export const syncDataSourceConfig = (): DeltaSyncConfig => ({ + DeltaSyncTableName: joinWithEnv('-', [ + SyncResourceIDs.syncTableName, + cdk.Fn.getAtt(ResourceConstants.RESOURCES.GraphQLAPILogicalID, 'ApiId'), + ]), + DeltaSyncTableTTL: 30, + BaseTableTTL: 43200, // 30 days +}); + +/** + * + */ +export const validateResolverConfigForType = (ctx: TransformerSchemaVisitStepContextProvider, typeName: string): void => { const resolverConfig = ctx.getResolverConfig(); const typeResolverConfig = resolverConfig?.models?.[typeName]; if (typeResolverConfig && (!typeResolverConfig.ConflictDetection || !typeResolverConfig.ConflictHandler)) { console.warn(`Invalid resolverConfig for type ${typeName}. Using the project resolverConfig instead.`); } -} +}; -export function getSyncConfig(ctx: TransformerTransformSchemaStepContextProvider, typeName: string): SyncConfig | undefined { +/** + * + */ +export const getSyncConfig = (ctx: TransformerTransformSchemaStepContextProvider, typeName: string): SyncConfig | undefined => { let syncConfig: SyncConfig | undefined; const resolverConfig = ctx.getResolverConfig(); @@ -114,15 +143,19 @@ export function getSyncConfig(ctx: TransformerTransformSchemaStepContextProvider } return syncConfig; -} +}; -export function isLambdaSyncConfig(syncConfig: SyncConfig): syncConfig is SyncConfigLambda { +/** + * + */ +export const isLambdaSyncConfig = (syncConfig: SyncConfig): syncConfig is SyncConfigLambda => { const lambdaConfigKey: keyof SyncConfigLambda = 'LambdaConflictHandler'; if (syncConfig && syncConfig.ConflictHandler === 'LAMBDA') { + // eslint-disable-next-line no-prototype-builtins if (syncConfig.hasOwnProperty(lambdaConfigKey)) { return true; } - throw Error(`Invalid Lambda SyncConfig`); + throw Error('Invalid Lambda SyncConfig'); } return false; } @@ -151,26 +184,18 @@ function syncLambdaArnResource(stackManager: StackManagerProvider, name: string, cdk.Fn.sub(lambdaArnKey(name, region), substitutions), cdk.Fn.sub(lambdaArnKey(removeEnvReference(name), region), {}), ).toString(); -} +}; -function referencesEnv(value: string): boolean { - return value.match(/(\${env})/) !== null; -} +const referencesEnv = (value: string): boolean => value.match(/(\${env})/) !== null; -function lambdaArnKey(name: string, region?: string): string { - return region - ? `arn:aws:lambda:${region}:\${AWS::AccountId}:function:${name}` - : `arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${name}`; -} +const lambdaArnKey = (name: string, region?: string): string => (region + ? `arn:aws:lambda:${region}:\${AWS::AccountId}:function:${name}` + : `arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${name}`); -function removeEnvReference(value: string): string { - return value.replace(/(-\${env})/, ''); -} +const removeEnvReference = (value: string): string => value.replace(/(-\${env})/, ''); -function joinWithEnv(separator: string, listToJoin: any[]) { - return cdk.Fn.conditionIf( - ResourceConstants.CONDITIONS.HasEnvironmentParameter, - cdk.Fn.join(separator, [...listToJoin, cdk.Fn.ref(ResourceConstants.PARAMETERS.Env)]), - cdk.Fn.join(separator, listToJoin), - ); -} +const joinWithEnv = (separator: string, listToJoin: any[]): cdk.ICfnRuleConditionExpression => cdk.Fn.conditionIf( + ResourceConstants.CONDITIONS.HasEnvironmentParameter, + cdk.Fn.join(separator, [...listToJoin, cdk.Fn.ref(ResourceConstants.PARAMETERS.Env)]), + cdk.Fn.join(separator, listToJoin), +); diff --git a/packages/graphql-transformer-common/src/ResourceConstants.ts b/packages/graphql-transformer-common/src/ResourceConstants.ts index 48b9d1a494..7384898e79 100644 --- a/packages/graphql-transformer-common/src/ResourceConstants.ts +++ b/packages/graphql-transformer-common/src/ResourceConstants.ts @@ -1,3 +1,6 @@ +/** + * List of Constants + */ export class ResourceConstants { public static NONE = 'NONE'; @@ -35,6 +38,7 @@ export class ResourceConstants { AuthCognitoUserPoolNativeClientLogicalID: 'AuthCognitoUserPoolNativeClient', AuthCognitoUserPoolJSClientLogicalID: 'AuthCognitoUserPoolJSClient', }; + public static PARAMETERS = { // cli Env: 'env', @@ -85,6 +89,7 @@ export class ResourceConstants { // Auth AuthCognitoUserPoolId: 'AuthCognitoUserPoolId', }; + public static MAPPINGS = {}; public static CONDITIONS = { // Environment @@ -99,6 +104,7 @@ export class ResourceConstants { ShouldCreateAPIKey: 'ShouldCreateAPIKey', APIKeyExpirationEpochIsPositive: 'APIKeyExpirationEpochIsPositive', }; + public static OUTPUTS = { // AppSync GraphQLAPIEndpointOutput: 'GraphQLAPIEndpointOutput', @@ -122,6 +128,7 @@ export class ResourceConstants { AuthCognitoUserPoolNativeClientOutput: 'AuthCognitoUserPoolNativeClientId', AuthCognitoUserPoolJSClientOutput: 'AuthCognitoUserPoolJSClientId', }; + public static METADATA = {}; public static readonly SNIPPETS = { diff --git a/packages/graphql-transformer-common/src/SyncResourceIDs.ts b/packages/graphql-transformer-common/src/SyncResourceIDs.ts index 37f4716641..9e92b5bf76 100644 --- a/packages/graphql-transformer-common/src/SyncResourceIDs.ts +++ b/packages/graphql-transformer-common/src/SyncResourceIDs.ts @@ -1,13 +1,23 @@ import { simplifyName } from './util'; +/** + * Constants for the DataStore resource + */ export class SyncResourceIDs { - public static syncDataSourceID: string = 'DataStore'; - public static syncTableName: string = 'AmplifyDataStore'; - public static syncPrimaryKey: string = 'ds_pk'; - public static syncRangeKey: string = 'ds_sk'; - public static syncIAMRoleID: string = 'DataStoreIAMRole' - public static syncIAMRoleName: string = 'AmplifyDataStoreIAMRole'; - public static syncFunctionRoleName: string = 'DataStoreLambdaRole'; + public static syncDataSourceID = 'DataStore'; + public static syncTableName = 'AmplifyDataStore'; + public static syncPrimaryKey = 'ds_pk'; + public static syncRangeKey = 'ds_sk'; + public static syncGSIName = 'deltaSyncGSI'; + public static syncGSIPartitionKey = 'gsi_ds_pk'; + public static syncGSISortKey = 'gsi_ds_sk'; + public static syncIAMRoleID = 'DataStoreIAMRole' + public static syncIAMRoleName = 'AmplifyDataStoreIAMRole'; + public static syncFunctionRoleName = 'DataStoreLambdaRole'; + + /** + * Returns the syncFunctionID + */ public static syncFunctionID(name: string, region?: string): string { return `${simplifyName(name)}${simplifyName(region || '')}Role`; }