1
import { ModelInstanceCreator } from '../datastore/datastore';
2 1
import {
3
	AuthorizationRule,
4
	GraphQLCondition,
5
	GraphQLFilter,
6
	GraphQLField,
7
	isEnumFieldType,
8
	isGraphQLScalarType,
9
	isPredicateObj,
10
	isSchemaModel,
11
	isTargetNameAssociation,
12
	isNonModelFieldType,
13
	ModelFields,
14
	ModelInstanceMetadata,
15
	OpType,
16
	PersistentModel,
17
	PersistentModelConstructor,
18
	PredicatesGroup,
19
	RelationshipType,
20
	SchemaModel,
21
	SchemaNamespace,
22
	SchemaNonModel,
23
} from '../types';
24 1
import { exhaustiveCheck } from '../util';
25
import { MutationEvent } from './';
26

27 1
enum GraphQLOperationType {
28 1
	LIST = 'query',
29 1
	CREATE = 'mutation',
30 1
	UPDATE = 'mutation',
31 1
	DELETE = 'mutation',
32 1
	GET = 'query',
33
}
34

35 1
export enum TransformerMutationType {
36 1
	CREATE = 'Create',
37 1
	UPDATE = 'Update',
38 1
	DELETE = 'Delete',
39 1
	GET = 'Get',
40
}
41

42 1
const dummyMetadata: Omit<ModelInstanceMetadata, 'id'> = {
43
	_version: undefined,
44
	_lastChangedAt: undefined,
45
	_deleted: undefined,
46
};
47

48
const metadataFields = <(keyof ModelInstanceMetadata)[]>(
49 1
	Object.keys(dummyMetadata)
50
);
51 1
export function getMetadataFields(): ReadonlyArray<string> {
52 1
	return metadataFields;
53
}
54

55 1
export function generateSelectionSet(
56
	namespace: SchemaNamespace,
57
	modelDefinition: SchemaModel | SchemaNonModel
58
): string {
59 1
	const scalarFields = getScalarFields(modelDefinition);
60 1
	const nonModelFields = getNonModelFields(namespace, modelDefinition);
61 1
	const implicitOwnerField = getImplicitOwnerField(
62
		modelDefinition,
63
		scalarFields
64
	);
65

66 1
	let scalarAndMetadataFields = Object.values(scalarFields)
67 1
		.map(({ name }) => name)
68
		.concat(implicitOwnerField)
69
		.concat(nonModelFields);
70

71 1
	if (isSchemaModel(modelDefinition)) {
72 1
		scalarAndMetadataFields = scalarAndMetadataFields
73
			.concat(getMetadataFields())
74
			.concat(getConnectionFields(modelDefinition));
75
	}
76

77 1
	const result = scalarAndMetadataFields.join('\n');
78

79 1
	return result;
80
}
81

82
function getImplicitOwnerField(
83
	modelDefinition: SchemaModel | SchemaNonModel,
84
	scalarFields: ModelFields
85
) {
86 1
	if (!scalarFields.owner && isOwnerBasedModel(modelDefinition)) {
87 1
		return ['owner'];
88
	}
89 1
	return [];
90
}
91

92
function isOwnerBasedModel(modelDefinition: SchemaModel | SchemaNonModel) {
93 1
	return (
94 1
		isSchemaModel(modelDefinition) &&
95
		modelDefinition.attributes &&
96
		modelDefinition.attributes.some(
97
			attr =>
98 1
				attr.properties &&
99
				attr.properties.rules &&
100 1
				attr.properties.rules.some(rule => rule.allow === 'owner')
101
		)
102
	);
103
}
104

105
function getScalarFields(
106
	modelDefinition: SchemaModel | SchemaNonModel
107
): ModelFields {
108 1
	const { fields } = modelDefinition;
109

110 1
	const result = Object.values(fields)
111
		.filter(field => {
112 1
			if (isGraphQLScalarType(field.type) || isEnumFieldType(field.type)) {
113 1
				return true;
114
			}
115

116 1
			return false;
117
		})
118
		.reduce((acc, field) => {
119 1
			acc[field.name] = field;
120

121 1
			return acc;
122
		}, {} as ModelFields);
123

124 1
	return result;
125
}
126

127
function getConnectionFields(modelDefinition: SchemaModel): string[] {
128 1
	const result = [];
129

130 1
	Object.values(modelDefinition.fields)
131 1
		.filter(({ association }) => association && Object.keys(association).length)
132 1
		.forEach(({ name, association }) => {
133 1
			const { connectionType } = association;
134

135 1
			switch (connectionType) {
136 1
				case 'HAS_ONE':
137
				case 'HAS_MANY':
138
					// Intentionally blank
139 1
					break;
140
				case 'BELONGS_TO':
141 1
					if (isTargetNameAssociation(association)) {
142 1
						result.push(`${name} { id _deleted }`);
143
					}
144 1
					break;
145
				default:
146 0
					exhaustiveCheck(connectionType);
147
			}
148
		});
149

150 1
	return result;
151
}
152

153
function getNonModelFields(
154
	namespace: SchemaNamespace,
155
	modelDefinition: SchemaModel | SchemaNonModel
156
): string[] {
157 1
	const result = [];
158

159 1
	Object.values(modelDefinition.fields).forEach(({ name, type }) => {
160 1
		if (isNonModelFieldType(type)) {
161 1
			const typeDefinition = namespace.nonModels![type.nonModel];
162 1
			const scalarFields = Object.values(getScalarFields(typeDefinition)).map(
163 1
				({ name }) => name
164
			);
165

166 1
			const nested = [];
167 1
			Object.values(typeDefinition.fields).forEach(field => {
168 1
				const { type, name } = field;
169

170 1
				if (isNonModelFieldType(type)) {
171 1
					const typeDefinition = namespace.nonModels![type.nonModel];
172

173 1
					nested.push(
174
						`${name} { ${generateSelectionSet(namespace, typeDefinition)} }`
175
					);
176
				}
177
			});
178

179 1
			result.push(`${name} { ${scalarFields.join(' ')} ${nested.join(' ')} }`);
180
		}
181
	});
182

183 1
	return result;
184
}
185

186 1
export function getAuthorizationRules(
187
	modelDefinition: SchemaModel
188
): AuthorizationRule[] {
189
	// Searching for owner authorization on attributes
190 1
	const authConfig = []
191
		.concat(modelDefinition.attributes)
192 1
		.find(attr => attr && attr.type === 'auth');
193

194 1
	const { properties: { rules = [] } = {} } = authConfig || {};
195

196 1
	const resultRules: AuthorizationRule[] = [];
197
	// Multiple rules can be declared for allow: owner
198 1
	rules.forEach(rule => {
199
		// setting defaults for backwards compatibility with old cli
200
		const {
201 1
			identityClaim = 'cognito:username',
202 1
			ownerField = 'owner',
203 1
			operations = ['create', 'update', 'delete', 'read'],
204 1
			provider = 'userPools',
205 1
			groupClaim = 'cognito:groups',
206 1
			allow: authStrategy = 'iam',
207 1
			groups = [],
208
		} = rule;
209

210 1
		const isReadAuthorized = operations.includes('read');
211 1
		const isOwnerAuth = authStrategy === 'owner';
212

213 1
		if (!isReadAuthorized && !isOwnerAuth) {
214 0
			return;
215
		}
216

217 1
		const authRule: AuthorizationRule = {
218
			identityClaim,
219
			ownerField,
220
			provider,
221
			groupClaim,
222
			authStrategy,
223
			groups,
224
			areSubscriptionsPublic: false,
225
		};
226

227 1
		if (isOwnerAuth) {
228
			// look for the subscription level override
229
			// only pay attention to the public level
230 1
			const modelConfig = (<typeof modelDefinition.attributes>[])
231
				.concat(modelDefinition.attributes)
232 1
				.find(attr => attr && attr.type === 'model');
233

234
			// find the subscriptions level. ON is default
235 1
			const { properties: { subscriptions: { level = 'on' } = {} } = {} } =
236
				modelConfig || {};
237

238
			// treat subscriptions as public for owner auth with unprotected reads
239
			// when `read` is omitted from `operations`
240 1
			authRule.areSubscriptionsPublic =
241 1
				!operations.includes('read') || level === 'public';
242
		}
243

244 1
		if (isOwnerAuth) {
245
			// owner rules has least priority
246 1
			resultRules.push(authRule);
247 1
			return;
248
		}
249

250 1
		resultRules.unshift(authRule);
251
	});
252

253 1
	return resultRules;
254
}
255

256 1
export function buildSubscriptionGraphQLOperation(
257
	namespace: SchemaNamespace,
258
	modelDefinition: SchemaModel,
259
	transformerMutationType: TransformerMutationType,
260
	isOwnerAuthorization: boolean,
261
	ownerField: string
262
): [TransformerMutationType, string, string] {
263 1
	const selectionSet = generateSelectionSet(namespace, modelDefinition);
264

265 1
	const { name: typeName, pluralName: pluralTypeName } = modelDefinition;
266

267 1
	const opName = `on${transformerMutationType}${typeName}`;
268 1
	let docArgs = '';
269 1
	let opArgs = '';
270

271 1
	if (isOwnerAuthorization) {
272 0
		docArgs = `($${ownerField}: String!)`;
273 0
		opArgs = `(${ownerField}: $${ownerField})`;
274
	}
275

276 1
	return [
277
		transformerMutationType,
278
		opName,
279
		`subscription operation${docArgs}{
280
			${opName}${opArgs}{
281
				${selectionSet}
282
			}
283
		}`,
284
	];
285
}
286

287 1
export function buildGraphQLOperation(
288
	namespace: SchemaNamespace,
289
	modelDefinition: SchemaModel,
290
	graphQLOpType: keyof typeof GraphQLOperationType
291
): [TransformerMutationType, string, string][] {
292 1
	let selectionSet = generateSelectionSet(namespace, modelDefinition);
293

294 1
	const { name: typeName, pluralName: pluralTypeName } = modelDefinition;
295

296
	let operation: string;
297 1
	let documentArgs: string = ' ';
298 1
	let operationArgs: string = ' ';
299
	let transformerMutationType: TransformerMutationType;
300

301 1
	switch (graphQLOpType) {
302 1
		case 'LIST':
303 1
			operation = `sync${pluralTypeName}`;
304 1
			documentArgs = `($limit: Int, $nextToken: String, $lastSync: AWSTimestamp, $filter: Model${typeName}FilterInput)`;
305 1
			operationArgs =
306
				'(limit: $limit, nextToken: $nextToken, lastSync: $lastSync, filter: $filter)';
307 1
			selectionSet = `items {
308
							${selectionSet}
309
						}
310
						nextToken
311
						startedAt`;
312 1
			break;
313
		case 'CREATE':
314 1
			operation = `create${typeName}`;
315 1
			documentArgs = `($input: Create${typeName}Input!)`;
316 1
			operationArgs = '(input: $input)';
317 1
			transformerMutationType = TransformerMutationType.CREATE;
318 1
			break;
319
		case 'UPDATE':
320 1
			operation = `update${typeName}`;
321 1
			documentArgs = `($input: Update${typeName}Input!, $condition: Model${typeName}ConditionInput)`;
322 1
			operationArgs = '(input: $input, condition: $condition)';
323 1
			transformerMutationType = TransformerMutationType.UPDATE;
324 1
			break;
325
		case 'DELETE':
326 1
			operation = `delete${typeName}`;
327 1
			documentArgs = `($input: Delete${typeName}Input!, $condition: Model${typeName}ConditionInput)`;
328 1
			operationArgs = '(input: $input, condition: $condition)';
329 1
			transformerMutationType = TransformerMutationType.DELETE;
330 1
			break;
331
		case 'GET':
332 1
			operation = `get${typeName}`;
333 1
			documentArgs = `($id: ID!)`;
334 1
			operationArgs = '(id: $id)';
335 1
			transformerMutationType = TransformerMutationType.GET;
336 1
			break;
337

338
		default:
339 0
			exhaustiveCheck(graphQLOpType);
340
	}
341

342 1
	return [
343
		[
344
			transformerMutationType,
345
			operation,
346
			`${GraphQLOperationType[graphQLOpType]} operation${documentArgs}{
347
		${operation}${operationArgs}{
348
			${selectionSet}
349
		}
350
	}`,
351
		],
352
	];
353
}
354

355 1
export function createMutationInstanceFromModelOperation<
356
	T extends PersistentModel
357
>(
358
	relationships: RelationshipType,
359
	modelDefinition: SchemaModel,
360
	opType: OpType,
361
	model: PersistentModelConstructor<T>,
362
	element: T,
363
	condition: GraphQLCondition,
364
	MutationEventConstructor: PersistentModelConstructor<MutationEvent>,
365
	modelInstanceCreator: ModelInstanceCreator,
366
	id?: string
367
): MutationEvent {
368
	let operation: TransformerMutationType;
369

370 0
	switch (opType) {
371 1
		case OpType.INSERT:
372 0
			operation = TransformerMutationType.CREATE;
373 0
			break;
374
		case OpType.UPDATE:
375 0
			operation = TransformerMutationType.UPDATE;
376 0
			break;
377
		case OpType.DELETE:
378 0
			operation = TransformerMutationType.DELETE;
379 0
			break;
380
		default:
381 0
			exhaustiveCheck(opType);
382
	}
383

384 0
	const mutationEvent = modelInstanceCreator(MutationEventConstructor, {
385 1
		...(id ? { id } : {}),
386
		data: JSON.stringify(element),
387
		modelId: element.id,
388
		model: model.name,
389
		operation,
390
		condition: JSON.stringify(condition),
391
	});
392

393 0
	return mutationEvent;
394
}
395

396 1
export function predicateToGraphQLCondition(
397
	predicate: PredicatesGroup<any>
398
): GraphQLCondition {
399 0
	const result = {};
400

401 1
	if (!predicate || !Array.isArray(predicate.predicates)) {
402 0
		return result;
403
	}
404

405 0
	predicate.predicates.forEach(p => {
406 1
		if (isPredicateObj(p)) {
407 0
			const { field, operator, operand } = p;
408

409 1
			if (field === 'id') {
410 0
				return;
411
			}
412

413 0
			result[field] = { [operator]: operand };
414
		} else {
415 0
			result[p.type] = predicateToGraphQLCondition(p);
416
		}
417
	});
418

419 0
	return result;
420
}
421

422 1
export function predicateToGraphQLFilter(
423
	predicatesGroup: PredicatesGroup<any>
424
): GraphQLFilter {
425 1
	const result: GraphQLFilter = {};
426

427 1
	if (!predicatesGroup || !Array.isArray(predicatesGroup.predicates)) {
428 0
		return result;
429
	}
430

431 1
	const { type, predicates } = predicatesGroup;
432 1
	const isList = type === 'and' || type === 'or';
433

434 1
	result[type] = isList ? [] : {};
435

436 1
	const appendToFilter = value =>
437 1
		isList ? result[type].push(value) : (result[type] = value);
438

439 1
	predicates.forEach(predicate => {
440 1
		if (isPredicateObj(predicate)) {
441 1
			const { field, operator, operand } = predicate;
442

443 1
			const gqlField: GraphQLField = {
444
				[field]: { [operator]: operand },
445
			};
446

447 1
			appendToFilter(gqlField);
448 1
			return;
449
		}
450

451 1
		appendToFilter(predicateToGraphQLFilter(predicate));
452
	});
453

454 1
	return result;
455
}
456

457 1
export function getUserGroupsFromToken(
458
	token: { [field: string]: any },
459
	rule: AuthorizationRule
460
): string[] {
461
	// validate token against groupClaim
462 1
	let userGroups: string[] | string = token[rule.groupClaim] || [];
463

464 1
	if (typeof userGroups === 'string') {
465 1
		let parsedGroups;
466 1
		try {
467 1
			parsedGroups = JSON.parse(userGroups);
468
		} catch (e) {
469 1
			parsedGroups = userGroups;
470
		}
471 1
		userGroups = [].concat(parsedGroups);
472
	}
473

474 1
	return userGroups;
475
}

Read our documentation on viewing source code .

Loading