@aws-amplify/datastore
Version:
AppSyncLocal support for aws-amplify
294 lines (292 loc) • 12.3 kB
JavaScript
'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