aws-amplify / amplify-js
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
} from '../types';
42 1
import {
43
	DATASTORE,
44
	establishRelation,
45
	exhaustiveCheck,
46
	isModelConstructor,
47
	monotonicUlidFactory,
48
	NAMESPACES,
49
	STORAGE,
50
	SYNC,
51
	USER,
52
	isNullOrUndefined,
53
} from '../util';
54

55 1
setAutoFreeze(true);
56

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

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

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

73 1
const SETTING_SCHEMA_VERSION = 'schemaVersion';
74

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

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

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

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

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

98
let dataStoreClasses: TypeConstructorMap;
99

100
let userClasses: TypeConstructorMap;
101

102
let syncClasses: TypeConstructorMap;
103

104
let storageClasses: TypeConstructorMap;
105

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

110 1
		return userClasses;
111
	}
112

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

194 1
	return userClasses;
195
};
196

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

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

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

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

216 1
	return classes;
217
};
218

219
export declare type ModelInstanceCreator = typeof modelInstanceCreator;
220

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

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

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

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

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

255 1
		if (isGraphQLScalarType(type)) {
256 1
			const jsType = GraphQLScalarType.getJSType(type);
257

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

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

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

278 1
					throw new Error(
279
						`All elements in the ${name} array should be of type ${errorTypeText}, [${elemTypes}] received. ${v}`
280
					);
281
				}
282 1
			} else if (!isRequired && v === undefined) {
283 1
				return;
284 1
			} else if (typeof v !== jsType && v !== null) {
285 1
				throw new Error(
286
					`Field ${name} should be of type ${jsType}, ${typeof v} received. ${v}`
287
				);
288
			}
289
		}
290
	}
291
};
292

293 1
const initializeInstance = <T>(
294
	init: ModelInit<T>,
295
	modelDefinition: SchemaModel | SchemaNonModel,
296
	draft: Draft<T & ModelInstanceMetadata>
297
) => {
298 1
	const modelValidator = validateModelFields(modelDefinition);
299 1
	Object.entries(init).forEach(([k, v]) => {
300 1
		modelValidator(k, v);
301 1
		(<any>draft)[k] = v;
302
	});
303
};
304

305 1
const createModelClass = <T extends PersistentModel>(
306
	modelDefinition: SchemaModel
307
) => {
308 1
	const clazz = <PersistentModelConstructor<T>>(<unknown>class Model {
309
		constructor(init: ModelInit<T>) {
310 1
			const instance = produce(
311
				this,
312
				(draft: Draft<T & ModelInstanceMetadata>) => {
313 1
					initializeInstance(init, modelDefinition, draft);
314

315 1
					const modelInstanceMetadata: ModelInstanceMetadata = instancesMetadata.has(
316
						init
317
					)
318 1
						? <ModelInstanceMetadata>(<unknown>init)
319
						: <ModelInstanceMetadata>{};
320
					const {
321 1
						id: _id,
322 1
						_version,
323 1
						_lastChangedAt,
324 1
						_deleted,
325
					} = modelInstanceMetadata;
326

327
					const id =
328
						// instancesIds is set by modelInstanceCreator, it is accessible only internally
329 1
						_id !== null && _id !== undefined
330 1
							? _id
331
							: modelDefinition.syncable
332 1
							? uuid4()
333
							: ulid();
334

335 1
					draft.id = id;
336

337 1
					if (modelDefinition.syncable) {
338 1
						draft._version = _version;
339 1
						draft._lastChangedAt = _lastChangedAt;
340 1
						draft._deleted = _deleted;
341
					}
342
				}
343
			);
344

345 1
			return instance;
346
		}
347

348 1
		static copyOf(source: T, fn: (draft: MutableModel<T>) => T) {
349 1
			const modelConstructor = Object.getPrototypeOf(source || {}).constructor;
350 1
			if (!isValidModelConstructor(modelConstructor)) {
351 1
				const msg = 'The source object is not a valid model';
352 1
				logger.error(msg, { source });
353 1
				throw new Error(msg);
354
			}
355 1
			return produce(source, draft => {
356 1
				fn(<MutableModel<T>>draft);
357 1
				draft.id = source.id;
358 1
				const modelValidator = validateModelFields(modelDefinition);
359 1
				Object.entries(draft).forEach(([k, v]) => {
360 1
					modelValidator(k, v);
361
				});
362
			});
363
		}
364

365
		// "private" method (that's hidden via `Setting`) for `withSSRContext` to use
366
		// to gain access to `modelInstanceCreator` and `clazz` for persisting IDs from server to client.
367 1
		static fromJSON(json: T | T[]) {
368 1
			if (Array.isArray(json)) {
369 0
				return json.map(init => this.fromJSON(init));
370
			}
371

372 0
			const instance = modelInstanceCreator(clazz, json);
373 0
			const modelValidator = validateModelFields(modelDefinition);
374

375 0
			Object.entries(instance).forEach(([k, v]) => {
376 0
				modelValidator(k, v);
377
			});
378

379 0
			return instance;
380
		}
381 1
	});
382

383 1
	clazz[immerable] = true;
384

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

387 1
	return clazz;
388
};
389

390 1
const createNonModelClass = <T>(typeDefinition: SchemaNonModel) => {
391 1
	const clazz = <NonModelTypeConstructor<T>>(<unknown>class Model {
392
		constructor(init: ModelInit<T>) {
393 1
			const instance = produce(
394
				this,
395
				(draft: Draft<T & ModelInstanceMetadata>) => {
396 1
					initializeInstance(init, typeDefinition, draft);
397
				}
398
			);
399

400 1
			return instance;
401
		}
402 1
	});
403

404 1
	clazz[immerable] = true;
405

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

408 1
	return clazz;
409
};
410

411
function isQueryOne(obj: any): obj is string {
412 1
	return typeof obj === 'string';
413
}
414

415
function defaultConflictHandler(conflictData: SyncConflict): PersistentModel {
416 0
	const { localModel, modelConstructor, remoteModel } = conflictData;
417 0
	const { _version } = remoteModel;
418 0
	return modelInstanceCreator(modelConstructor, { ...localModel, _version });
419
}
420

421
function defaultErrorHandler(error: SyncError) {
422 0
	logger.warn(error);
423
}
424

425
function getModelConstructorByModelName(
426
	namespaceName: NAMESPACES,
427
	modelName: string
428
): PersistentModelConstructor<any> {
429
	let result: PersistentModelConstructor<any> | NonModelTypeConstructor<any>;
430

431 1
	switch (namespaceName) {
432 1
		case DATASTORE:
433 1
			result = dataStoreClasses[modelName];
434 1
			break;
435
		case USER:
436 1
			result = userClasses[modelName];
437 1
			break;
438
		case SYNC:
439 0
			result = syncClasses[modelName];
440 0
			break;
441
		case STORAGE:
442 0
			result = storageClasses[modelName];
443 0
			break;
444
		default:
445 0
			exhaustiveCheck(namespaceName);
446 0
			break;
447
	}
448

449 1
	if (isValidModelConstructor(result)) {
450 1
		return result;
451
	} else {
452 0
		const msg = `Model name is not valid for namespace. modelName: ${modelName}, namespace: ${namespaceName}`;
453 0
		logger.error(msg);
454

455 0
		throw new Error(msg);
456
	}
457
}
458

459
async function checkSchemaVersion(
460
	storage: Storage,
461
	version: string
462
): Promise<void> {
463 1
	const Setting = dataStoreClasses.Setting as PersistentModelConstructor<
464
		Setting
465
	>;
466

467 1
	const modelDefinition = schema.namespaces[DATASTORE].models.Setting;
468

469 1
	await storage.runExclusive(async s => {
470 1
		const [schemaVersionSetting] = await s.query(
471
			Setting,
472
			ModelPredicateCreator.createFromExisting(modelDefinition, c =>
473
				// @ts-ignore Argument of type '"eq"' is not assignable to parameter of type 'never'.
474 1
				c.key('eq', SETTING_SCHEMA_VERSION)
475
			),
476
			{ page: 0, limit: 1 }
477
		);
478

479 1
		if (schemaVersionSetting !== undefined) {
480 1
			const storedValue = JSON.parse(schemaVersionSetting.value);
481

482 1
			if (storedValue !== version) {
483 0
				await s.clear(false);
484
			}
485
		} else {
486 1
			await s.save(
487
				modelInstanceCreator(Setting, {
488
					key: SETTING_SCHEMA_VERSION,
489
					value: JSON.stringify(version),
490
				})
491
			);
492
		}
493
	});
494
}
495

496
let syncSubscription: ZenObservable.Subscription;
497

498
function getNamespace(): SchemaNamespace {
499 1
	const namespace: SchemaNamespace = {
500
		name: DATASTORE,
501
		relationships: {},
502
		enums: {},
503
		nonModels: {},
504
		models: {
505
			Setting: {
506
				name: 'Setting',
507
				pluralName: 'Settings',
508
				syncable: false,
509
				fields: {
510
					id: {
511
						name: 'id',
512
						type: 'ID',
513
						isRequired: true,
514
						isArray: false,
515
					},
516
					key: {
517
						name: 'key',
518
						type: 'String',
519
						isRequired: true,
520
						isArray: false,
521
					},
522
					value: {
523
						name: 'value',
524
						type: 'String',
525
						isRequired: true,
526
						isArray: false,
527
					},
528
				},
529
			},
530
		},
531
	};
532

533 1
	return namespace;
534
}
535

536 1
class DataStore {
537 1
	private amplifyConfig: Record<string, any> = {};
538
	private conflictHandler: ConflictHandler;
539
	private errorHandler: (error: SyncError) => void;
540
	private fullSyncInterval: number;
541
	private initialized: Promise<void>;
542
	private initReject: Function;
543
	private initResolve: Function;
544
	private maxRecordsToSync: number;
545
	private storage: Storage;
546
	private sync: SyncEngine;
547
	private syncPageSize: number;
548

549 1
	getModuleName() {
550 1
		return 'DataStore';
551
	}
552

553 1
	start = async (): Promise<void> => {
554 1
		if (this.initialized === undefined) {
555 1
			logger.debug('Starting DataStore');
556 1
			this.initialized = new Promise((res, rej) => {
557 1
				this.initResolve = res;
558 1
				this.initReject = rej;
559
			});
560
		} else {
561 1
			await this.initialized;
562

563 1
			return;
564
		}
565

566 1
		this.storage = new Storage(
567
			schema,
568
			namespaceResolver,
569
			getModelConstructorByModelName,
570
			modelInstanceCreator
571
		);
572

573 1
		await this.storage.init();
574

575 1
		await checkSchemaVersion(this.storage, schema.version);
576

577 1
		const { aws_appsync_graphqlEndpoint } = this.amplifyConfig;
578

579 1
		if (aws_appsync_graphqlEndpoint) {
580 0
			logger.debug('GraphQL endpoint available', aws_appsync_graphqlEndpoint);
581

582 0
			this.sync = new SyncEngine(
583
				schema,
584
				namespaceResolver,
585
				syncClasses,
586
				userClasses,
587
				this.storage,
588
				modelInstanceCreator,
589
				this.maxRecordsToSync,
590
				this.syncPageSize,
591
				this.conflictHandler,
592
				this.errorHandler
593
			);
594

595
			// tslint:disable-next-line:max-line-length
596 0
			const fullSyncIntervalInMilliseconds = this.fullSyncInterval * 1000 * 60; // fullSyncInterval from param is in minutes
597 0
			syncSubscription = this.sync
598
				.start({ fullSyncInterval: fullSyncIntervalInMilliseconds })
599
				.subscribe({
600 0
					next: ({ type, data }) => {
601
						// In Node, we need to wait for queries to be synced to prevent returning empty arrays.
602
						// In the Browser, we can begin returning data once subscriptions are in place.
603 0
						const readyType = isNode
604 1
							? ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY
605
							: ControlMessage.SYNC_ENGINE_STORAGE_SUBSCRIBED;
606

607 1
						if (type === readyType) {
608 0
							this.initResolve();
609
						}
610

611 0
						Hub.dispatch('datastore', {
612
							event: type,
613
							data,
614
						});
615
					},
616
					error: err => {
617 0
						logger.warn('Sync error', err);
618 0
						this.initReject();
619
					},
620
				});
621
		} else {
622 1
			logger.warn(
623
				"Data won't be synchronized. No GraphQL endpoint configured. Did you forget `Amplify.configure(awsconfig)`?",
624
				{
625
					config: this.amplifyConfig,
626
				}
627
			);
628

629 1
			this.initResolve();
630
		}
631

632 1
		await this.initialized;
633
	};
634

635 1
	query: {
636
		<T extends PersistentModel>(
637
			modelConstructor: PersistentModelConstructor<T>,
638
			id: string
639
		): Promise<T | undefined>;
640
		<T extends PersistentModel>(
641
			modelConstructor: PersistentModelConstructor<T>,
642
			criteria?: ProducerModelPredicate<T> | typeof PredicateAll,
643
			paginationProducer?: ProducerPaginationInput<T>
644
		): Promise<T[]>;
645
	} = async <T extends PersistentModel>(
646
		modelConstructor: PersistentModelConstructor<T>,
647
		idOrCriteria?: string | ProducerModelPredicate<T> | typeof PredicateAll,
648 1
		paginationProducer?: ProducerPaginationInput<T>
649
	): Promise<T | T[] | undefined> => {
650 1
		await this.start();
651

652
		//#region Input validation
653

654 1
		if (!isValidModelConstructor(modelConstructor)) {
655 1
			const msg = 'Constructor is not for a valid model';
656 1
			logger.error(msg, { modelConstructor });
657

658 1
			throw new Error(msg);
659
		}
660

661 1
		if (typeof idOrCriteria === 'string') {
662 1
			if (paginationProducer !== undefined) {
663 1
				logger.warn('Pagination is ignored when querying by id');
664
			}
665
		}
666

667 1
		const modelDefinition = getModelDefinition(modelConstructor);
668
		let predicate: ModelPredicate<T>;
669

670 1
		if (isQueryOne(idOrCriteria)) {
671 1
			predicate = ModelPredicateCreator.createForId<T>(
672
				modelDefinition,
673
				idOrCriteria
674
			);
675
		} else {
676 1
			if (isPredicatesAll(idOrCriteria)) {
677
				// Predicates.ALL means "all records", so no predicate (undefined)
678 0
				predicate = undefined;
679
			} else {
680 1
				predicate = ModelPredicateCreator.createFromExisting(
681
					modelDefinition,
682
					idOrCriteria
683
				);
684

685 1
				logger.debug('after createFromExisting - predicate', predicate);
686
			}
687
		}
688

689 1
		const pagination = this.processPagination(
690
			modelDefinition,
691
			paginationProducer
692
		);
693

694
		//#endregion
695

696 1
		logger.debug('params ready', {
697
			modelConstructor,
698
			predicate: ModelPredicateCreator.getPredicates(predicate, false),
699
			pagination: {
700
				...pagination,
701
				sort: ModelSortPredicateCreator.getPredicates(pagination.sort, false),
702
			},
703
		});
704

705 1
		const result = await this.storage.query(
706
			modelConstructor,
707
			predicate,
708
			pagination
709
		);
710

711 1
		return isQueryOne(idOrCriteria) ? result[0] : result;
712
	};
713

714 1
	save = async <T extends PersistentModel>(
715
		model: T,
716 1
		condition?: ProducerModelPredicate<T>
717
	): Promise<T> => {
718 1
		await this.start();
719

720 1
		const modelConstructor: PersistentModelConstructor<T> = model
721 1
			? <PersistentModelConstructor<T>>model.constructor
722
			: undefined;
723

724 1
		if (!isValidModelConstructor(modelConstructor)) {
725 1
			const msg = 'Object is not an instance of a valid model';
726 1
			logger.error(msg, { model });
727

728 1
			throw new Error(msg);
729
		}
730

731 1
		const modelDefinition = getModelDefinition(modelConstructor);
732

733 1
		const producedCondition = ModelPredicateCreator.createFromExisting(
734
			modelDefinition,
735
			condition
736
		);
737

738 1
		const [savedModel] = await this.storage.runExclusive(async s => {
739 1
			await s.save(model, producedCondition);
740

741 1
			return s.query(
742
				modelConstructor,
743
				ModelPredicateCreator.createForId(modelDefinition, model.id)
744
			);
745
		});
746

747 1
		return savedModel;
748
	};
749

750 1
	setConflictHandler = (config: DataStoreConfig): ConflictHandler => {
751 1
		const { DataStore: configDataStore } = config;
752

753 1
		const conflictHandlerIsDefault: () => boolean = () =>
754 1
			this.conflictHandler === defaultConflictHandler;
755

756 1
		if (configDataStore) {
757 0
			return configDataStore.conflictHandler;
758
		}
759 1
		if (conflictHandlerIsDefault() && config.conflictHandler) {
760 0
			return config.conflictHandler;
761
		}
762

763 1
		return this.conflictHandler || defaultConflictHandler;
764
	};
765

766 1
	setErrorHandler = (config: DataStoreConfig): ErrorHandler => {
767 1
		const { DataStore: configDataStore } = config;
768

769 1
		const errorHandlerIsDefault: () => boolean = () =>
770 1
			this.errorHandler === defaultErrorHandler;
771

772 1
		if (configDataStore) {
773 0
			return configDataStore.errorHandler;
774
		}
775 1
		if (errorHandlerIsDefault() && config.errorHandler) {
776 0
			return config.errorHandler;
777
		}
778

779 1
		return this.errorHandler || defaultErrorHandler;
780
	};
781

782 1
	delete: {
783
		<T extends PersistentModel>(
784
			model: T,
785
			condition?: ProducerModelPredicate<T>
786
		): Promise<T>;
787
		<T extends PersistentModel>(
788
			modelConstructor: PersistentModelConstructor<T>,
789
			id: string
790
		): Promise<T>;
791
		<T extends PersistentModel>(
792
			modelConstructor: PersistentModelConstructor<T>,
793
			condition: ProducerModelPredicate<T> | typeof PredicateAll
794
		): Promise<T[]>;
795
	} = async <T extends PersistentModel>(
796
		modelOrConstructor: T | PersistentModelConstructor<T>,
797 1
		idOrCriteria?: string | ProducerModelPredicate<T> | typeof PredicateAll
798
	) => {
799 1
		await this.start();
800

801
		let condition: ModelPredicate<T>;
802

803 1
		if (!modelOrConstructor) {
804 1
			const msg = 'Model or Model Constructor required';
805 1
			logger.error(msg, { modelOrConstructor });
806

807 1
			throw new Error(msg);
808
		}
809

810 1
		if (isValidModelConstructor(modelOrConstructor)) {
811 1
			const modelConstructor = modelOrConstructor;
812

813 1
			if (!idOrCriteria) {
814 1
				const msg =
815
					'Id to delete or criteria required. Do you want to delete all? Pass Predicates.ALL';
816 1
				logger.error(msg, { idOrCriteria });
817

818 1
				throw new Error(msg);
819
			}
820

821 1
			if (typeof idOrCriteria === 'string') {
822 1
				condition = ModelPredicateCreator.createForId<T>(
823
					getModelDefinition(modelConstructor),
824
					idOrCriteria
825
				);
826
			} else {
827 1
				condition = ModelPredicateCreator.createFromExisting(
828
					getModelDefinition(modelConstructor),
829
					/**
830
					 * idOrCriteria is always a ProducerModelPredicate<T>, never a symbol.
831
					 * The symbol is used only for typing purposes. e.g. see Predicates.ALL
832
					 */
833
					idOrCriteria as ProducerModelPredicate<T>
834
				);
835

836 1
				if (!condition || !ModelPredicateCreator.isValidPredicate(condition)) {
837 1
					const msg =
838
						'Criteria required. Do you want to delete all? Pass Predicates.ALL';
839 1
					logger.error(msg, { condition });
840

841 1
					throw new Error(msg);
842
				}
843
			}
844

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

847 1
			return deleted;
848
		} else {
849 1
			const model = modelOrConstructor;
850 1
			const modelConstructor = Object.getPrototypeOf(model || {})
851
				.constructor as PersistentModelConstructor<T>;
852

853 1
			if (!isValidModelConstructor(modelConstructor)) {
854 1
				const msg = 'Object is not an instance of a valid model';
855 1
				logger.error(msg, { model });
856

857 1
				throw new Error(msg);
858
			}
859

860 1
			const modelDefinition = getModelDefinition(modelConstructor);
861

862 1
			const idPredicate = ModelPredicateCreator.createForId<T>(
863
				modelDefinition,
864
				model.id
865
			);
866

867 1
			if (idOrCriteria) {
868 1
				if (typeof idOrCriteria !== 'function') {
869 1
					const msg = 'Invalid criteria';
870 1
					logger.error(msg, { idOrCriteria });
871

872 1
					throw new Error(msg);
873
				}
874

875 0
				condition = idOrCriteria(idPredicate);
876
			} else {
877 1
				condition = idPredicate;
878
			}
879

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

882 1
			return deleted;
883
		}
884
	};
885

886 1
	observe: {
887
		(): Observable<SubscriptionMessage<PersistentModel>>;
888

889
		<T extends PersistentModel>(model: T): Observable<SubscriptionMessage<T>>;
890

891
		<T extends PersistentModel>(
892
			modelConstructor: PersistentModelConstructor<T>,
893
			criteria?: string | ProducerModelPredicate<T>
894
		): Observable<SubscriptionMessage<T>>;
895
	} = <T extends PersistentModel = PersistentModel>(
896
		modelOrConstructor?: T | PersistentModelConstructor<T>,
897
		idOrCriteria?: string | ProducerModelPredicate<T>
898
	): Observable<SubscriptionMessage<T>> => {
899
		let predicate: ModelPredicate<T>;
900

901
		const modelConstructor: PersistentModelConstructor<T> =
902 1
			modelOrConstructor && isValidModelConstructor(modelOrConstructor)
903 1
				? modelOrConstructor
904
				: undefined;
905

906 1
		if (modelOrConstructor && modelConstructor === undefined) {
907 1
			const model = <T>modelOrConstructor;
908
			const modelConstructor =
909 1
				model && (<Object>Object.getPrototypeOf(model)).constructor;
910

911 1
			if (isValidModelConstructor<T>(modelConstructor)) {
912 1
				if (idOrCriteria) {
913 0
					logger.warn('idOrCriteria is ignored when using a model instance', {
914
						model,
915
						idOrCriteria,
916
					});
917
				}
918

919 1
				return this.observe(modelConstructor, model.id);
920
			} else {
921
				const msg =
922 0
					'The model is not an instance of a PersistentModelConstructor';
923 0
				logger.error(msg, { model });
924

925 0
				throw new Error(msg);
926
			}
927
		}
928

929 1
		if (idOrCriteria !== undefined && modelConstructor === undefined) {
930 0
			const msg = 'Cannot provide criteria without a modelConstructor';
931 0
			logger.error(msg, idOrCriteria);
932 0
			throw new Error(msg);
933
		}
934

935 1
		if (modelConstructor && !isValidModelConstructor(modelConstructor)) {
936 0
			const msg = 'Constructor is not for a valid model';
937 0
			logger.error(msg, { modelConstructor });
938

939 0
			throw new Error(msg);
940
		}
941

942 1
		if (typeof idOrCriteria === 'string') {
943 1
			predicate = ModelPredicateCreator.createForId<T>(
944
				getModelDefinition(modelConstructor),
945
				idOrCriteria
946
			);
947
		} else {
948 1
			predicate =
949 1
				modelConstructor &&
950
				ModelPredicateCreator.createFromExisting<T>(
951
					getModelDefinition(modelConstructor),
952
					idOrCriteria
953
				);
954
		}
955

956 1
		return new Observable<SubscriptionMessage<T>>(observer => {
957
			let handle: ZenObservable.Subscription;
958

959 1
			(async () => {
960 1
				await this.start();
961

962 1
				handle = this.storage
963
					.observe(modelConstructor, predicate)
964 0
					.filter(({ model }) => namespaceResolver(model) === USER)
965
					.subscribe(observer);
966
			})();
967

968 1
			return () => {
969 1
				if (handle) {
970 1
					handle.unsubscribe();
971
				}
972
			};
973
		});
974
	};
975

976 1
	configure = (config: DataStoreConfig = {}) => {
977
		const {
978 1
			DataStore: configDataStore,
979 1
			conflictHandler: configConflictHandler,
980 1
			errorHandler: configErrorHandler,
981 1
			maxRecordsToSync: configMaxRecordsToSync,
982 1
			syncPageSize: configSyncPageSize,
983 1
			fullSyncInterval: configFullSyncInterval,
984 1
			...configFromAmplify
985
		} = config;
986

987 1
		this.amplifyConfig = { ...configFromAmplify, ...this.amplifyConfig };
988

989 1
		this.conflictHandler = this.setConflictHandler(config);
990 1
		this.errorHandler = this.setErrorHandler(config);
991

992 1
		this.maxRecordsToSync =
993 1
			(configDataStore && configDataStore.maxRecordsToSync) ||
994
			this.maxRecordsToSync ||
995
			config.maxRecordsToSync;
996

997 1
		this.syncPageSize =
998 1
			(configDataStore && configDataStore.syncPageSize) ||
999
			this.syncPageSize ||
1000
			config.syncPageSize;
1001

1002 1
		this.fullSyncInterval =
1003 1
			(configDataStore && configDataStore.fullSyncInterval) ||
1004
			configFullSyncInterval ||
1005
			config.fullSyncInterval ||
1006
			24 * 60; // 1 day
1007
	};
1008

1009 1
	clear = async function clear() {
1010 1
		if (this.storage === undefined) {
1011 0
			return;
1012
		}
1013

1014 1
		if (syncSubscription && !syncSubscription.closed) {
1015 0
			syncSubscription.unsubscribe();
1016
		}
1017

1018 1
		await this.storage.clear();
1019

1020 1
		this.initialized = undefined; // Should re-initialize when start() is called.
1021 1
		this.storage = undefined;
1022 1
		this.sync = undefined;
1023
	};
1024

1025 1
	private processPagination<T extends PersistentModel>(
1026
		modelDefinition: SchemaModel,
1027
		paginationProducer: ProducerPaginationInput<T>
1028
	): PaginationInput<T> {
1029
		let sortPredicate: SortPredicate<T>;
1030 1
		const { limit, page, sort } = paginationProducer || {};
1031

1032 1
		if (page !== undefined && limit === undefined) {
1033 1
			throw new Error('Limit is required when requesting a page');
1034
		}
1035

1036 1
		if (page !== undefined) {
1037 1
			if (typeof page !== 'number') {
1038 1
				throw new Error('Page should be a number');
1039
			}
1040

1041 1
			if (page < 0) {
1042 1
				throw new Error("Page can't be negative");
1043
			}
1044
		}
1045

1046 1
		if (limit !== undefined) {
1047 1
			if (typeof limit !== 'number') {
1048 1
				throw new Error('Limit should be a number');
1049
			}
1050

1051 1
			if (limit < 0) {
1052 1
				throw new Error("Limit can't be negative");
1053
			}
1054
		}
1055

1056 1
		if (sort) {
1057 1
			sortPredicate = ModelSortPredicateCreator.createFromExisting(
1058
				modelDefinition,
1059
				paginationProducer.sort
1060
			);
1061
		}
1062

1063 1
		return {
1064
			limit,
1065
			page,
1066
			sort: sortPredicate,
1067
		};
1068
	}
1069 1
}
1070

1071 1
const instance = new DataStore();
1072 1
Amplify.register(instance);
1073

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

Read our documentation on viewing source code .

Loading