@aws-amplify/graphql-model-transformer 0.6.4 → 0.6.5-amplify-export2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -881,7 +881,7 @@ describe('ModelTransformer: ', () => {
881
881
  validateModelSchema(parse(out.schema));
882
882
  });
883
883
 
884
- it('should generate sync resolver with ConflictHandlerType.AUTOMERGE', () => {
884
+ it('should generate sync resolver with ConflictHandlerType.Automerge', () => {
885
885
  const validSchema = `
886
886
  type Post @model {
887
887
  id: ID!
@@ -950,7 +950,7 @@ describe('ModelTransformer: ', () => {
950
950
  validateModelSchema(parse(definition));
951
951
  });
952
952
 
953
- it('should generate sync resolver with ConflictHandlerType.OPTIMISTIC', () => {
953
+ it('should generate sync resolver with ConflictHandlerType.Optimistic', () => {
954
954
  const validSchema = `
955
955
  type Post @model {
956
956
  id: ID!
@@ -13,4 +13,4 @@ export const BOOLEAN_FUNCTIONS = new Set<string>(['attributeExists', 'attributeT
13
13
 
14
14
  export const ATTRIBUTE_TYPES = ['binary', 'binarySet', 'bool', 'list', 'map', 'number', 'numberSet', 'string', 'stringSet', '_null'];
15
15
 
16
-
16
+ export const OPERATION_KEY = '__operation';
@@ -54,6 +54,7 @@ import {
54
54
  makeUpdateInputField,
55
55
  } from './graphql-types';
56
56
  import {
57
+ generateAuthExpressionForSandboxMode,
57
58
  generateCreateInitSlotTemplate,
58
59
  generateCreateRequestTemplate,
59
60
  generateDefaultResponseMappingTemplate,
@@ -64,7 +65,12 @@ import {
64
65
  generateUpdateInitSlotTemplate,
65
66
  generateUpdateRequestTemplate,
66
67
  } from './resolvers';
67
- import { generateGetRequestTemplate, generateListRequestTemplate, generateSyncRequestTemplate } from './resolvers/query';
68
+ import {
69
+ generateGetRequestTemplate,
70
+ generateGetResponseTemplate,
71
+ generateListRequestTemplate,
72
+ generateSyncRequestTemplate,
73
+ } from './resolvers/query';
68
74
  import {
69
75
  DirectiveWrapper,
70
76
  FieldWrapper,
@@ -88,17 +94,17 @@ export type ModelDirectiveConfiguration = {
88
94
  list: OptionalAndNullable<string>;
89
95
  sync: OptionalAndNullable<string>;
90
96
  }>;
91
- mutations: {
97
+ mutations: OptionalAndNullable<{
92
98
  create: OptionalAndNullable<string>;
93
99
  update: OptionalAndNullable<string>;
94
100
  delete: OptionalAndNullable<string>;
95
- } | null;
96
- subscriptions: {
101
+ }>;
102
+ subscriptions: OptionalAndNullable<{
97
103
  onCreate: OptionalAndNullable<string>[];
98
104
  onUpdate: OptionalAndNullable<string>[];
99
105
  onDelete: OptionalAndNullable<string>[];
100
- level: Partial<SubscriptionLevel>;
101
- } | null;
106
+ level: SubscriptionLevel;
107
+ }>;
102
108
  timestamps: OptionalAndNullable<{
103
109
  createdAt: OptionalAndNullable<string>;
104
110
  updatedAt: OptionalAndNullable<string>;
@@ -169,12 +175,13 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
169
175
  `'${definition.name.value}' is a reserved type name and currently in use within the default schema element.`,
170
176
  );
171
177
  }
172
-
173
178
  // todo: get model configuration with default values and store it in the map
174
179
  const typeName = definition.name.value;
180
+
175
181
  if (ctx.isProjectUsingDataStore()) {
176
182
  SyncUtils.validateResolverConfigForType(ctx, typeName);
177
183
  }
184
+
178
185
  const directiveWrapped: DirectiveWrapper = new DirectiveWrapper(directive);
179
186
  const options = directiveWrapped.getArguments({
180
187
  queries: {
@@ -188,7 +195,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
188
195
  delete: toCamelCase(['delete', typeName]),
189
196
  },
190
197
  subscriptions: {
191
- level: SubscriptionLevel.public,
198
+ level: SubscriptionLevel.on,
192
199
  onCreate: [this.ensureValidSubscriptionName(toCamelCase(['onCreate', typeName]))],
193
200
  onDelete: [this.ensureValidSubscriptionName(toCamelCase(['onDelete', typeName]))],
194
201
  onUpdate: [this.ensureValidSubscriptionName(toCamelCase(['onUpdate', typeName]))],
@@ -241,8 +248,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
241
248
 
242
249
  generateResolvers = (context: TransformerContextProvider): void => {
243
250
  for (let type of this.typesWithModelDirective) {
244
- const def = context.output.getObject(type);
245
-
251
+ const def = context.output.getObject(type)!;
246
252
  // This name is used by the mock functionality. Changing this can break mock.
247
253
  const tableLogicalName = `${def!.name.value}Table`;
248
254
  const stack = context.stackManager.getStackFor(tableLogicalName, def!.name.value);
@@ -265,7 +271,13 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
265
271
  default:
266
272
  throw new Error('Unknown query field type');
267
273
  }
268
-
274
+ resolver.addToSlot(
275
+ 'postAuth',
276
+ MappingTemplate.s3MappingTemplateFromString(
277
+ generateAuthExpressionForSandboxMode(context),
278
+ `${query.typeName}.${query.fieldName}.{slotName}.{slotIndex}.req.vtl`,
279
+ ),
280
+ );
269
281
  resolver.mapToStack(stack);
270
282
  context.resolvers.addResolver(query.typeName, query.fieldName, resolver);
271
283
  }
@@ -284,11 +296,49 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
284
296
  resolver = this.generateUpdateResolver(context, def!, mutation.typeName, mutation.fieldName);
285
297
  break;
286
298
  default:
287
- throw new Error('Unknown query field type');
299
+ throw new Error('Unknown mutation field type');
288
300
  }
301
+ resolver.addToSlot(
302
+ 'postAuth',
303
+ MappingTemplate.s3MappingTemplateFromString(
304
+ generateAuthExpressionForSandboxMode(context),
305
+ `${mutation.typeName}.${mutation.fieldName}.{slotName}.{slotIndex}.req.vtl`,
306
+ ),
307
+ );
289
308
  resolver.mapToStack(stack);
290
309
  context.resolvers.addResolver(mutation.typeName, mutation.fieldName, resolver);
291
310
  }
311
+
312
+ const subscriptionLevel = this.modelDirectiveConfig.get(def.name.value)?.subscriptions?.level;
313
+ // in order to create subscription resolvers the level needs to be on
314
+ if (subscriptionLevel === SubscriptionLevel.on) {
315
+ const subscriptionFields = this.getSubscriptionFieldNames(context, def!);
316
+ for (let subscription of subscriptionFields.values()) {
317
+ let resolver;
318
+ switch (subscription.type) {
319
+ case SubscriptionFieldType.ON_CREATE:
320
+ resolver = this.generateOnCreateResolver(context, def, subscription.typeName, subscription.fieldName);
321
+ break;
322
+ case SubscriptionFieldType.ON_UPDATE:
323
+ resolver = this.generateOnUpdateResolver(context, def, subscription.typeName, subscription.fieldName);
324
+ break;
325
+ case SubscriptionFieldType.ON_DELETE:
326
+ resolver = this.generateOnDeleteResolver(context, def, subscription.typeName, subscription.fieldName);
327
+ break;
328
+ default:
329
+ throw new Error('Unknown subscription field type');
330
+ }
331
+ resolver.addToSlot(
332
+ 'postAuth',
333
+ MappingTemplate.s3MappingTemplateFromString(
334
+ generateAuthExpressionForSandboxMode(context),
335
+ `${subscription.typeName}.${subscription.fieldName}.{slotName}.{slotIndex}.req.vtl`,
336
+ ),
337
+ );
338
+ resolver.mapToStack(stack);
339
+ context.resolvers.addResolver(subscription.typeName, subscription.fieldName, resolver);
340
+ }
341
+ }
292
342
  }
293
343
  };
294
344
 
@@ -307,10 +357,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
307
357
  fieldName,
308
358
  dataSource,
309
359
  MappingTemplate.s3MappingTemplateFromString(generateGetRequestTemplate(), `${typeName}.${fieldName}.req.vtl`),
310
- MappingTemplate.s3MappingTemplateFromString(
311
- generateDefaultResponseMappingTemplate(isSyncEnabled),
312
- `${typeName}.${fieldName}.res.vtl`,
313
- ),
360
+ MappingTemplate.s3MappingTemplateFromString(generateGetResponseTemplate(isSyncEnabled), `${typeName}.${fieldName}.res.vtl`),
314
361
  );
315
362
  }
316
363
  return this.resolverMap[resolverKey];
@@ -359,7 +406,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
359
406
  `${typeName}.${fieldName}.req.vtl`,
360
407
  ),
361
408
  MappingTemplate.s3MappingTemplateFromString(
362
- generateDefaultResponseMappingTemplate(isSyncEnabled),
409
+ generateDefaultResponseMappingTemplate(isSyncEnabled, true),
363
410
  `${typeName}.${fieldName}.res.vtl`,
364
411
  ),
365
412
  );
@@ -391,7 +438,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
391
438
  dataSource,
392
439
  MappingTemplate.s3MappingTemplateFromString(generateDeleteRequestTemplate(isSyncEnabled), `${typeName}.${fieldName}.req.vtl`),
393
440
  MappingTemplate.s3MappingTemplateFromString(
394
- generateDefaultResponseMappingTemplate(isSyncEnabled),
441
+ generateDefaultResponseMappingTemplate(isSyncEnabled, true),
395
442
  `${typeName}.${fieldName}.res.vtl`,
396
443
  ),
397
444
  );
@@ -701,7 +748,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
701
748
  dataSource,
702
749
  MappingTemplate.s3MappingTemplateFromString(generateCreateRequestTemplate(type.name.value), `${typeName}.${fieldName}.req.vtl`),
703
750
  MappingTemplate.s3MappingTemplateFromString(
704
- generateDefaultResponseMappingTemplate(isSyncEnabled),
751
+ generateDefaultResponseMappingTemplate(isSyncEnabled, true),
705
752
  `${typeName}.${fieldName}.res.vtl`,
706
753
  ),
707
754
  );
@@ -1164,7 +1211,6 @@ export class ModelTransformer extends TransformerModelBase implements Transforme
1164
1211
  SyncUtils.createSyncLambdaIAMPolicy(stack, syncConfig.LambdaConflictHandler.name, syncConfig.LambdaConflictHandler.region),
1165
1212
  );
1166
1213
  }
1167
-
1168
1214
  return role;
1169
1215
  }
1170
1216
 
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { ModelTransformer } from './graphql-model-transformer';
1
+ export { ModelTransformer, ModelDirectiveConfiguration, SubscriptionLevel } from './graphql-model-transformer';
2
+ export { OPERATION_KEY } from './definitions';
2
3
  export * from './graphql-types';
3
4
  export * from './resolvers';
@@ -15,7 +15,13 @@ import {
15
15
  ifElse,
16
16
  printBlock,
17
17
  toJson,
18
+ qref,
19
+ str,
20
+ not,
18
21
  } from 'graphql-mapping-template';
22
+ import { OPERATION_KEY } from '../definitions';
23
+
24
+ const API_KEY = 'API Key Authorization';
19
25
 
20
26
  /**
21
27
  * Helper method to generate code that converts DynamoDB condition object to condition
@@ -57,9 +63,11 @@ export const generateConditionSlot = (inputConditionObjectName: string, conditio
57
63
 
58
64
  /**
59
65
  * Generate common response template used by most of the resolvers.
66
+ * Append operation if response is coming from a mutation, this is to protect field resolver for subscriptions
60
67
  */
61
- export const generateDefaultResponseMappingTemplate = (isSyncEnabled: boolean): string => {
68
+ export const generateDefaultResponseMappingTemplate = (isSyncEnabled: boolean, mutation = false): string => {
62
69
  const statements: Expression[] = [];
70
+ if (mutation) statements.push(qref(methodCall(ref('ctx.result.put'), str(OPERATION_KEY), str('Mutation'))));
63
71
  if (isSyncEnabled) {
64
72
  statements.push(
65
73
  ifElse(
@@ -74,14 +82,30 @@ export const generateDefaultResponseMappingTemplate = (isSyncEnabled: boolean):
74
82
  );
75
83
  }
76
84
 
77
- return printBlock('Get ResponseTemplate')(compoundExpression(statements));
85
+ return printBlock('ResponseTemplate')(compoundExpression(statements));
78
86
  };
79
87
 
80
88
  /**
81
- * Util function to gernate resolver key used to keep track of all the resolvers in memory
89
+ * Util function to generate resolver key used to keep track of all the resolvers in memory
82
90
  * @param typeName Name of the type
83
91
  * @param fieldName Name of the field
84
92
  */
85
93
  export const generateResolverKey = (typeName: string, fieldName: string): string => {
86
94
  return `${typeName}.${fieldName}`;
87
95
  };
96
+
97
+ /**
98
+ * Util function to generate sandbox mode expression
99
+ * @param ctx context to get sandbox mode
100
+ */
101
+ export const generateAuthExpressionForSandboxMode = (ctx: any): string => {
102
+ const enabled = ctx.resourceHelper.api.sandboxModeEnabled;
103
+ let exp;
104
+
105
+ if (enabled) exp = iff(notEquals(methodCall(ref('util.authType')), str(API_KEY)), methodCall(ref('util.unauthorized')));
106
+ else exp = methodCall(ref('util.unauthorized'));
107
+
108
+ return printBlock(`Sandbox Mode ${enabled ? 'Enabled' : 'Disabled'}`)(
109
+ compoundExpression([iff(not(ref('ctx.stash.get("hasAuth")')), exp), toJson(obj({}))]),
110
+ );
111
+ };
@@ -152,8 +152,6 @@ export const generateUpdateRequestTemplate = (modelName: string, isSyncEnabled:
152
152
  */
153
153
  export const generateCreateRequestTemplate = (modelName: string): string => {
154
154
  const statements: Expression[] = [
155
- // set key the condition
156
- ...generateKeyConditionTemplate(false),
157
155
  // Generate conditions
158
156
  comment('Set the default values to put request'),
159
157
  set(ref('mergedValues'), methodCall(ref('util.defaultIfNull'), ref('ctx.stash.defaultValues'), obj({}))),
@@ -174,7 +172,6 @@ export const generateCreateRequestTemplate = (modelName: string): string => {
174
172
  ),
175
173
 
176
174
  // add conditions
177
-
178
175
  iff(ref('context.args.condition'), qref(methodCall(ref('ctx.stash.conditions.add'), ref('context.args.condition')))),
179
176
  // key conditions
180
177
  ...generateKeyConditionTemplate(false),
@@ -16,27 +16,86 @@ import {
16
16
  equals,
17
17
  bool,
18
18
  and,
19
+ isNullOrEmpty,
20
+ list,
21
+ forEach,
19
22
  nul,
20
23
  } from 'graphql-mapping-template';
21
24
  import { ResourceConstants } from 'graphql-transformer-common';
25
+ const authFilter = ref('ctx.stash.authFilter');
26
+
22
27
  /**
23
28
  * Generate get query resolver template
24
29
  */
25
30
  export const generateGetRequestTemplate = (): string => {
26
31
  const statements: Expression[] = [
27
- set(ref('GetRequest'), obj({ version: str('2018-05-29'), operation: str('GetItem') })),
32
+ set(ref('GetRequest'), obj({ version: str('2018-05-29'), operation: str('Query') })),
28
33
  ifElse(
29
34
  ref('ctx.stash.metadata.modelObjectKey'),
30
- set(ref('key'), ref('ctx.stash.metadata.modelObjectKey')),
31
- compoundExpression([set(ref('key'), obj({ id: methodCall(ref('util.dynamodb.toDynamoDB'), ref('ctx.args.id')) }))]),
35
+ compoundExpression([
36
+ set(ref('expression'), str('')),
37
+ set(ref('expressionNames'), obj({})),
38
+ set(ref('expressionValues'), obj({})),
39
+ forEach(ref('item'), ref('ctx.stash.metadata.modelObjectKey.entrySet()'), [
40
+ set(ref('expression'), str('$expression#keyCount$velocityCount = :valueCount$velocityCount AND ')),
41
+ qref(methodCall(ref('expressionNames.put'), str('#keyCount$velocityCount'), ref('item.key'))),
42
+ qref(methodCall(ref('expressionValues.put'), str(':valueCount$velocityCount'), ref('item.value'))),
43
+ ]),
44
+ set(ref('expression'), methodCall(ref('expression.replaceAll'), str('AND $'), str(''))),
45
+ set(
46
+ ref('query'),
47
+ obj({ expression: ref('expression'), expressionNames: ref('expressionNames'), expressionValues: ref('expressionValues') }),
48
+ ),
49
+ ]),
50
+ set(
51
+ ref('query'),
52
+ obj({
53
+ expression: str('id = :id'),
54
+ expressionValues: obj({
55
+ ':id': methodCall(ref('util.parseJson'), methodCall(ref('util.dynamodb.toDynamoDBJson'), ref('ctx.args.id'))),
56
+ }),
57
+ }),
58
+ ),
59
+ ),
60
+ qref(methodCall(ref('GetRequest.put'), str('query'), ref('query'))),
61
+ iff(
62
+ not(isNullOrEmpty(authFilter)),
63
+ qref(
64
+ methodCall(
65
+ ref('GetRequest.put'),
66
+ str('filter'),
67
+ methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), authFilter)),
68
+ ),
69
+ ),
32
70
  ),
33
- qref(methodCall(ref('GetRequest.put'), str('key'), ref('key'))),
34
71
  toJson(ref('GetRequest')),
35
72
  ];
36
73
 
37
74
  return printBlock('Get Request template')(compoundExpression(statements));
38
75
  };
39
76
 
77
+ export const generateGetResponseTemplate = (isSyncEnabled: boolean): string => {
78
+ const statements = new Array<Expression>();
79
+ if (isSyncEnabled) {
80
+ statements.push(
81
+ iff(ref('ctx.error'), methodCall(ref('util.error'), ref('ctx.error.message'), ref('ctx.error.type'), ref('ctx.result'))),
82
+ );
83
+ } else {
84
+ statements.push(iff(ref('ctx.error'), methodCall(ref('util.error'), ref('ctx.error.message'), ref('ctx.error.type'))));
85
+ }
86
+ statements.push(
87
+ ifElse(
88
+ and([not(ref('ctx.result.items.isEmpty()')), equals(ref('ctx.result.scannedCount'), int(1))]),
89
+ toJson(ref('ctx.result.items[0]')),
90
+ compoundExpression([
91
+ iff(and([ref('ctx.result.items.isEmpty()'), equals(ref('ctx.result.scannedCount'), int(1))]), ref('util.unauthorized()')),
92
+ toJson(nul()),
93
+ ]),
94
+ ),
95
+ );
96
+ return printBlock('Get Response template')(compoundExpression(statements));
97
+ };
98
+
40
99
  export const generateListRequestTemplate = (): string => {
41
100
  const requestVariable = 'ListRequest';
42
101
  const modelQueryObj = 'ctx.stash.modelQueryExpression';
@@ -51,12 +110,20 @@ export const generateListRequestTemplate = (): string => {
51
110
  }),
52
111
  ),
53
112
  iff(ref('context.args.nextToken'), set(ref(`${requestVariable}.nextToken`), ref('context.args.nextToken'))),
113
+ ifElse(
114
+ not(isNullOrEmpty(authFilter)),
115
+ compoundExpression([
116
+ set(ref('filter'), authFilter),
117
+ iff(not(isNullOrEmpty(ref('ctx.args.filter'))), set(ref('filter'), obj({ and: list([ref('filter'), ref('ctx.args.filter')]) }))),
118
+ ]),
119
+ iff(not(isNullOrEmpty(ref('ctx.args.filter'))), set(ref('filter'), ref('ctx.args.filter'))),
120
+ ),
54
121
  iff(
55
- ref('context.args.filter'),
122
+ not(isNullOrEmpty(ref('filter'))),
56
123
  compoundExpression([
57
124
  set(
58
125
  ref(`filterExpression`),
59
- methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('ctx.args.filter'))),
126
+ methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('filter'))),
60
127
  ),
61
128
  iff(
62
129
  not(methodCall(ref('util.isNullOrBlank'), ref('filterExpression.expression'))),
@@ -95,10 +162,37 @@ export const generateListRequestTemplate = (): string => {
95
162
  export const generateSyncRequestTemplate = (): string => {
96
163
  return printBlock('Sync Request template')(
97
164
  compoundExpression([
165
+ ifElse(
166
+ not(isNullOrEmpty(authFilter)),
167
+ compoundExpression([
168
+ set(ref('filter'), authFilter),
169
+ iff(not(isNullOrEmpty(ref('ctx.args.filter'))), set(ref('filter'), obj({ and: list([ref('filter'), ref('ctx.args.filter')]) }))),
170
+ ]),
171
+ iff(not(isNullOrEmpty(ref('ctx.args.filter'))), set(ref('filter'), ref('ctx.args.filter'))),
172
+ ),
173
+ iff(
174
+ not(isNullOrEmpty(ref('filter'))),
175
+ compoundExpression([
176
+ set(
177
+ ref(`filterExpression`),
178
+ methodCall(ref('util.parseJson'), methodCall(ref('util.transform.toDynamoDBFilterExpression'), ref('filter'))),
179
+ ),
180
+ iff(
181
+ not(methodCall(ref('util.isNullOrBlank'), ref('filterExpression.expression'))),
182
+ compoundExpression([
183
+ iff(
184
+ equals(methodCall(ref('filterEpression.expressionValues.size')), int(0)),
185
+ qref(methodCall(ref('filterEpression.remove'), str('expressionValues'))),
186
+ ),
187
+ set(ref('filter'), ref('filterExpression')),
188
+ ]),
189
+ ),
190
+ ]),
191
+ ),
98
192
  obj({
99
193
  version: str('2018-05-29'),
100
194
  operation: str('Sync'),
101
- filter: ifElse(ref('context.args.filter'), ref('util.transform.toDynamoDBFilterExpression($ctx.args.filter)'), nul()),
195
+ filter: ifElse(ref('filter'), ref('util.toJson($filter)'), nul()),
102
196
  limit: ref(`util.defaultIfNull($ctx.args.limit, ${ResourceConstants.DEFAULT_SYNC_QUERY_PAGE_LIMIT})`),
103
197
  lastSync: ref('util.toJson($util.defaultIfNull($ctx.args.lastSync, null))'),
104
198
  nextToken: ref('util.toJson($util.defaultIfNull($ctx.args.nextToken, null))'),