UNPKG

@commodo/fields-storage

Version:

We're working hard to get all the docs in order. New articles will be added daily.

582 lines (480 loc) 16.4 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _name = require("@commodo/name"); var _getStorageName = _interopRequireDefault(require("./getStorageName")); var _repropose = require("repropose"); var _lodash = _interopRequireDefault(require("lodash.clonedeep")); var _hooks = require("@commodo/hooks"); var _WithStorageError = _interopRequireDefault(require("./WithStorageError")); var _Collection = _interopRequireDefault(require("./Collection")); var _StoragePool = _interopRequireDefault(require("./StoragePool")); var _FieldsStorageAdapter = _interopRequireDefault(require("./FieldsStorageAdapter")); var _cursor = require("./cursor"); var _idGenerator = _interopRequireDefault(require("./idGenerator")); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } const defaults = { save: { hooks: {} }, delete: { hooks: {} } }; const generateId = () => _idGenerator.default.generate(); const hook = async (name, { options, model }) => { if (options.hooks[name] === false) { return; } await model.hook(name, { model, options }); }; const registerSaveUpdateCreateHooks = async (prefix, { existing, model, options }) => { await hook(prefix + "Save", { model, options }); if (existing) { await hook(prefix + "Update", { model, options }); } else { await hook(prefix + "Create", { model, options }); } }; const getName = instance => { return (0, _getStorageName.default)(instance) || (0, _name.getName)(instance); }; function cursorFrom(data, keys) { return (0, _cursor.encodeCursor)(keys.reduce((acc, key) => { if (key === "id") { return acc; } acc[key] = data[key]; return acc; }, { id: data.id })); } const withStorage = configuration => { return baseFn => { let fn = (0, _hooks.withHooks)({ delete() { if (!this.id) { throw new _WithStorageError.default("Cannot delete before saving to storage.", _WithStorageError.default.CANNOT_DELETE_NO_ID); } } })(baseFn); fn = (0, _repropose.withProps)(props => ({ __withStorage: { existing: false, processing: false, fieldsStorageAdapter: new _FieldsStorageAdapter.default() }, generateId, isId(value) { return typeof value === "string" && !!value.match(/^[a-zA-Z0-9]*$/); }, isExisting() { return this.__withStorage.existing; }, setExisting(existing = true) { this.__withStorage.existing = existing; return this; }, async save(options) { options = _objectSpread(_objectSpread({}, options), defaults.save); if (this.__withStorage.processing) { return; } this.__withStorage.processing = "save"; const existing = this.isExisting(); await registerSaveUpdateCreateHooks("before", { existing, model: this, options }); try { await hook("__save", { model: this, options }); if (existing) { await hook("__update", { model: this, options }); } else { await hook("__create", { model: this, options }); } options.validation !== false && (await this.validate()); await registerSaveUpdateCreateHooks("__before", { existing, model: this, options }); if (this.isDirty()) { if (!this.id) { this.id = this.constructor.generateId(); } if (existing) { const { getId } = options; await this.getStorageDriver().update([{ name: getName(this), query: typeof getId === "function" ? getId(this) : { id: this.id }, data: await this.toStorage() }]); } else { await this.getStorageDriver().create([{ name: getName(this), data: await this.toStorage() }]); } } await registerSaveUpdateCreateHooks("__after", { existing, model: this, options }); this.setExisting(); this.clean(); this.constructor.getStoragePool().add(this); } catch (e) { if (!existing) { this.getField("id").reset(); } throw e; } finally { this.__withStorage.processing = null; } await registerSaveUpdateCreateHooks("after", { existing, model: this, options }); }, /** * Deletes current and all linked models (if autoDelete on the attribute was enabled). * @param options */ async delete(options) { if (this.__withStorage.processing) { return; } this.__withStorage.processing = "delete"; options = _objectSpread(_objectSpread({}, options), defaults.delete); let getId; var _options = options; ({ getId } = _options); options = (0, _objectWithoutProperties2.default)(_options, ["getId"]); _options; try { await this.hook("delete", { options, model: this }); options.validation !== false && (await this.validate()); await this.hook("beforeDelete", { options, model: this }); await this.getStorageDriver().delete({ name: getName(this), options: _objectSpread({ query: typeof getId === "function" ? getId(this) : { id: this.id } }, options) }); await this.hook("afterDelete", { options, model: this }); this.constructor.getStoragePool().remove(this); } finally { props.__withStorage.processing = null; } }, getStorageDriver() { return this.constructor.__withStorage.driver; }, async populateFromStorage(data) { await this.__withStorage.fieldsStorageAdapter.fromStorage({ data, fields: this.getFields() }); return this; }, async toStorage({ skipDifferenceCheck = false } = {}) { return this.__withStorage.fieldsStorageAdapter.toStorage({ fields: this.getFields(), skipDifferenceCheck }); } }))(fn); fn = (0, _repropose.withStaticProps)(() => { const __withStorage = _objectSpread({}, configuration); if (!__withStorage.driver) { throw new _WithStorageError.default(`Storage driver missing.`, _WithStorageError.default.STORAGE_DRIVER_MISSING); } __withStorage.driver = typeof __withStorage.driver === "function" ? __withStorage.driver(void 0) : __withStorage.driver; if (configuration.pool) { __withStorage.storagePool = typeof __withStorage.pool === "function" ? __withStorage.pool(void 0) : __withStorage.pool; } else { __withStorage.storagePool = new _StoragePool.default(); } return { __withStorage, getStoragePool() { return this.__withStorage.storagePool; }, getStorageDriver() { return this.__withStorage.driver; }, isId(value) { return typeof value === "string" && !!value.match(/^[0-9a-fA-F]{24}$/); }, generateId, async find(options) { if (!options) { options = {}; } const maxPerPage = this.__withStorage.maxPerPage || 100; let { query = {}, sort, limit, before, after, totalCount: countTotal = false, defaultSortField = "id" } = options, other = (0, _objectWithoutProperties2.default)(options, ["query", "sort", "limit", "before", "after", "totalCount", "defaultSortField"]); if (!sort) { sort = {}; } limit = Number.isInteger(limit) && limit > 0 ? limit : maxPerPage; if (limit > maxPerPage) { throw new _WithStorageError.default(`Cannot query for more than ${maxPerPage} models per page.`, _WithStorageError.default.MAX_PER_PAGE_EXCEEDED); } // Keep a backup of query for optional total count const originalQuery = (0, _lodash.default)(query); /** * By default, there are no sorters nor cursor set so we use `id` to sort records in descending order. */ let forward = Boolean(after || !before); const cursor = (0, _cursor.decodeCursor)(after || before); if (cursor) { if (!query.$and) { query["$and"] = []; } const { id } = cursor, fields = (0, _objectWithoutProperties2.default)(cursor, ["id"]); const sortFields = [Object.keys(fields).shift()].filter(Boolean); const sortDirection = sort[sortFields[0]]; /** * By default, cursor contains only `id`, so we define the direction based on that assumption. */ let op = forward ? "$lt" : "$gt"; /** * If there are other sort fields besides the `id`, we need to take the direction of sorting into account. */ if (sortFields.length) { if (forward) { op = sortDirection === 1 ? "$gt" : "$lt"; } else { op = sortDirection === 1 ? "$lt" : "$gt"; } } if (sortFields.length) { query["$and"].push({ $or: [// Build condition from cursor fields sortFields.reduce((acc, key) => { acc[key] = { [op]: fields[key] }; return acc; }, {}), // Add condition to handle "exact match" records sortFields.reduce((acc, key) => { acc[key] = fields[key]; return acc; }, { id: { [op]: id } })] }); } else { query["$and"].push({ id: { [op]: id } }); } } if (!forward) { Object.keys(sort).forEach(key => { sort[key] *= -1; }); } if (sort && !sort[defaultSortField]) { sort[defaultSortField] = forward ? -1 : 1; } const params = _objectSpread({ query, sort, limit: limit + 1 }, other); let [results, meta] = await this.getStorageDriver().find({ name: getName(this), options: params }); // Have we reached the last record? const hasMore = results.length > limit; if (hasMore) { results.pop(); } const hasNextPage = !!before || hasMore; const hasPreviousPage = !!after || !!(before && hasMore); let totalCount = null; if (countTotal) { totalCount = await this.getStorageDriver().count({ name: getName(this), options: _objectSpread({ query: originalQuery }, other) }); } const lastIndex = results.length - 1; const nextCursor = hasNextPage ? cursorFrom(forward ? results[lastIndex] : results[0], Object.keys(sort)) : null; const previousCursor = hasPreviousPage ? cursorFrom(forward ? results[0] : results[lastIndex], Object.keys(sort)) : null; if (!forward) { results = results.reverse(); } const collection = new _Collection.default().setParams(params).setMeta(_objectSpread(_objectSpread({}, meta), {}, { cursors: { next: nextCursor, previous: previousCursor }, hasPreviousPage, hasNextPage, totalCount })); const result = results; if (result instanceof Array) { for (let i = 0; i < result.length; i++) { const pooled = this.getStoragePool().get(this, result[i].id); if (pooled) { collection.push(pooled); } else { const model = new this(); model.setExisting(); await model.populateFromStorage(result[i]); this.getStoragePool().add(model); collection.push(model); } } } return collection; }, /** * Finds a single model matched by given ID. * @param id * @param options */ async findById(id, options) { if (!id || !this.isId(id)) { return null; } const pooled = this.getStoragePool().get(this, id); if (pooled) { return pooled; } if (!options) { options = {}; } const newParams = _objectSpread(_objectSpread({}, options), {}, { query: { id } }); return await this.findOne(newParams); }, /** * Finds one model matched by given query parameters. * @param options */ async findOne(options) { if (!options) { options = {}; } const prepared = _objectSpread({}, options); const result = await this.getStorageDriver().findOne({ name: getName(this), options: prepared }); if (result) { const pooled = this.getStoragePool().get(this, result.id); if (pooled) { return pooled; } const model = new this(); model.setExisting(); await model.populateFromStorage(result); this.getStoragePool().add(model); return model; } return null; }, /** * Counts total number of models matched by given query parameters. * @param options */ async count(options) { if (!options) { options = {}; } const prepared = _objectSpread({}, options); return await this.getStorageDriver().count({ name: getName(this), options: prepared }); } }; })(fn); return fn; }; }; var _default = withStorage; exports.default = _default; //# sourceMappingURL=withStorage.js.map