@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
JavaScript
"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