UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

608 lines (519 loc) • 16.4 kB
import { ConsoleLogger as Logger } from '@aws-amplify/core'; import AsyncStorageDatabase from './AsyncStorageDatabase'; import { Adapter } from './index'; import { ModelInstanceCreator } from '../../datastore/datastore'; import { ModelPredicateCreator, ModelSortPredicateCreator, } from '../../predicates'; import { InternalSchema, isPredicateObj, ModelInstanceMetadata, ModelPredicate, NamespaceResolver, OpType, PaginationInput, PersistentModel, PersistentModelConstructor, PredicateObject, PredicatesGroup, QueryOne, RelationType, } from '../../types'; import { exhaustiveCheck, getIndex, getIndexFromAssociation, isModelConstructor, traverseModel, validatePredicate, sortCompareFunction, } from '../../util'; const logger = new Logger('DataStore'); export class AsyncStorageAdapter implements Adapter { private schema: InternalSchema; private namespaceResolver: NamespaceResolver; private modelInstanceCreator: ModelInstanceCreator; private getModelConstructorByModelName: ( namsespaceName: string, modelName: string ) => PersistentModelConstructor<any>; private db: AsyncStorageDatabase; private initPromise: Promise<void>; private resolve: (value?: any) => void; private reject: (value?: any) => void; private getStorenameForModel( modelConstructor: PersistentModelConstructor<any> ) { const namespace = this.namespaceResolver(modelConstructor); const { name: modelName } = modelConstructor; return this.getStorename(namespace, modelName); } private getStorename(namespace: string, modelName: string) { const storeName = `${namespace}_${modelName}`; return storeName; } async setUp( theSchema: InternalSchema, namespaceResolver: NamespaceResolver, modelInstanceCreator: ModelInstanceCreator, getModelConstructorByModelName: ( namsespaceName: string, modelName: string ) => PersistentModelConstructor<any> ) { if (!this.initPromise) { this.initPromise = new Promise((res, rej) => { this.resolve = res; this.reject = rej; }); } else { await this.initPromise; return; } this.schema = theSchema; this.namespaceResolver = namespaceResolver; this.modelInstanceCreator = modelInstanceCreator; this.getModelConstructorByModelName = getModelConstructorByModelName; try { if (!this.db) { this.db = new AsyncStorageDatabase(); await this.db.init(); this.resolve(); } } catch (error) { this.reject(error); } } async save<T extends PersistentModel>( model: T, condition?: ModelPredicate<T> ): Promise<[T, OpType.INSERT | OpType.UPDATE][]> { const modelConstructor = Object.getPrototypeOf(model) .constructor as PersistentModelConstructor<T>; const storeName = this.getStorenameForModel(modelConstructor); const connectedModels = traverseModel( modelConstructor.name, model, this.schema.namespaces[this.namespaceResolver(modelConstructor)], this.modelInstanceCreator, this.getModelConstructorByModelName ); const namespaceName = this.namespaceResolver(modelConstructor); const set = new Set<string>(); const connectionStoreNames = Object.values(connectedModels).map( ({ modelName, item, instance }) => { const storeName = this.getStorename(namespaceName, modelName); set.add(storeName); return { storeName, item, instance }; } ); const fromDB = await this.db.get(model.id, storeName); if (condition && fromDB) { const predicates = ModelPredicateCreator.getPredicates(condition); const { predicates: predicateObjs, type } = predicates; const isValid = validatePredicate(fromDB, type, predicateObjs); if (!isValid) { const msg = 'Conditional update failed'; logger.error(msg, { model: fromDB, condition: predicateObjs }); throw new Error(msg); } } const result: [T, OpType.INSERT | OpType.UPDATE][] = []; for await (const resItem of connectionStoreNames) { const { storeName, item, instance } = resItem; const { id } = item; const fromDB = <T>await this.db.get(id, storeName); const opType: OpType = fromDB ? OpType.UPDATE : OpType.INSERT; if (id === model.id || opType === OpType.INSERT) { await this.db.save(item, storeName); result.push([instance, opType]); } } return result; } private async load<T>( namespaceName: string, srcModelName: string, records: T[] ): Promise<T[]> { const namespace = this.schema.namespaces[namespaceName]; const relations = namespace.relationships[srcModelName].relationTypes; const connectionStoreNames = relations.map(({ modelName }) => { return this.getStorename(namespaceName, modelName); }); const modelConstructor = this.getModelConstructorByModelName( namespaceName, srcModelName ); if (connectionStoreNames.length === 0) { return records.map(record => this.modelInstanceCreator(modelConstructor, record) ); } for await (const relation of relations) { const { fieldName, modelName, targetName, relationType } = relation; const storeName = this.getStorename(namespaceName, modelName); const modelConstructor = this.getModelConstructorByModelName( namespaceName, modelName ); switch (relationType) { case 'HAS_ONE': for await (const recordItem of records) { const getByfield = recordItem[targetName] ? targetName : fieldName; if (!recordItem[getByfield]) break; const connectionRecord = await this.db.get( recordItem[getByfield], storeName ); recordItem[fieldName] = connectionRecord && this.modelInstanceCreator(modelConstructor, connectionRecord); } break; case 'BELONGS_TO': for await (const recordItem of records) { if (recordItem[targetName]) { const connectionRecord = await this.db.get( recordItem[targetName], storeName ); recordItem[fieldName] = connectionRecord && this.modelInstanceCreator(modelConstructor, connectionRecord); delete recordItem[targetName]; } } break; case 'HAS_MANY': // TODO: Lazy loading break; default: exhaustiveCheck(relationType); break; } } return records.map(record => this.modelInstanceCreator(modelConstructor, record) ); } async query<T extends PersistentModel>( modelConstructor: PersistentModelConstructor<T>, predicate?: ModelPredicate<T>, pagination?: PaginationInput<T> ): Promise<T[]> { const storeName = this.getStorenameForModel(modelConstructor); const namespaceName = this.namespaceResolver(modelConstructor); const predicates = predicate && ModelPredicateCreator.getPredicates(predicate); const queryById = predicates && this.idFromPredicate(predicates); const hasSort = pagination && pagination.sort; const hasPagination = pagination && pagination.limit; const records: T[] = await (async () => { if (queryById) { const record = await this.getById(storeName, queryById); return record ? [record] : []; } if (predicates) { const filtered = await this.filterOnPredicate(storeName, predicates); return this.inMemoryPagination(filtered, pagination); } if (hasSort || hasPagination) { const all = await this.getAll(storeName); return this.inMemoryPagination(all, pagination); } return this.getAll(storeName); })(); return await this.load(namespaceName, modelConstructor.name, records); } private async getById<T extends PersistentModel>( storeName: string, id: string ): Promise<T> { const record = <T>await this.db.get(id, storeName); return record; } private async getAll<T extends PersistentModel>( storeName: string ): Promise<T[]> { return await this.db.getAll(storeName); } private idFromPredicate<T extends PersistentModel>( predicates: PredicatesGroup<T> ) { const { predicates: predicateObjs } = predicates; const idPredicate = predicateObjs.length === 1 && (predicateObjs.find( p => isPredicateObj(p) && p.field === 'id' && p.operator === 'eq' ) as PredicateObject<T>); return idPredicate && idPredicate.operand; } private async filterOnPredicate<T extends PersistentModel>( storeName: string, predicates: PredicatesGroup<T> ) { const { predicates: predicateObjs, type } = predicates; const all = <T[]>await this.getAll(storeName); const filtered = predicateObjs ? all.filter(m => validatePredicate(m, type, predicateObjs)) : all; return filtered; } private inMemoryPagination<T extends PersistentModel>( records: T[], pagination?: PaginationInput<T> ): T[] { if (pagination && records.length > 1) { if (pagination.sort) { const sortPredicates = ModelSortPredicateCreator.getPredicates( pagination.sort ); if (sortPredicates.length) { const compareFn = sortCompareFunction(sortPredicates); records.sort(compareFn); } } const { page = 0, limit = 0 } = pagination; const start = Math.max(0, page * limit) || 0; const end = limit > 0 ? start + limit : records.length; return records.slice(start, end); } return records; } async queryOne<T extends PersistentModel>( modelConstructor: PersistentModelConstructor<T>, firstOrLast: QueryOne = QueryOne.FIRST ): Promise<T | undefined> { const storeName = this.getStorenameForModel(modelConstructor); const result = <T>await this.db.getOne(firstOrLast, storeName); return result && this.modelInstanceCreator(modelConstructor, result); } async delete<T extends PersistentModel>( modelOrModelConstructor: T | PersistentModelConstructor<T>, condition?: ModelPredicate<T> ): Promise<[T[], T[]]> { const deleteQueue: { storeName: string; items: T[] }[] = []; if (isModelConstructor(modelOrModelConstructor)) { const modelConstructor = modelOrModelConstructor; const nameSpace = this.namespaceResolver(modelConstructor); // models to be deleted. const models = await this.query(modelConstructor, condition); // TODO: refactor this to use a function like getRelations() const relations = this.schema.namespaces[nameSpace].relationships[modelConstructor.name] .relationTypes; if (condition !== undefined) { await this.deleteTraverse( relations, models, modelConstructor.name, nameSpace, deleteQueue ); await this.deleteItem(deleteQueue); const deletedModels = deleteQueue.reduce( (acc, { items }) => acc.concat(items), <T[]>[] ); return [models, deletedModels]; } else { await this.deleteTraverse( relations, models, modelConstructor.name, nameSpace, deleteQueue ); await this.deleteItem(deleteQueue); const deletedModels = deleteQueue.reduce( (acc, { items }) => acc.concat(items), <T[]>[] ); return [models, deletedModels]; } } else { const model = modelOrModelConstructor; const modelConstructor = Object.getPrototypeOf(model) .constructor as PersistentModelConstructor<T>; const nameSpace = this.namespaceResolver(modelConstructor); const storeName = this.getStorenameForModel(modelConstructor); if (condition) { const fromDB = await this.db.get(model.id, storeName); if (fromDB === undefined) { const msg = 'Model instance not found in storage'; logger.warn(msg, { model }); return [[model], []]; } const predicates = ModelPredicateCreator.getPredicates(condition); const { predicates: predicateObjs, type } = predicates; const isValid = validatePredicate(fromDB, type, predicateObjs); if (!isValid) { const msg = 'Conditional update failed'; logger.error(msg, { model: fromDB, condition: predicateObjs }); throw new Error(msg); } const relations = this.schema.namespaces[nameSpace].relationships[modelConstructor.name] .relationTypes; await this.deleteTraverse( relations, [model], modelConstructor.name, nameSpace, deleteQueue ); } else { const relations = this.schema.namespaces[nameSpace].relationships[modelConstructor.name] .relationTypes; await this.deleteTraverse( relations, [model], modelConstructor.name, nameSpace, deleteQueue ); } await this.deleteItem(deleteQueue); const deletedModels = deleteQueue.reduce( (acc, { items }) => acc.concat(items), <T[]>[] ); return [[model], deletedModels]; } } private async deleteItem<T extends PersistentModel>( deleteQueue?: { storeName: string; items: T[] | IDBValidKey[] }[] ) { for await (const deleteItem of deleteQueue) { const { storeName, items } = deleteItem; for await (const item of items) { if (item) { if (typeof item === 'object') { const id = item['id']; await this.db.delete(id, storeName); } } } } } /** * Populates the delete Queue with all the items to delete * @param relations * @param models * @param srcModel * @param nameSpace * @param deleteQueue */ private async deleteTraverse<T extends PersistentModel>( relations: RelationType[], models: T[], srcModel: string, nameSpace: string, deleteQueue: { storeName: string; items: T[] }[] ): Promise<void> { for await (const rel of relations) { const { relationType, modelName, targetName } = rel; const storeName = this.getStorename(nameSpace, modelName); const index: string = getIndex( this.schema.namespaces[nameSpace].relationships[modelName] .relationTypes, srcModel ) || // if we were unable to find an index via relationTypes // i.e. for keyName connections, attempt to find one by the // associatedWith property getIndexFromAssociation( this.schema.namespaces[nameSpace].relationships[modelName].indexes, rel.associatedWith ); switch (relationType) { case 'HAS_ONE': for await (const model of models) { const hasOneIndex = index || 'byId'; const hasOneCustomField = targetName in model; const value = hasOneCustomField ? model[targetName] : model.id; if (!value) break; const allRecords = await this.db.getAll(storeName); const recordToDelete = allRecords.filter( childItem => childItem[hasOneIndex] === value ); await this.deleteTraverse( this.schema.namespaces[nameSpace].relationships[modelName] .relationTypes, recordToDelete, modelName, nameSpace, deleteQueue ); } break; case 'HAS_MANY': for await (const model of models) { const allRecords = await this.db.getAll(storeName); const childrenArray = allRecords.filter( childItem => childItem[index] === model.id ); await this.deleteTraverse( this.schema.namespaces[nameSpace].relationships[modelName] .relationTypes, childrenArray, modelName, nameSpace, deleteQueue ); } break; case 'BELONGS_TO': // Intentionally blank break; default: exhaustiveCheck(relationType); break; } } deleteQueue.push({ storeName: this.getStorename(nameSpace, srcModel), items: models.map(record => this.modelInstanceCreator( this.getModelConstructorByModelName(nameSpace, srcModel), record ) ), }); } async clear(): Promise<void> { await this.db.clear(); this.db = undefined; this.initPromise = undefined; } async batchSave<T extends PersistentModel>( modelConstructor: PersistentModelConstructor<any>, items: ModelInstanceMetadata[] ): Promise<[T, OpType][]> { const { name: modelName } = modelConstructor; const namespaceName = this.namespaceResolver(modelConstructor); const storeName = this.getStorename(namespaceName, modelName); const batch: ModelInstanceMetadata[] = []; for (const item of items) { const { id } = item; const connectedModels = traverseModel( modelConstructor.name, this.modelInstanceCreator(modelConstructor, item), this.schema.namespaces[this.namespaceResolver(modelConstructor)], this.modelInstanceCreator, this.getModelConstructorByModelName ); const { instance } = connectedModels.find( ({ instance }) => instance.id === id ); batch.push(instance); } return await this.db.batchSave(storeName, batch); } } export default new AsyncStorageAdapter();