@getanthill/datastore
Version:
Event-Sourced Datastore
243 lines • 8.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.decrypt = exports.checkProcessingAuthorization = exports.buildCursorLastId = exports.getQueryFromCursorLastId = exports.mapFindQuery = exports.mapQueryValues = exports.controllerBuilder = void 0;
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 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) => {
// @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 (req?.params?.model) {
err.details.push({
model: req.params.model,
});
}
return next(err);
}
}
next(err);
}
};
}
exports.controllerBuilder = controllerBuilder;
const FORMATTER_REGEXP = /^([a-z]+)\((.*)\)$/;
function mapQueryValues(Model, properties, obj, mustHash = true) {
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, properties?.[name]?.properties ?? {}, 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] = FORMATTER_REGEXP.exec(key) ?? [];
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;
}
exports.mapQueryValues = mapQueryValues;
function mapFindQuery(Model, query) {
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) => parseInt(v, 10)),
_id: q?._sort?._id ?? 1,
},
projection: (0, mapValues_1.default)(q._fields || {}, (v) => 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,
};
}
exports.mapFindQuery = mapFindQuery;
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;
}
exports.getQueryFromCursorLastId = getQueryFromCursorLastId;
function buildCursorLastId(entity, options) {
return entity
? Buffer.from(JSON.stringify((0, pick_1.default)(entity, Object.keys(options.sort)))).toString('hex')
: '';
}
exports.buildCursorLastId = buildCursorLastId;
function checkProcessingAuthorization(services, tokenId, modelConfig, data) {
if (services?.config?.features?.api?.checkProcessingAuthorization !== true) {
return;
}
if (Array.isArray(modelConfig.processings)) {
const authorizedProcessings = modelConfig.processings.filter((processing) => (processing.tokens ?? [tokenId]).includes(tokenId));
for (const encryptedField of modelConfig.encrypted_fields ?? []) {
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');
}
}
}
}
exports.checkProcessingAuthorization = checkProcessingAuthorization;
async function decrypt(services, access, modelName, data, fields) {
const Model = services.models.getModel(modelName);
const modelConfig = Model.getModelConfig();
checkProcessingAuthorization(services, access.id, modelConfig, data);
services.models?.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);
}
exports.decrypt = decrypt;
//# sourceMappingURL=index.js.map