UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

294 lines (292 loc) • 12.3 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.StorageAdapterBase = void 0; const core_1 = require("@aws-amplify/core"); const predicates_1 = require("../../predicates"); const types_1 = require("../../types"); const util_1 = require("../../util"); const relationship_1 = require("../relationship"); const logger = new core_1.ConsoleLogger('DataStore'); const DB_NAME = 'amplify-datastore'; class StorageAdapterBase { constructor() { this.dbName = DB_NAME; } /** * Initializes local DB * * @param theSchema * @param namespaceResolver * @param modelInstanceCreator * @param getModelConstructorByModelName * @param sessionId */ async setUp(theSchema, namespaceResolver, modelInstanceCreator, getModelConstructorByModelName, sessionId) { await this.preSetUpChecks(); if (!this.initPromise) { this.initPromise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } else { await this.initPromise; return; } if (sessionId) { this.dbName = `${DB_NAME}-${sessionId}`; } this.schema = theSchema; this.namespaceResolver = namespaceResolver; this.modelInstanceCreator = modelInstanceCreator; this.getModelConstructorByModelName = getModelConstructorByModelName; try { if (!this.db) { this.db = await this.initDb(); this.resolve(); } } catch (error) { this.reject(error); } } /** * @param modelConstructor * @returns local DB table name */ getStorenameForModel(modelConstructor) { const namespace = this.namespaceResolver(modelConstructor); const { name: modelName } = modelConstructor; return (0, util_1.getStorename)(namespace, modelName); } /** * * @param model - instantiated model record * @returns the record's primary key values */ getIndexKeyValuesFromModel(model) { const modelConstructor = Object.getPrototypeOf(model) .constructor; const namespaceName = this.namespaceResolver(modelConstructor); const keys = (0, util_1.getIndexKeys)(this.schema.namespaces[namespaceName], modelConstructor.name); return (0, util_1.extractPrimaryKeyValues)(model, keys); } /** * Common metadata for `save` operation * used by individual storage adapters * * @param model */ saveMetadata(model) { const modelConstructor = Object.getPrototypeOf(model) .constructor; const storeName = this.getStorenameForModel(modelConstructor); const namespaceName = this.namespaceResolver(modelConstructor); const connectedModels = (0, util_1.traverseModel)(modelConstructor.name, model, this.schema.namespaces[namespaceName], this.modelInstanceCreator, this.getModelConstructorByModelName); const set = new Set(); const connectionStoreNames = Object.values(connectedModels).map(({ modelName, item, instance }) => { const resolvedStoreName = (0, util_1.getStorename)(namespaceName, modelName); set.add(resolvedStoreName); const keys = (0, util_1.getIndexKeys)(this.schema.namespaces[namespaceName], modelName); return { storeName: resolvedStoreName, item, instance, keys }; }); const modelKeyValues = this.getIndexKeyValuesFromModel(model); return { storeName, set, connectionStoreNames, modelKeyValues }; } /** * Enforces conditional save. Throws if condition is not met. * used by individual storage adapters * * @param model */ validateSaveCondition(condition, fromDB) { if (!(condition && fromDB)) { return; } const predicates = predicates_1.ModelPredicateCreator.getPredicates(condition); const { predicates: predicateObjs, type } = predicates; const isValid = (0, util_1.validatePredicate)(fromDB, type, predicateObjs); if (!isValid) { const msg = 'Conditional update failed'; logger.error(msg, { model: fromDB, condition: predicateObjs }); throw new Error(msg); } } /** * Instantiate models from POJO records returned from the database * * @param namespaceName - string model namespace * @param srcModelName - string model name * @param records - array of uninstantiated records * @returns */ async load(namespaceName, srcModelName, records) { const namespace = this.schema.namespaces[namespaceName]; const relations = namespace.relationships[srcModelName].relationTypes; const connectionStoreNames = relations.map(({ modelName }) => { return (0, util_1.getStorename)(namespaceName, modelName); }); const modelConstructor = this.getModelConstructorByModelName(namespaceName, srcModelName); if (connectionStoreNames.length === 0) { return records.map(record => this.modelInstanceCreator(modelConstructor, record)); } return records.map(record => this.modelInstanceCreator(modelConstructor, record)); } /** * Extracts operands from a predicate group into an array of key values * Used in the query method * * @param predicates - predicate group * @param keyPath - string array of key names ['id', 'sortKey'] * @returns string[] of key values * * @example * ```js * { and:[{ id: { eq: 'abc' }}, { sortKey: { eq: 'def' }}] } * ``` * Becomes * ``` * ['abc', 'def'] * ``` */ keyValueFromPredicate(predicates, keyPath) { const { predicates: predicateObjs } = predicates; if (predicateObjs.length !== keyPath.length) { return; } const keyValues = []; for (const key of keyPath) { const predicateObj = predicateObjs.find(p => // it's a relevant predicate object only if it's an equality // operation for a key field from the key: (0, types_1.isPredicateObj)(p) && p.field === key && p.operator === 'eq' && p.operand !== null && p.operand !== undefined); predicateObj && keyValues.push(predicateObj.operand); } return keyValues.length === keyPath.length ? keyValues : undefined; } /** * Common metadata for `query` operation * used by individual storage adapters * * @param modelConstructor * @param predicate * @param pagination */ queryMetadata(modelConstructor, predicate, pagination) { const storeName = this.getStorenameForModel(modelConstructor); const namespaceName = this.namespaceResolver(modelConstructor); const predicates = predicate && predicates_1.ModelPredicateCreator.getPredicates(predicate); const keyPath = (0, util_1.getIndexKeys)(this.schema.namespaces[namespaceName], modelConstructor.name); const queryByKey = predicates && this.keyValueFromPredicate(predicates, keyPath); const hasSort = pagination && pagination.sort; const hasPagination = pagination && pagination.limit; return { storeName, namespaceName, queryByKey, predicates, hasSort, hasPagination, }; } /** * Delete record * Cascades to related records (for Has One and Has Many relationships) * * @param modelOrModelConstructor * @param condition * @returns */ async delete(modelOrModelConstructor, condition) { await this.preOpCheck(); const deleteQueue = []; if ((0, util_1.isModelConstructor)(modelOrModelConstructor)) { const modelConstructor = modelOrModelConstructor; const namespace = this.namespaceResolver(modelConstructor); const models = await this.query(modelConstructor, condition); if (condition !== undefined) { await this.deleteTraverse(models, modelConstructor, namespace, deleteQueue); await this.deleteItem(deleteQueue); const deletedModels = deleteQueue.reduce((acc, { items }) => acc.concat(items), []); return [models, deletedModels]; } else { await this.deleteTraverse(models, modelConstructor, namespace, deleteQueue); await this.deleteItem(deleteQueue); const deletedModels = deleteQueue.reduce((acc, { items }) => acc.concat(items), []); return [models, deletedModels]; } } else { const model = modelOrModelConstructor; const modelConstructor = Object.getPrototypeOf(model) .constructor; const namespaceName = this.namespaceResolver(modelConstructor); const storeName = this.getStorenameForModel(modelConstructor); if (condition) { const keyValues = this.getIndexKeyValuesFromModel(model); const fromDB = await this._get(storeName, keyValues); if (fromDB === undefined) { const msg = 'Model instance not found in storage'; logger.warn(msg, { model }); return [[model], []]; } const predicates = predicates_1.ModelPredicateCreator.getPredicates(condition); const { predicates: predicateObjs, type } = predicates; const isValid = (0, util_1.validatePredicate)(fromDB, type, predicateObjs); if (!isValid) { const msg = 'Conditional update failed'; logger.error(msg, { model: fromDB, condition: predicateObjs }); throw new Error(msg); } await this.deleteTraverse([model], modelConstructor, namespaceName, deleteQueue); } else { await this.deleteTraverse([model], modelConstructor, namespaceName, deleteQueue); } await this.deleteItem(deleteQueue); const deletedModels = deleteQueue.reduce((acc, { items }) => acc.concat(items), []); return [[model], deletedModels]; } } /** * Recursively traverse relationship graph and add * all Has One and Has Many relations to `deleteQueue` param * * Actual deletion of records added to `deleteQueue` occurs in the `delete` method * * @param models * @param modelConstructor * @param namespace * @param deleteQueue */ async deleteTraverse(models, modelConstructor, namespace, deleteQueue) { const cascadingRelationTypes = ['HAS_ONE', 'HAS_MANY']; for await (const model of models) { const modelDefinition = this.schema.namespaces[namespace].models[modelConstructor.name]; const modelMeta = { builder: modelConstructor, schema: modelDefinition, pkField: (0, util_1.extractPrimaryKeyFieldNames)(modelDefinition), }; const relationships = relationship_1.ModelRelationship.allFrom(modelMeta).filter(r => cascadingRelationTypes.includes(r.type)); for await (const r of relationships) { const queryObject = r.createRemoteQueryObject(model); if (queryObject !== null) { const relatedRecords = await this.query(r.remoteModelConstructor, predicates_1.ModelPredicateCreator.createFromFlatEqualities(r.remoteDefinition, queryObject)); await this.deleteTraverse(relatedRecords, r.remoteModelConstructor, namespace, deleteQueue); } } } deleteQueue.push({ storeName: (0, util_1.getStorename)(namespace, modelConstructor.name), items: models, }); } } exports.StorageAdapterBase = StorageAdapterBase; //# sourceMappingURL=StorageAdapterBase.js.map