UNPKG

@restorecommerce/chassis-srv

Version:

Restore Commerce microservice chassis

383 lines 13.9 kB
import { clone, isArray, isString, isDate, isBoolean, isNumber, keys, isNullish, isEmptyish, startsWith, forEachObj, mapKeys, join } from 'remeda'; import Long from 'long'; /** * Ensure that the collection exists and process the query * @param {Object} db arangodb connection * @param {string} collectionName collection name * @param {string} query query string * @param {Object} args list of arguments, optional * @return {Promise} arangojs query result */ export const query = async (db, collectionName, query, args) => { const collection = db.collection(collectionName); const collectionExists = await collection.exists(); if (!collectionExists) { await collection.create(); } return await db.query(query, args); }; /** * Convert id to arangodb friendly key. * @param {string} id document identification * @return {any} arangodb friendly key */ export const idToKey = (id) => { return id.replace(/\//g, '_'); }; /** * Ensure that the _key exists. * @param {Object} document Document template. * @return {any} Clone of the document with the _key field set. */ const ensureKey = (document) => { const doc = clone(document); if ('_key' in doc) { return doc; } const id = doc.id; if (id) { doc['_key'] = idToKey(id); } return doc; }; const ensureDatatypes = (document) => { const doc = clone(document); for (const key of keys(doc)) { if (Long.isLong(doc[key])) { doc[key] = doc[key].toNumber(); } } return doc; }; /** * Remove arangodb specific fields. * @param {Object} document A document returned from arangodb. * @return {Object} A clone of the document without arangodb specific fields. */ export const sanitizeOutputFields = (document) => { const doc = clone(document); delete doc['_id']; delete doc['_key']; delete doc['_rev']; return doc; }; export const sanitizeInputFields = (document) => { const doc = ensureDatatypes(document); return ensureKey(doc); }; /** * Auto-casting reference value by using native function of arangoDB * * @param {string} key * @param {object} value - raw value optional * @return {object} interpreted value */ export const autoCastKey = (key, value) => { if (isDate(value)) { // Date return `DATE_TIMESTAMP(node.${key})`; } else if (key === 'id') { return `node._key`; } return `node.${key}`; }; /** * Auto-casting raw data * * @param {object} value - raw value * @returns {any} interpreted value */ export const autoCastValue = (key, value) => { if (isArray(value)) { return value.map((value) => key === 'id' ? idToKey(value) : value?.toString()); } if (isString(value)) { // String return key === 'id' ? idToKey(value) : value?.toString(); } if (isBoolean(value)) { // Boolean return Boolean(value); } if (isNumber(value)) { return Number(value); } if (Long.isLong(value)) { return value?.toNumber(); } if (isDate(value)) { // Date return new Date(value); } return value; }; /** * Links children of filter together via a comparision operator. * @param {any} filter * @param {string} op comparision operator * @param {number} index to keep track of bind variables * @param {any} bindVarsMap mapping of keys to values for bind variables * @return {any} query template string and bind variables */ export const buildComparison = (filter, op, index, bindVarsMap) => { const ele = filter.map((e) => { if (!isArray(e)) { e = [e]; } e = buildFilter(e, index, bindVarsMap); index += 1; return e.q; }); let q = '( '; for (let i = 0; i < ele.length; i += 1) { if (i == ele.length - 1) { q = `${q} ${ele[i]} )`; } else { q = `${q} ${ele[i]} ${op} `; } } return { q, bindVarsMap }; }; /** * Creates a filter key, value. * When the value is a string, boolean, number or date a equal comparision is created. * Otherwise if the key corresponds to a known operator, the operator is constructed. * @param {string} key * @param {string|boolean|number|date|object} value * @param {number} index to keep track of bind variables * @param {any} bindVarsMap mapping of keys to values for bind variables * @return {String} query template string */ export const buildField = (key, value, index, bindVarsMap) => { const bindValueVar = `@value${index}`; const bindValueVarWithOutPrefix = `value${index}`; if (isString(value) || isBoolean(value) || isNumber(value) || isDate(value)) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value); return `${autoCastKey(key, value)} == ${bindValueVar}`; } if (value?.$eq !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$eq); return `${autoCastKey(key, value)} == ${bindValueVar}`; } if (value?.$gt !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$gt); return `${autoCastKey(key, value)} > ${bindValueVar}`; } if (value?.$gte !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$gte); return `${autoCastKey(key, value)} >= ${bindValueVar}`; } if (value?.$lt !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$lt); return `${autoCastKey(key, value)} < ${bindValueVar}`; } if (value?.$lte !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$lte); return `${autoCastKey(key, value)} <= ${bindValueVar}`; } if (value?.$ne !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$ne); return `${autoCastKey(key, value)} != ${bindValueVar}`; } if (value?.$inVal !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$inVal); return `${bindValueVar} IN ${autoCastKey(key, value)}`; } if (value?.$in !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$in); if (isString(value.$in)) { // if it is a field which should be an array // (useful for querying within a document list-like attributen return `${bindValueVar} IN ${autoCastKey(key)}`; } // assuming it is a list of provided values return `${autoCastKey(key, value)} IN ${bindValueVar}`; } if (value?.$nin !== undefined) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$nin); if (isString(value.$nin)) { // if it is a field which should be an array // (useful for querying within a document list-like attributen return `${bindValueVar} NOT IN ${autoCastKey(key)}`; } return `${autoCastKey(key, value)} NOT IN ${bindValueVar}`; } if (isString(value?.$iLike)) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, value.$iLike); // @param 'true' is for case insensitive return `LIKE (${autoCastKey(key, value)}, ${bindValueVar}, true)`; } if (value?.$not !== undefined) { const temp = buildField(key, value.$not, index, bindVarsMap); return `!(${temp})`; } if ('$isEmpty' in value) { bindVarsMap[bindValueVarWithOutPrefix] = autoCastValue(key, ''); // will always search for an empty string return `${autoCastKey(key, '')} == ${bindValueVar}`; } if (isString(value?.$startswith)) { const bindValueVar1 = `@value${index + 1}`; const bindValueVarWithOutPrefix1 = `value${index + 1}`; const k = autoCastKey(key); const v = autoCastValue(key, value.$startswith); bindVarsMap[bindValueVarWithOutPrefix] = v; bindVarsMap[bindValueVarWithOutPrefix1] = v; return `LEFT(${k}, LENGTH(${bindValueVar})) == ${bindValueVar1}`; } if (isString(value?.$endswith)) { const bindValueVar1 = `@value${index + 1}`; const bindValueVarWithOutPrefix1 = `value${index + 1}`; const k = autoCastKey(key); const v = autoCastValue(key, value.$endswith); bindVarsMap[bindValueVarWithOutPrefix] = v; bindVarsMap[bindValueVarWithOutPrefix1] = v; return `RIGHT(${k}, LENGTH(${bindValueVar})) == ${bindValueVar1}`; } throw new Error(`Unsupported operator ${keys(value)} in ${key}!`); }; /** * Build ArangoDB query based on filter. * @param {Object} filter key, value tree object. * @param {number} index to keep track of bind variables * @param {any} bindVarsMap mapping of keys to values for bind variables * @return {any} query template string and bind variables */ export const buildFilter = (filter, index, bindVarsMap) => { if (!index) { index = 0; } if (!bindVarsMap) { bindVarsMap = {}; } if (filter.length > 0) { let q = ''; let multipleFilters = false; for (const eachFilter of filter) { forEachObj(eachFilter, (value, key) => { switch (key) { case '$or': if (!multipleFilters) { if (isEmptyish(value)) { q = true; } else { q = buildComparison(value, '||', index, bindVarsMap).q; } multipleFilters = true; // since there is a possiblility for recursive call from buildComparision to buildFilter again. index += 1; } else { q = q + '&& ' + buildComparison(value, '||', index, bindVarsMap).q; index += 1; } break; case '$and': if (!multipleFilters) { if (isEmptyish(value)) { q = false; } else { q = buildComparison(value, '&&', index, bindVarsMap).q; } multipleFilters = true; index += 1; } else { q = q + '&& ' + buildComparison(value, '&&', index, bindVarsMap).q; index += 1; } break; default: if (startsWith(key, '$')) { throw new Error(`unsupported query operator ${key}`); } if (!multipleFilters) { q = buildField(key, value, index, bindVarsMap); multipleFilters = true; index += 1; } else { q = q + ' && ' + buildField(key, value, index, bindVarsMap); index += 1; } break; } }); } return { q, bindVarsMap }; } }; /** * Build count and offset filters. * @param {Object} options query options * @return {String} template query string */ export const buildLimiter = (options) => { // LIMIT count // LIMIT offset, count if (!isNullish(options.limit)) { if (!isNullish(options.offset)) { return `LIMIT @offset, @limit`; } return `LIMIT @limit`; } return ''; }; /** * Build sort filter. * @param {Object} options query options * @param {number} index to keep track of bind variables * @param {any} bindVarsMap Object containing bind key to values * @return {any} template query string and bind variables Object */ export const buildSorter = (options, index, bindVarsMap) => { if (isNullish(options.sort) || isEmptyish(options.sort)) { return ''; } const sort = mapKeys(options.sort, (key, value) => { return autoCastKey(key); }); const sortKeysOrder = Object.entries(sort).map(([key, value]) => `${key} ${value}`).join(', '); return `SORT ${sortKeysOrder}`; }; export const buildReturn = (options) => { let excludeIndex = 0; let includeIndex = 0; const bindVarsMap = {}; let q = ''; if (isNullish(options.fields) || isEmptyish(options.fields)) { return { q, bindVarsMap }; } const keep = []; const exclude = []; forEachObj(options.fields, (value, key) => { switch (value) { case 0: bindVarsMap[`exclude${excludeIndex}`] = key; exclude.push(`@exclude${excludeIndex}`); excludeIndex += 1; break; case 1: default: bindVarsMap[`include${includeIndex}`] = key; keep.push(`@include${includeIndex}`); includeIndex += 1; } }); if (keep.length > 0) { const include = join(keep.map((e) => { return e; }), ','); q = `RETURN KEEP( node, ${include} )`; return { q, bindVarsMap }; } if (exclude.length > 0) { const unset = join(exclude.map((e) => { return e; }), ','); q = `RETURN UNSET( node, ${unset} )`; return { q, bindVarsMap }; } q = 'RETURN result'; return { q, bindVarsMap }; }; export const encodeMessage = (object) => { return Buffer.from(JSON.stringify(object)); }; //# sourceMappingURL=common.js.map