@getanthill/datastore
Version:
Event-Sourced Datastore
274 lines • 10.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.controllerBuilder = controllerBuilder;
exports.mapQueryValues = mapQueryValues;
exports.mapFindQuery = mapFindQuery;
exports.getQueryFromCursorLastId = getQueryFromCursorLastId;
exports.buildCursorLastId = buildCursorLastId;
exports.checkProcessingAuthorization = checkProcessingAuthorization;
exports.decrypt = decrypt;
exports.getVersionFromDate = getVersionFromDate;
const has_1 = __importDefault(require("lodash/has"));
const isObject_1 = __importDefault(require("lodash/isObject"));
const mapValues_1 = __importDefault(require("lodash/mapValues"));
const pick_1 = __importDefault(require("lodash/pick"));
const pickBy_1 = __importDefault(require("lodash/pickBy"));
const utils_1 = require("../../utils");
const constants_1 = require("../../constants");
const DEFAULT_ERRORS = {
message: {
'Invalid Model': 400,
'Event schema validation error': 400,
'Not Found': 404,
'Entity is readonly': 405,
'Imperative condition failed': 412,
'Entity must be created first': 422,
'State schema validation error': 422,
},
code: {
11000: 409,
},
};
function controllerBuilder(services, meter, handler, errors = {}) {
const _errors = {
message: {
...DEFAULT_ERRORS.message,
...errors.message,
},
code: {
...DEFAULT_ERRORS.code,
...errors.code,
},
};
return async (req, res, next) => {
var _a;
// @ts-ignore
if (res.body) {
return next();
}
try {
res.locals.meter = meter;
res.locals.attributes = {
model: req.params.model,
};
meter({
state: 'request',
model: req.params.model,
});
await handler(req, res, next);
}
catch (err) {
for (const key in _errors) {
if (err[key] in _errors[key]) {
let status = _errors[key][err[key]];
if (typeof status === 'function') {
status = await status(err, req, res, next);
}
err.status = status;
err.details = err.details || [];
if ((_a = req === null || req === void 0 ? void 0 : req.params) === null || _a === void 0 ? void 0 : _a.model) {
err.details.push({
model: req.params.model,
});
}
return next(err);
}
}
next(err);
}
};
}
const FORMATTER_REGEXP = /^([a-z]+)\((.*)\)$/;
function mapQueryValues(Model, properties, obj, mustHash = true) {
var _a, _b, _c;
const propNames = Object.getOwnPropertyNames(obj);
for (const name of propNames) {
let prop = obj[name];
if (!Array.isArray(prop) && (0, isObject_1.default)(prop)) {
obj[name] = mapQueryValues(Model, (_b = (_a = properties === null || properties === void 0 ? void 0 : properties[name]) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : {}, prop);
continue;
}
let key = name;
/**
* @deprecated Backward compatibility for the previous
* implementation of the `_must_hash` logic applied
* globally to the query result.
*/
if (mustHash === true && Model.isEncryptedField(key)) {
key = `hash(${name})`;
}
const [, formatter, _name] = (_c = FORMATTER_REGEXP.exec(key)) !== null && _c !== void 0 ? _c : [];
if (formatter) {
key = _name;
switch (formatter) {
case 'date':
prop = Array.isArray(prop)
? prop.map((p) => (0, utils_1.getDate)(p))
: (0, utils_1.getDate)(prop);
break;
case 'hash':
key = `${_name}.hash`;
prop = Array.isArray(prop)
? prop.map((p) => Model.hashValue(p))
: Model.hashValue(prop);
break;
default:
// ...
}
delete obj[name];
obj[key] = prop;
}
const { type } = properties[key] || {};
if (type !== 'array' && Array.isArray(prop)) {
if (prop.length === 0) {
return null;
}
obj[key] = {
$in: prop,
};
}
}
return obj;
}
function mapFindQuery(Model, query) {
var _a, _b;
const schema = Model.getSchema().model;
const properties = schema.properties;
const q = 'q' in query ? JSON.parse(query.q) : query;
let _query = (0, pickBy_1.default)(q, (_v, k) => !k.startsWith('_'));
const _options = {
sort: {
...(0, mapValues_1.default)(q._sort || {
created_at: 1,
}, (v) => Number.parseInt(v, 10)),
_id: (_b = (_a = q === null || q === void 0 ? void 0 : q._sort) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : 1,
},
projection: (0, mapValues_1.default)(q._fields || {}, (v) => Number.parseInt(v, 10)),
};
if ('_fields' in q && Object.keys(_options.projection).length > 0) {
_options.projection = {
..._options.projection,
// Add mandatory fields for find and walk pagination logic
[Model.getCorrelationField()]: 1,
created_at: 1,
updated_at: 1,
version: 1,
_id: 1,
};
}
/**
* @deprecated in favor of v2 request with `hash()`
* formatter
*/
const mustHash = q._must_hash === true;
_query = mapQueryValues(Model, properties, _query, mustHash);
if (_query === null) {
return {
query: null,
options: _options,
};
}
_query = (0, utils_1.deepCoerce)(_query, schema);
/**
* @deprecated in favor of v2 request without special
* field `_q`
*/
if (q._q) {
try {
q._q = JSON.parse(q._q);
q._q = JSON.parse(q._q);
}
catch (err) {
// ..
}
_query = {
..._query,
...(0, utils_1.deepCoerce)(q._q, schema),
};
}
return {
query: _query,
options: _options,
};
}
function getQueryFromCursorLastId(Model, options, cursorLastId, cursorLastCorrelationId) {
const _revertId = JSON.parse(Buffer.from(cursorLastId, 'hex').toString());
const { query: _lastIdQuery } = mapFindQuery(Model, _revertId);
const cursorQuery = (0, mapValues_1.default)(_lastIdQuery, (val, key) => {
if (options.sort[key] === 1) {
// Following condition for backward compatibility:
// `2024-06-26`: Nominal case should be `$gte` with `cursorLastCorrelationId`
return { [cursorLastCorrelationId ? '$gte' : '$gt']: val };
}
// Following condition for backward compatibility:
// `2024-06-26`: Nominal case should be `$gte` with `cursorLastCorrelationId`
return { [cursorLastCorrelationId ? '$lte' : '$lt']: val };
});
return cursorQuery;
}
function buildCursorLastId(entity, options) {
return entity
? Buffer.from(JSON.stringify((0, pick_1.default)(entity, Object.keys(options.sort)))).toString('hex')
: '';
}
function checkProcessingAuthorization(services, tokenId, modelConfig, data) {
var _a, _b, _c, _d;
if (((_c = (_b = (_a = services === null || services === void 0 ? void 0 : services.config) === null || _a === void 0 ? void 0 : _a.features) === null || _b === void 0 ? void 0 : _b.api) === null || _c === void 0 ? void 0 : _c.checkProcessingAuthorization) !== true) {
return;
}
if (Array.isArray(modelConfig.processings)) {
const authorizedProcessings = modelConfig.processings.filter((processing) => { var _a; return ((_a = processing.tokens) !== null && _a !== void 0 ? _a : [tokenId]).includes(tokenId); });
for (const encryptedField of (_d = modelConfig.encrypted_fields) !== null && _d !== void 0 ? _d : []) {
if ((0, has_1.default)(data, encryptedField) === false) {
continue;
}
const isProcessingAuthorizedOnField = authorizedProcessings.find((processing) => processing.field === encryptedField);
if (!isProcessingAuthorizedOnField) {
throw new Error('Unauthorized field processing');
}
}
}
}
async function decrypt(services, access, modelName, data, fields) {
var _a;
const Model = services.models.getModel(modelName);
const modelConfig = Model.getModelConfig();
checkProcessingAuthorization(services, access.id, modelConfig, data);
(_a = services.models) === null || _a === void 0 ? void 0 : _a.log(50, modelConfig.name, data[Model.getCorrelationField()], '[decrypt] Entity decrypted', {
model: modelConfig.name,
correlation_id: data[Model.getCorrelationField()],
id: access.id,
level: access.level,
persist: true,
});
return Model.decrypt(data, fields);
}
async function getVersionFromDate(services, Model, correlationId, version) {
if (version === undefined) {
return version;
}
if (typeof version === 'number') {
return version;
}
if (!constants_1.REGEXP_DATE_ISO_STRING_8601.test(version)) {
return Number.parseInt(version, 10);
}
const lastEvent = await Model.getEventsCollection(Model.db(services.mongodb)).findOne({
[Model.getCorrelationField()]: correlationId,
created_at: {
$lte: (0, utils_1.getDate)(version),
},
}, {
sort: {
version: -1,
},
});
if (!lastEvent) {
throw new Error('Not Found');
}
return lastEvent.version;
}
//# sourceMappingURL=index.js.map