1 1
import { Amplify, ConsoleLogger as Logger, Hub, JS } from '@aws-amplify/core';
2 1
import { Draft, immerable, produce, setAutoFreeze } from 'immer';
3 1
import { v4 as uuid4 } from 'uuid';
4 1
import Observable, { ZenObservable } from 'zen-observable-ts';
5 1
import {
6
	isPredicatesAll,
7
	ModelPredicateCreator,
8
	ModelSortPredicateCreator,
9
	PredicateAll,
10
} from '../predicates';
11 1
import { ExclusiveStorage as Storage } from '../storage/storage';
12 1
import { ControlMessage, SyncEngine } from '../sync';
13 1
import {
14
	ConflictHandler,
15
	DataStoreConfig,
16
	GraphQLScalarType,
17
	InternalSchema,
18
	isGraphQLScalarType,
19
	ModelFieldType,
20
	ModelInit,
21
	ModelInstanceMetadata,
22
	ModelPredicate,
23
	SortPredicate,
24
	MutableModel,
25
	NamespaceResolver,
26
	NonModelTypeConstructor,
27
	ProducerPaginationInput,
28
	PaginationInput,
29
	PersistentModel,
30
	PersistentModelConstructor,
31
	ProducerModelPredicate,
32
	Schema,
33
	SchemaModel,
34
	SchemaNamespace,
35
	SchemaNonModel,
36
	SubscriptionMessage,
37
	SyncConflict,
38
	SyncError,
39
	TypeConstructorMap,
40
	ErrorHandler,
41
	SyncExpression,
42
} from '../types';
43 1
import {
44
	DATASTORE,
45
	establishRelation,
46
	exhaustiveCheck,
47
	isModelConstructor,
48
	monotonicUlidFactory,
49
	NAMESPACES,
50
	STORAGE,
51
	SYNC,
52
	USER,
53
	isNullOrUndefined,
54
} from '../util';
55

56 1
setAutoFreeze(true);
57

58 1
const logger = new Logger('DataStore');
59

60 1
const ulid = monotonicUlidFactory(Date.now());
61 1
const { isNode } = JS.browserOrNode();
62

63
declare class Setting {
64
	constructor(init: ModelInit<Setting>);
65
	static copyOf(
66
		src: Setting,
67
		mutator: (draft: MutableModel<Setting>) => void | Setting
68
	): Setting;
69
	public readonly id: string;
70
	public readonly key: string;
71
	public readonly value: string;
72
}
73

74 1
const SETTING_SCHEMA_VERSION = 'schemaVersion';
75

76
let schema: InternalSchema;
77 1
const modelNamespaceMap = new WeakMap<
78
	PersistentModelConstructor<any>,
79
	string
80
>();
81

82 1
const getModelDefinition = (
83
	modelConstructor: PersistentModelConstructor<any>
84
) => {
85 1
	const namespace = modelNamespaceMap.get(modelConstructor);
86

87 1
	return schema.namespaces[namespace].models[modelConstructor.name];
88
};
89

90 1
const isValidModelConstructor = <T extends PersistentModel>(
91
	obj: any
92
): obj is PersistentModelConstructor<T> => {
93 1
	return isModelConstructor(obj) && modelNamespaceMap.has(obj);
94
};
95

96 1
const namespaceResolver: NamespaceResolver = modelConstructor =>
97 1
	modelNamespaceMap.get(modelConstructor);
98

99
let dataStoreClasses: TypeConstructorMap;
100

101
let userClasses: TypeConstructorMap;
102

103
let syncClasses: TypeConstructorMap;
104

105
let storageClasses: TypeConstructorMap;
106

107 1
const initSchema = (userSchema: Schema) => {
108 1
	if (schema !== undefined) {
109 1
		console.warn('The schema has already been initialized');
110

111 1
		return userClasses;
112
	}
113

114 1
	logger.log('validating schema', { schema: userSchema });
115

116 1
	const internalUserNamespace: SchemaNamespace = {
117
		name: USER,
118
		...userSchema,
119
	};
120

121 1
	logger.log('DataStore', 'Init models');
122 1
	userClasses = createTypeClasses(internalUserNamespace);
123 1
	logger.log('DataStore', 'Models initialized');
124

125 1
	const dataStoreNamespace = getNamespace();
126 1
	const storageNamespace = Storage.getNamespace();
127 1
	const syncNamespace = SyncEngine.getNamespace();
128

129 1
	dataStoreClasses = createTypeClasses(dataStoreNamespace);
130 1
	storageClasses = createTypeClasses(storageNamespace);
131 1
	syncClasses = createTypeClasses(syncNamespace);
132

133 1
	schema = {
134
		namespaces: {
135
			[dataStoreNamespace.name]: dataStoreNamespace,
136
			[internalUserNamespace.name]: internalUserNamespace,
137
			[storageNamespace.name]: storageNamespace,
138
			[syncNamespace.name]: syncNamespace,
139
		},
140
		version: userSchema.version,
141
	};
142

143 1
	Object.keys(schema.namespaces).forEach(namespace => {
144 1
		schema.namespaces[namespace].relationships = establishRelation(
145
			schema.namespaces[namespace]
146
		);
147

148 1
		const modelAssociations = new Map<string, string[]>();
149

150 1
		Object.values(schema.namespaces[namespace].models).forEach(model => {
151 1
			const connectedModels: string[] = [];
152

153 1
			Object.values(model.fields)
154
				.filter(
155
					field =>
156 1
						field.association &&
157
						field.association.connectionType === 'BELONGS_TO' &&
158
						(<ModelFieldType>field.type).model !== model.name
159
				)
160
				.forEach(field =>
161 1
					connectedModels.push((<ModelFieldType>field.type).model)
162
				);
163

164 1
			modelAssociations.set(model.name, connectedModels);
165
		});
166

167 1
		const result = new Map<string, string[]>();
168

169 1
		let count = 1000;
170 1
		while (true && count > 0) {
171 1
			if (modelAssociations.size === 0) {
172 1
				break;
173
			}
174 1
			count--;
175 1
			if (count === 0) {
176 0
				throw new Error(
177
					'Models are not topologically sortable. Please verify your schema.'
178
				);
179
			}
180

181 1
			for (const modelName of Array.from(modelAssociations.keys())) {
182 1
				const parents = modelAssociations.get(modelName);
183

184 1
				if (parents.every(x => result.has(x))) {
185 1
					result.set(modelName, parents);
186
				}
187
			}
188

189 1
			Array.from(result.keys()).forEach(x => modelAssociations.delete(x));
190
		}
191

192 1
		schema.namespaces[namespace].modelTopologicalOrdering = result;
193
	});
194

195 1
	return userClasses;
196
};
197

198
const createTypeClasses: (
199
	namespace: SchemaNamespace
200 1
) => TypeConstructorMap = namespace => {
201 1
	const classes: TypeConstructorMap = {};
202

203 1
	Object.entries(namespace.models).forEach(([modelName, modelDefinition]) => {
204 1
		const clazz = createModelClass(modelDefinition);
205 1
		classes[modelName] = clazz;
206

207 1
		modelNamespaceMap.set(clazz, namespace.name);
208
	});
209

210 1
	Object.entries(namespace.nonModels || {}).forEach(
211 1
		([typeName, typeDefinition]) => {
212 1
			const clazz = createNonModelClass(typeDefinition);
213 1
			classes[typeName] = clazz;
214
		}
215
	);
216

217 1
	return classes;
218
};
219

220
export declare type ModelInstanceCreator = typeof modelInstanceCreator;
221

222 1
const instancesMetadata = new WeakSet<
223
	ModelInit<PersistentModel & Partial<ModelInstanceMetadata>>
224
>();
225
function modelInstanceCreator<T extends PersistentModel = PersistentModel>(
226
	modelConstructor: PersistentModelConstructor<T>,
227
	init: ModelInit<T> & Partial<ModelInstanceMetadata>
228
): T {
229 1
	instancesMetadata.add(init);
230

231 1
	return <T>new modelConstructor(init);
232
}
233

234 1
const validateModelFields = (modelDefinition: SchemaModel | SchemaNonModel) => (
235
	k: string,
236
	v: any
237
) => {
238 1
	const fieldDefinition = modelDefinition.fields[k];
239

240 1
	if (fieldDefinition !== undefined) {
241
		const {
242 1
			type,
243 1
			isRequired,
244 1
			isArrayNullable,
245 1
			name,
246 1
			isArray,
247
		} = fieldDefinition;
248

249 1
		if (
250 1
			((!isArray && isRequired) || (isArray && !isArrayNullable)) &&
251
			(v === null || v === undefined)
252
		) {
253 1
			throw new Error(`Field ${name} is required`);
254
		}
255

256 1
		if (isGraphQLScalarType(type)) {
257 1
			const jsType = GraphQLScalarType.getJSType(type);
258 1
			const validateScalar = GraphQLScalarType.getValidationFunction(type);
259

260 1
			if (isArray) {
261 1
				let errorTypeText: string = jsType;
262 1
				if (!isRequired) {
263 1
					errorTypeText = `${jsType} | null | undefined`;
264
				}
265

266 1
				if (!Array.isArray(v) && !isArrayNullable) {
267 0
					throw new Error(
268
						`Field ${name} should be of type [${errorTypeText}], ${typeof v} received. ${v}`
269
					);
270
				}
271

272 1
				if (
273 1
					!isNullOrUndefined(v) &&
274
					(<[]>v).some(
275 1
						e => isNullOrUndefined(e) ? isRequired : typeof e !== jsType
276
					)
277
				) {
278 1
					const elemTypes = (<[]>v).map(e => e === null ? 'null' : typeof e).join(',');
279

280 1
					throw new Error(
281
						`All elements in the ${name} array should be of type ${errorTypeText}, [${elemTypes}] received. ${v}`
282
					);
283
				}
284

285 1
				if (
286 1
					validateScalar &&
287
					!isNullOrUndefined(v)
288
				) {
289 1
					const validationStatus = (<[]>v).map(
290
						e => {
291 1
							if (!isNullOrUndefined(e)) {
292 1
								return validateScalar(e);
293 1
							} else if (isNullOrUndefined(e) && !isRequired) {
294 1
								return true;
295
							} else {
296 0
								return false;
297
							}
298
						}
299
					);
300

301 1
					if (!validationStatus.every(s => s)) {
302 1
						throw new Error(
303
							`All elements in the ${name} array should be of type ${type}, validation failed for one or more elements. ${v}`
304
						);
305
					}
306
				}
307 1
			} else if (!isRequired && v === undefined) {
308 1
				return;
309 1
			} else if (typeof v !== jsType && v !== null) {
310 1
				throw new Error(
311
					`Field ${name} should be of type ${jsType}, ${typeof v} received. ${v}`
312
				);
313 1
			} else if (!isNullOrUndefined(v) && validateScalar && !validateScalar(v)) {
314 1
				throw new Error(
315
					`Field ${name} should be of type ${type}, validation failed. ${v}`
316
				);
317
			}
318
		}
319
	}
320
};
321

322 1
const initializeInstance = <T>(
323
	init: ModelInit<T>,
324
	modelDefinition: SchemaModel | SchemaNonModel,
325
	draft: Draft<T & ModelInstanceMetadata>
326
) => {
327 1
	const modelValidator = validateModelFields(modelDefinition);
328 1
	Object.entries(init).forEach(([k, v]) => {
329 1
		modelValidator(k, v);
330 1
		(<any>draft)[k] = v;
331
	});
332
};
333

334 1
const createModelClass = <T extends PersistentModel>(
335
	modelDefinition: SchemaModel
336
) => {
337 1
	const clazz = <PersistentModelConstructor<T>>(<unknown>class Model {
338
		constructor(init: ModelInit<T>) {
339 1
			const instance = produce(
340
				this,
341
				(draft: Draft<T & ModelInstanceMetadata>) => {
342 1
					initializeInstance(init, modelDefinition, draft);
343

344 1
					const modelInstanceMetadata: ModelInstanceMetadata = instancesMetadata.has(
345
						init
346
					)
347 1
						? <ModelInstanceMetadata>(<unknown>init)
348
						: <ModelInstanceMetadata>{};
349
					const {
350 1
						id: _id,
351 1
						_version,
352 1
						_lastChangedAt,
353 1
						_deleted,
354
					} = modelInstanceMetadata;
355

356
					const id =
357
						// instancesIds is set by modelInstanceCreator, it is accessible only internally
358 1
						_id !== null && _id !== undefined
359 1
							? _id
360
							: modelDefinition.syncable
361 1
							? uuid4()
362
							: ulid();
363

364 1
					draft.id = id;
365

366 1
					if (modelDefinition.syncable) {
367 1
						draft._version = _version;
368 1
						draft._lastChangedAt = _lastChangedAt;
369 1
						draft._deleted = _deleted;
370
					}
371
				}
372
			);
373

374 1
			return instance;
375
		}
376

377 1
		static copyOf(source: T, fn: (draft: MutableModel<T>) => T) {
378 1
			const modelConstructor = Object.getPrototypeOf(source || {}).constructor;
379 1
			if (!isValidModelConstructor(modelConstructor)) {
380 1
				const msg = 'The source object is not a valid model';
381 1
				logger.error(msg, { source });
382 1
				throw new Error(msg);
383
			}
384 1
			return produce(source, draft => {
385 1
				fn(<MutableModel<T>>draft);
386 1
				draft.id = source.id;
387 1
				const modelValidator = validateModelFields(modelDefinition);
388 1
				Object.entries(draft).forEach(([k, v]) => {
389 1
					modelValidator(k, v);
390
				});
391
			});
392
		}
393

394
		// "private" method (that's hidden via `Setting`) for `withSSRContext` to use
395
		// to gain access to `modelInstanceCreator` and `clazz` for persisting IDs from server to client.
396 1
		static fromJSON(json: T | T[]) {
397 1
			if (Array.isArray(json)) {
398 0
				return json.map(init => this.fromJSON(init));
399
			}
400

401 0
			const instance = modelInstanceCreator(clazz, json);
402 0
			const modelValidator = validateModelFields(modelDefinition);
403

404 0
			Object.entries(instance).forEach(([k, v]) => {
405 0
				modelValidator(k, v);
406
			});
407

408 0
			return instance;
409
		}
410 1
	});
411

412 1
	clazz[immerable] = true;
413

414 1
	Object.defineProperty(clazz, 'name', { value: modelDefinition.name });
415

416 1
	return clazz;
417
};
418

419 1
const createNonModelClass = <T>(typeDefinition: SchemaNonModel) => {
420 1
	const clazz = <NonModelTypeConstructor<T>>(<unknown>class Model {
421
		constructor(init: ModelInit<T>) {
422 1
			const instance = produce(
423
				this,
424
				(draft: Draft<T & ModelInstanceMetadata>) => {
425 1
					initializeInstance(init, typeDefinition, draft);
426
				}
427
			);
428

429 1
			return instance;
430
		}
431 1
	});
432

433 1
	clazz[immerable] = true;
434

435 1
	Object.defineProperty(clazz, 'name', { value: typeDefinition.name });
436

437 1
	return clazz;
438
};
439

440
function isQueryOne(obj: any): obj is string {
441 1
	return typeof obj === 'string';
442
}
443

444
function defaultConflictHandler(conflictData: SyncConflict): PersistentModel {
445 0
	const { localModel, modelConstructor, remoteModel } = conflictData;
446 0
	const { _version } = remoteModel;
447 0
	return modelInstanceCreator(modelConstructor, { ...localModel, _version });
448
}
449

450
function defaultErrorHandler(error: SyncError) {
451 0
	logger.warn(error);
452
}
453

454
function getModelConstructorByModelName(
455
	namespaceName: NAMESPACES,
456
	modelName: string
457
): PersistentModelConstructor<any> {
458
	let result: PersistentModelConstructor<any> | NonModelTypeConstructor<any>;
459

460 1
	switch (namespaceName) {
461 1
		case DATASTORE:
462 1
			result = dataStoreClasses[modelName];
463 1
			break;
464
		case USER:
465 1
			result = userClasses[modelName];
466 1
			break;
467
		case SYNC:
468 0
			result = syncClasses[modelName];
469 0
			break;
470
		case STORAGE:
471 0
			result = storageClasses[modelName];
472 0
			break;
473
		default:
474 0
			exhaustiveCheck(namespaceName);
475 0
			break;
476
	}
477

478 1
	if (isValidModelConstructor(result)) {
479 1
		return result;
480
	} else {
481 0
		const msg = `Model name is not valid for namespace. modelName: ${modelName}, namespace: ${namespaceName}`;
482 0
		logger.error(msg);
483

484 0
		throw new Error(msg);
485
	}
486
}
487

488
async function checkSchemaVersion(
489
	storage: Storage,
490
	version: string
491
): Promise<void> {
492 1
	const Setting = dataStoreClasses.Setting as PersistentModelConstructor<
493
		Setting
494
	>;
495

496 1
	const modelDefinition = schema.namespaces[DATASTORE].models.Setting;
497

498 1
	await storage.runExclusive(async s => {
499 1
		const [schemaVersionSetting] = await s.query(
500
			Setting,
501
			ModelPredicateCreator.createFromExisting(modelDefinition, c =>
502
				// @ts-ignore Argument of type '"eq"' is not assignable to parameter of type 'never'.
503 1
				c.key('eq', SETTING_SCHEMA_VERSION)
504
			),
505
			{ page: 0, limit: 1 }
506
		);
507

508 1
		if (schemaVersionSetting !== undefined) {
509 1
			const storedValue = JSON.parse(schemaVersionSetting.value);
510

511 1
			if (storedValue !== version) {
512 0
				await s.clear(false);
513
			}
514
		} else {
515 1
			await s.save(
516
				modelInstanceCreator(Setting, {
517
					key: SETTING_SCHEMA_VERSION,
518
					value: JSON.stringify(version),
519
				})
520
			);
521
		}
522
	});
523
}
524

525
let syncSubscription: ZenObservable.Subscription;
526

527
function getNamespace(): SchemaNamespace {
528 1
	const namespace: SchemaNamespace = {
529
		name: DATASTORE,
530
		relationships: {},
531
		enums: {},
532
		nonModels: {},
533
		models: {
534
			Setting: {
535
				name: 'Setting',
536
				pluralName: 'Settings',
537
				syncable: false,
538
				fields: {
539
					id: {
540
						name: 'id',
541
						type: 'ID',
542
						isRequired: true,
543
						isArray: false,
544
					},
545
					key: {
546
						name: 'key',
547
						type: 'String',
548
						isRequired: true,
549
						isArray: false,
550
					},
551
					value: {
552
						name: 'value',
553
						type: 'String',
554
						isRequired: true,
555
						isArray: false,
556
					},
557
				},
558
			},
559
		},
560
	};
561

562 1
	return namespace;
563
}
564

565 1
class DataStore {
566 1
	private amplifyConfig: Record<string, any> = {};
567
	private conflictHandler: ConflictHandler;
568
	private errorHandler: (error: SyncError) => void;
569
	private fullSyncInterval: number;
570
	private initialized: Promise<void>;
571
	private initReject: Function;
572
	private initResolve: Function;
573
	private maxRecordsToSync: number;
574
	private storage: Storage;
575
	private sync: SyncEngine;
576
	private syncPageSize: number;
577
	private syncExpressions: SyncExpression[];
578 1
	private syncPredicates: WeakMap<
579
		SchemaModel,
580
		ModelPredicate<any>
581
	> = new WeakMap<SchemaModel, ModelPredicate<any>>();
582

583 1
	getModuleName() {
584 1
		return 'DataStore';
585
	}
586

587 1
	start = async (): Promise<void> => {
588 1
		if (this.initialized === undefined) {
589 1
			logger.debug('Starting DataStore');
590 1
			this.initialized = new Promise((res, rej) => {
591 1
				this.initResolve = res;
592 1
				this.initReject = rej;
593
			});
594
		} else {
595 1
			await this.initialized;
596

597 1
			return;
598
		}
599

600 1
		this.storage = new Storage(
601
			schema,
602
			namespaceResolver,
603
			getModelConstructorByModelName,
604
			modelInstanceCreator
605
		);
606

607 1
		await this.storage.init();
608

609 1
		await checkSchemaVersion(this.storage, schema.version);
610

611 1
		const { aws_appsync_graphqlEndpoint } = this.amplifyConfig;
612

613 1
		if (aws_appsync_graphqlEndpoint) {
614 0
			logger.debug('GraphQL endpoint available', aws_appsync_graphqlEndpoint);
615

616 0
			this.syncPredicates = await this.processSyncExpressions();
617

618 0
			this.sync = new SyncEngine(
619
				schema,
620
				namespaceResolver,
621
				syncClasses,
622
				userClasses,
623
				this.storage,
624
				modelInstanceCreator,
625
				this.maxRecordsToSync,
626
				this.syncPageSize,
627
				this.conflictHandler,
628
				this.errorHandler,
629
				this.syncPredicates,
630
				this.amplifyConfig
631
			);
632

633
			// tslint:disable-next-line:max-line-length
634 0
			const fullSyncIntervalInMilliseconds = this.fullSyncInterval * 1000 * 60; // fullSyncInterval from param is in minutes
635 0
			syncSubscription = this.sync
636
				.start({ fullSyncInterval: fullSyncIntervalInMilliseconds })
637
				.subscribe({
638 0
					next: ({ type, data }) => {
639
						// In Node, we need to wait for queries to be synced to prevent returning empty arrays.
640
						// In the Browser, we can begin returning data once subscriptions are in place.
641 0
						const readyType = isNode
642 1
							? ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY
643
							: ControlMessage.SYNC_ENGINE_STORAGE_SUBSCRIBED;
644

645 1
						if (type === readyType) {
646 0
							this.initResolve();
647
						}
648

649 0
						Hub.dispatch('datastore', {
650
							event: type,
651
							data,
652
						});
653
					},
654
					error: err => {
655 0
						logger.warn('Sync error', err);
656 0
						this.initReject();
657
					},
658
				});
659
		} else {
660 1
			logger.warn(
661
				"Data won't be synchronized. No GraphQL endpoint configured. Did you forget `Amplify.configure(awsconfig)`?",
662
				{
663
					config: this.amplifyConfig,
664
				}
665
			);
666

667 1
			this.initResolve();
668
		}
669

670 1
		await this.initialized;
671
	};
672

673 1
	query: {
674
		<T extends PersistentModel>(
675
			modelConstructor: PersistentModelConstructor<T>,
676
			id: string
677
		): Promise<T | undefined>;
678
		<T extends PersistentModel>(
679
			modelConstructor: PersistentModelConstructor<T>,
680
			criteria?: ProducerModelPredicate<T> | typeof PredicateAll,
681
			paginationProducer?: ProducerPaginationInput<T>
682
		): Promise<T[]>;
683
	} = async <T extends PersistentModel>(
684
		modelConstructor: PersistentModelConstructor<T>,
685
		idOrCriteria?: string | ProducerModelPredicate<T> | typeof PredicateAll,
686 1
		paginationProducer?: ProducerPaginationInput<T>
687
	): Promise<T | T[] | undefined> => {
688 1
		await this.start();
689

690
		//#region Input validation
691

692 1
		if (!isValidModelConstructor(modelConstructor)) {
693 1
			const msg = 'Constructor is not for a valid model';
694 1
			logger.error(msg, { modelConstructor });
695

696 1
			throw new Error(msg);
697
		}
698

699 1
		if (typeof idOrCriteria === 'string') {
700 1
			if (paginationProducer !== undefined) {
701 1
				logger.warn('Pagination is ignored when querying by id');
702
			}
703
		}
704

705 1
		const modelDefinition = getModelDefinition(modelConstructor);
706
		let predicate: ModelPredicate<T>;
707

708 1
		if (isQueryOne(idOrCriteria)) {
709 1
			predicate = ModelPredicateCreator.createForId<T>(
710
				modelDefinition,
711
				idOrCriteria
712
			);
713
		} else {
714 1
			if (isPredicatesAll(idOrCriteria)) {
715
				// Predicates.ALL means "all records", so no predicate (undefined)
716 0
				predicate = undefined;
717
			} else {
718 1
				predicate = ModelPredicateCreator.createFromExisting(
719
					modelDefinition,
720
					idOrCriteria
721
				);
722
			}
723
		}
724

725 1
		const pagination = this.processPagination(
726
			modelDefinition,
727
			paginationProducer
728
		);
729

730
		//#endregion
731

732 1
		logger.debug('params ready', {
733
			modelConstructor,
734
			predicate: ModelPredicateCreator.getPredicates(predicate, false),
735
			pagination: {
736
				...pagination,
737
				sort: ModelSortPredicateCreator.getPredicates(pagination.sort, false),
738
			},
739
		});
740

741 1
		const result = await this.storage.query(
742
			modelConstructor,
743
			predicate,
744
			pagination
745
		);
746

747 1
		return isQueryOne(idOrCriteria) ? result[0] : result;
748
	};
749

750 1
	save = async <T extends PersistentModel>(
751
		model: T,
752 1
		condition?: ProducerModelPredicate<T>
753
	): Promise<T> => {
754 1
		await this.start();
755

756 1
		const modelConstructor: PersistentModelConstructor<T> = model
757 1
			? <PersistentModelConstructor<T>>model.constructor
758
			: undefined;
759

760 1
		if (!isValidModelConstructor(modelConstructor)) {
761 1
			const msg = 'Object is not an instance of a valid model';
762 1
			logger.error(msg, { model });
763

764 1
			throw new Error(msg);
765
		}
766

767 1
		const modelDefinition = getModelDefinition(modelConstructor);
768

769 1
		const producedCondition = ModelPredicateCreator.createFromExisting(
770
			modelDefinition,
771
			condition
772
		);
773

774 1
		const [savedModel] = await this.storage.runExclusive(async s => {
775 1
			await s.save(model, producedCondition);
776

777 1
			return s.query(
778
				modelConstructor,
779
				ModelPredicateCreator.createForId(modelDefinition, model.id)
780
			);
781
		});
782

783 1
		return savedModel;
784
	};
785

786 1
	setConflictHandler = (config: DataStoreConfig): ConflictHandler => {
787 1
		const { DataStore: configDataStore } = config;
788

789 1
		const conflictHandlerIsDefault: () => boolean = () =>
790 1
			this.conflictHandler === defaultConflictHandler;
791

792 1
		if (configDataStore) {
793 0
			return configDataStore.conflictHandler;
794
		}
795 1
		if (conflictHandlerIsDefault() && config.conflictHandler) {
796 0
			return config.conflictHandler;
797
		}
798

799 1
		return this.conflictHandler || defaultConflictHandler;
800
	};
801

802 1
	setErrorHandler = (config: DataStoreConfig): ErrorHandler => {
803 1
		const { DataStore: configDataStore } = config;
804

805 1
		const errorHandlerIsDefault: () => boolean = () =>
806 1
			this.errorHandler === defaultErrorHandler;
807

808 1
		if (configDataStore) {
809 0
			return configDataStore.errorHandler;
810
		}
811 1
		if (errorHandlerIsDefault() && config.errorHandler) {
812 0
			return config.errorHandler;
813
		}
814

815 1
		return this.errorHandler || defaultErrorHandler;
816
	};
817

818 1
	delete: {
819
		<T extends PersistentModel>(
820
			model: T,
821
			condition?: ProducerModelPredicate<T>
822
		): Promise<T>;
823
		<T extends PersistentModel>(
824
			modelConstructor: PersistentModelConstructor<T>,
825
			id: string
826
		): Promise<T>;
827
		<T extends PersistentModel>(
828
			modelConstructor: PersistentModelConstructor<T>,
829
			condition: ProducerModelPredicate<T> | typeof PredicateAll
830
		): Promise<T[]>;
831
	} = async <T extends PersistentModel>(
832
		modelOrConstructor: T | PersistentModelConstructor<T>,
833 1
		idOrCriteria?: string | ProducerModelPredicate<T> | typeof PredicateAll
834
	) => {
835 1
		await this.start();
836

837
		let condition: ModelPredicate<T>;
838

839 1
		if (!modelOrConstructor) {
840 1
			const msg = 'Model or Model Constructor required';
841 1
			logger.error(msg, { modelOrConstructor });
842

843 1
			throw new Error(msg);
844
		}
845

846 1
		if (isValidModelConstructor(modelOrConstructor)) {
847 1
			const modelConstructor = modelOrConstructor;
848

849 1
			if (!idOrCriteria) {
850 1
				const msg =
851
					'Id to delete or criteria required. Do you want to delete all? Pass Predicates.ALL';
852 1
				logger.error(msg, { idOrCriteria });
853

854 1
				throw new Error(msg);
855
			}
856

857 1
			if (typeof idOrCriteria === 'string') {
858 1
				condition = ModelPredicateCreator.createForId<T>(
859
					getModelDefinition(modelConstructor),
860
					idOrCriteria
861
				);
862
			} else {
863 1
				condition = ModelPredicateCreator.createFromExisting(
864
					getModelDefinition(modelConstructor),
865
					/**
866
					 * idOrCriteria is always a ProducerModelPredicate<T>, never a symbol.
867
					 * The symbol is used only for typing purposes. e.g. see Predicates.ALL
868
					 */
869
					idOrCriteria as ProducerModelPredicate<T>
870
				);
871

872 1
				if (!condition || !ModelPredicateCreator.isValidPredicate(condition)) {
873 1
					const msg =
874
						'Criteria required. Do you want to delete all? Pass Predicates.ALL';
875 1
					logger.error(msg, { condition });
876

877 1
					throw new Error(msg);
878
				}
879
			}
880

881 1
			const [deleted] = await this.storage.delete(modelConstructor, condition);
882

883 1
			return deleted;
884
		} else {
885 1
			const model = modelOrConstructor;
886 1
			const modelConstructor = Object.getPrototypeOf(model || {})
887
				.constructor as PersistentModelConstructor<T>;
888

889 1
			if (!isValidModelConstructor(modelConstructor)) {
890 1
				const msg = 'Object is not an instance of a valid model';
891 1
				logger.error(msg, { model });
892

893 1
				throw new Error(msg);
894
			}
895

896 1
			const modelDefinition = getModelDefinition(modelConstructor);
897

898 1
			const idPredicate = ModelPredicateCreator.createForId<T>(
899
				modelDefinition,
900
				model.id
901
			);
902

903 1
			if (idOrCriteria) {
904 1
				if (typeof idOrCriteria !== 'function') {
905 1
					const msg = 'Invalid criteria';
906 1
					logger.error(msg, { idOrCriteria });
907

908 1
					throw new Error(msg);
909
				}
910

911 0
				condition = idOrCriteria(idPredicate);
912
			} else {
913 1
				condition = idPredicate;
914
			}
915

916 1
			const [[deleted]] = await this.storage.delete(model, condition);
917

918 1
			return deleted;
919
		}
920
	};
921

922 1
	observe: {
923
		(): Observable<SubscriptionMessage<PersistentModel>>;
924

925
		<T extends PersistentModel>(model: T): Observable<SubscriptionMessage<T>>;
926

927
		<T extends PersistentModel>(
928
			modelConstructor: PersistentModelConstructor<T>,
929
			criteria?: string | ProducerModelPredicate<T>
930
		): Observable<SubscriptionMessage<T>>;
931
	} = <T extends PersistentModel = PersistentModel>(
932
		modelOrConstructor?: T | PersistentModelConstructor<T>,
933
		idOrCriteria?: string | ProducerModelPredicate<T>
934
	): Observable<SubscriptionMessage<T>> => {
935
		let predicate: ModelPredicate<T>;
936

937
		const modelConstructor: PersistentModelConstructor<T> =
938 1
			modelOrConstructor && isValidModelConstructor(modelOrConstructor)
939 1
				? modelOrConstructor
940
				: undefined;
941

942 1
		if (modelOrConstructor && modelConstructor === undefined) {
943 1
			const model = <T>modelOrConstructor;
944
			const modelConstructor =
945 1
				model && (<Object>Object.getPrototypeOf(model)).constructor;
946

947 1
			if (isValidModelConstructor<T>(modelConstructor)) {
948 1
				if (idOrCriteria) {
949 0
					logger.warn('idOrCriteria is ignored when using a model instance', {
950
						model,
951
						idOrCriteria,
952
					});
953
				}
954

955 1
				return this.observe(modelConstructor, model.id);
956
			} else {
957
				const msg =
958 0
					'The model is not an instance of a PersistentModelConstructor';
959 0
				logger.error(msg, { model });
960

961 0
				throw new Error(msg);
962
			}
963
		}
964

965 1
		if (idOrCriteria !== undefined && modelConstructor === undefined) {
966 0
			const msg = 'Cannot provide criteria without a modelConstructor';
967 0
			logger.error(msg, idOrCriteria);
968 0
			throw new Error(msg);
969
		}
970

971 1
		if (modelConstructor && !isValidModelConstructor(modelConstructor)) {
972 0
			const msg = 'Constructor is not for a valid model';
973 0
			logger.error(msg, { modelConstructor });
974

975 0
			throw new Error(msg);
976
		}
977

978 1
		if (typeof idOrCriteria === 'string') {
979 1
			predicate = ModelPredicateCreator.createForId<T>(
980
				getModelDefinition(modelConstructor),
981
				idOrCriteria
982
			);
983
		} else {
984 1
			predicate =
985 1
				modelConstructor &&
986
				ModelPredicateCreator.createFromExisting<T>(
987
					getModelDefinition(modelConstructor),
988
					idOrCriteria
989
				);
990
		}
991

992 1
		return new Observable<SubscriptionMessage<T>>(observer => {
993
			let handle: ZenObservable.Subscription;
994

995 1
			(async () => {
996 1
				await this.start();
997

998 1
				handle = this.storage
999
					.observe(modelConstructor, predicate)
1000 0
					.filter(({ model }) => namespaceResolver(model) === USER)
1001
					.subscribe(observer);
1002
			})();
1003

1004 1
			return () => {
1005 1
				if (handle) {
1006 1
					handle.unsubscribe();
1007
				}
1008
			};
1009
		});
1010
	};
1011

1012 1
	configure = (config: DataStoreConfig = {}) => {
1013
		const {
1014 1
			DataStore: configDataStore,
1015 1
			conflictHandler: configConflictHandler,
1016 1
			errorHandler: configErrorHandler,
1017 1
			maxRecordsToSync: configMaxRecordsToSync,
1018 1
			syncPageSize: configSyncPageSize,
1019 1
			fullSyncInterval: configFullSyncInterval,
1020 1
			syncExpressions: configSyncExpressions,
1021 1
			...configFromAmplify
1022
		} = config;
1023

1024 1
		this.amplifyConfig = { ...configFromAmplify, ...this.amplifyConfig };
1025

1026 1
		this.conflictHandler = this.setConflictHandler(config);
1027 1
		this.errorHandler = this.setErrorHandler(config);
1028

1029 1
		this.syncExpressions =
1030 1
			(configDataStore && configDataStore.syncExpressions) ||
1031
			this.syncExpressions ||
1032
			configSyncExpressions;
1033

1034 1
		this.maxRecordsToSync =
1035 1
			(configDataStore && configDataStore.maxRecordsToSync) ||
1036
			this.maxRecordsToSync ||
1037
			configMaxRecordsToSync;
1038

1039 1
		this.syncPageSize =
1040 1
			(configDataStore && configDataStore.syncPageSize) ||
1041
			this.syncPageSize ||
1042
			configSyncPageSize;
1043

1044 1
		this.fullSyncInterval =
1045 1
			(configDataStore && configDataStore.fullSyncInterval) ||
1046
			this.fullSyncInterval ||
1047
			configFullSyncInterval ||
1048
			24 * 60; // 1 day
1049
	};
1050

1051 1
	clear = async function clear() {
1052 1
		if (this.storage === undefined) {
1053 0
			return;
1054
		}
1055

1056 1
		if (syncSubscription && !syncSubscription.closed) {
1057 0
			syncSubscription.unsubscribe();
1058
		}
1059

1060 1
		await this.storage.clear();
1061

1062 1
		this.initialized = undefined; // Should re-initialize when start() is called.
1063 1
		this.storage = undefined;
1064 1
		this.sync = undefined;
1065 1
		this.syncPredicates = new WeakMap<SchemaModel, ModelPredicate<any>>();
1066
	};
1067

1068 1
	stop = async function stop() {
1069 1
		if (this.initialized !== undefined) {
1070 0
			await this.start();
1071
		}
1072

1073 1
		if (syncSubscription && !syncSubscription.closed) {
1074 0
			syncSubscription.unsubscribe();
1075
		}
1076

1077 0
		this.initialized = undefined; // Should re-initialize when start() is called.
1078 0
		this.sync = undefined;
1079
	};
1080

1081 1
	private processPagination<T extends PersistentModel>(
1082
		modelDefinition: SchemaModel,
1083
		paginationProducer: ProducerPaginationInput<T>
1084
	): PaginationInput<T> {
1085
		let sortPredicate: SortPredicate<T>;
1086 1
		const { limit, page, sort } = paginationProducer || {};
1087

1088 1
		if (page !== undefined && limit === undefined) {
1089 1
			throw new Error('Limit is required when requesting a page');
1090
		}
1091

1092 1
		if (page !== undefined) {
1093 1
			if (typeof page !== 'number') {
1094 1
				throw new Error('Page should be a number');
1095
			}
1096

1097 1
			if (page < 0) {
1098 1
				throw new Error("Page can't be negative");
1099
			}
1100
		}
1101

1102 1
		if (limit !== undefined) {
1103 1
			if (typeof limit !== 'number') {
1104 1
				throw new Error('Limit should be a number');
1105
			}
1106

1107 1
			if (limit < 0) {
1108 1
				throw new Error("Limit can't be negative");
1109
			}
1110
		}
1111

1112 1
		if (sort) {
1113 1
			sortPredicate = ModelSortPredicateCreator.createFromExisting(
1114
				modelDefinition,
1115
				paginationProducer.sort
1116
			);
1117
		}
1118

1119 1
		return {
1120
			limit,
1121
			page,
1122
			sort: sortPredicate,
1123
		};
1124
	}
1125

1126 1
	private async processSyncExpressions(): Promise<
1127
		WeakMap<SchemaModel, ModelPredicate<any>>
1128
	> {
1129 1
		if (!this.syncExpressions || !this.syncExpressions.length) {
1130 0
			return new WeakMap<SchemaModel, ModelPredicate<any>>();
1131
		}
1132

1133 0
		const syncPredicates = await Promise.all(
1134
			this.syncExpressions.map(
1135
				async (
1136 0
					syncExpression: SyncExpression
1137
				): Promise<[SchemaModel, ModelPredicate<any>]> => {
1138 1
					const { modelConstructor, conditionProducer } = await syncExpression;
1139 0
					const modelDefinition = getModelDefinition(modelConstructor);
1140

1141
					// conditionProducer is either a predicate, e.g. (c) => c.field('eq', 1)
1142
					// OR a function/promise that returns a predicate
1143 0
					const condition = await this.unwrapPromise(conditionProducer);
1144 1
					if (isPredicatesAll(condition)) {
1145 0
						return [modelDefinition, null];
1146
					}
1147

1148 0
					const predicate = this.createFromCondition(
1149
						modelDefinition,
1150
						condition
1151
					);
1152

1153 0
					return [modelDefinition, predicate];
1154
				}
1155
			)
1156
		);
1157

1158 0
		return this.weakMapFromEntries(syncPredicates);
1159
	}
1160

1161 1
	private createFromCondition(
1162
		modelDefinition: SchemaModel,
1163
		condition: ProducerModelPredicate<PersistentModel>
1164
	) {
1165 0
		try {
1166 0
			return ModelPredicateCreator.createFromExisting(
1167
				modelDefinition,
1168
				condition
1169
			);
1170
		} catch (error) {
1171 0
			logger.error('Error creating Sync Predicate');
1172 0
			throw error;
1173
		}
1174
	}
1175

1176 1
	private async unwrapPromise<T extends PersistentModel>(
1177
		conditionProducer
1178
	): Promise<ProducerModelPredicate<T>> {
1179
		try {
1180 0
			const condition = await conditionProducer();
1181 0
			return condition;
1182
		} catch (error) {
1183 1
			if (error instanceof TypeError) {
1184 0
				return conditionProducer;
1185
			}
1186 0
			throw error;
1187
		}
1188
	}
1189

1190 1
	private weakMapFromEntries(
1191
		entries: [SchemaModel, ModelPredicate<any>][]
1192
	): WeakMap<SchemaModel, ModelPredicate<any>> {
1193 0
		return entries.reduce((map, [modelDefinition, predicate]) => {
1194 1
			if (map.has(modelDefinition)) {
1195 0
				const { name } = modelDefinition;
1196 0
				logger.warn(
1197
					`You can only utilize one Sync Expression per model.
1198
          Subsequent sync expressions for the ${name} model will be ignored.`
1199
				);
1200 0
				return map;
1201
			}
1202

1203 1
			if (predicate) {
1204 0
				map.set(modelDefinition, predicate);
1205
			}
1206

1207 0
			return map;
1208
		}, new WeakMap<SchemaModel, ModelPredicate<any>>());
1209
	}
1210 1
}
1211

1212 1
const instance = new DataStore();
1213 1
Amplify.register(instance);
1214

1215 1
export { DataStore as DataStoreClass, initSchema, instance as DataStore };

Read our documentation on viewing source code .

Loading