UNPKG

@restorecommerce/chassis-srv

Version:
646 lines 27.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Arango = void 0; const arangojs_1 = require("arangojs"); const _ = __importStar(require("lodash")); const common_1 = require("./common"); /** * ArangoDB database provider. */ class Arango { /** * * @param {Object} conn Arangojs database connection. */ constructor(db, logger) { this.db = db; this.logger = logger; this.customQueries = new Map(); this.collectionNameAnalyzerMap = new Map(); } /** * Find documents based on filter. * * @param {String} collectionName Collection name * @param {Object} filter Key, value Object * @param {Object} options options.limit, options.offset * @return {Promise<any>} Promise for list of found documents. */ async find(collectionName, filter, options) { if (_.isNil(collectionName) || !_.isString(collectionName) || _.isEmpty(collectionName)) { this.logger?.error('invalid or missing collection argument for find operation'); throw new Error('invalid or missing collection argument for find operation'); } let filterQuery = filter ?? {}; const opts = options ?? {}; let filterResult; let bindVars; let customFilter = ''; // checking if a custom query should be used if (!_.isEmpty(opts.customQueries)) { for (const queryName of opts.customQueries) { if (!this.customQueries.has(queryName)) { this.logger?.error(`custom query ${common_1.query} not found`); throw new Error(`custom query ${common_1.query} not found`); } const customQuery = this.customQueries.get(queryName); if (customQuery.type == 'query') { // standalone query const result = await (0, common_1.query)(this.db, collectionName, customQuery.code, opts.customArguments || {}); // Cursor object return result.all(); // TODO: paginate } else { // filter customFilter += ` ${customQuery.code} `; } } } if (!_.isArray(filterQuery)) { filterQuery = [filterQuery]; } if (_.isEmpty(filterQuery[0])) { filterQuery = true; } else { filterResult = (0, common_1.buildFilter)(filterQuery); filterQuery = filterResult.q; } let sortQuery = (0, common_1.buildSorter)(opts); const limitQuery = (0, common_1.buildLimiter)(opts); const returnResult = (0, common_1.buildReturn)(opts); let returnQuery = returnResult.q; // return complete node in case no specific fields are specified if (_.isEmpty(returnQuery)) { returnQuery = 'RETURN node'; } // if search options are set build search query let searchQuery; if (opts?.search?.search) { const searchString = JSON.stringify(options.search.search); if (this.collectionNameAnalyzerMap && this.collectionNameAnalyzerMap.get(collectionName)) { const searchFields = (options?.search?.fields?.length > 0) ? options.search.fields : this.collectionNameAnalyzerMap.get(collectionName).fields; const similarityThreshold = this.collectionNameAnalyzerMap.get(collectionName).similarityThreshold; const viewName = this.collectionNameAnalyzerMap.get(collectionName).viewName; const caseSensitive = options?.search?.case_sensitive; const analyzerOptions = this.collectionNameAnalyzerMap.get(collectionName).analyzerOptions; let analyzerName; if (caseSensitive) { // for casesensitive search use "ngram" analayzer type analyzerOptions.forEach((optionObj) => { const keyName = Object.keys(optionObj)[0]; if (optionObj[keyName].type === 'ngram') { analyzerName = JSON.stringify(keyName); } }); } else { // for case-insensitive search use "pipleline" type (ngram + norm) analyzerOptions.forEach((optionObj) => { const keyName = Object.keys(optionObj)[0]; if (optionObj[keyName].type === 'pipeline') { analyzerName = JSON.stringify(keyName); } }); } for (const field of searchFields) { if (!searchQuery) { searchQuery = `NGRAM_MATCH(node.${field}, ${searchString}, ${similarityThreshold}, ${analyzerName}) `; } else { searchQuery = searchQuery + `OR NGRAM_MATCH(node.${field}, ${searchString}, ${similarityThreshold}, ${analyzerName}) `; } } // override collection name with view name collectionName = viewName; // override sortQuery (to rank based on score for frequency search match - term frequency–inverse document frequency algorithm TF-IDF) sortQuery = `SORT TFIDF(node) DESC`; } else { this.logger?.info(`View and analyzer configuration data not set for ${collectionName} and hence ignoring search string`); } } let queryString = `FOR node in @@collection FILTER ${filterQuery}`; if (searchQuery) { queryString = `FOR node in @@collection SEARCH ${searchQuery} FILTER ${filterQuery}`; } if (!_.isEmpty(customFilter)) { queryString += customFilter; } queryString += ` ${sortQuery} ${limitQuery} ${returnQuery}`; let varArgs = {}; if (filterResult && filterResult.bindVarsMap) { varArgs = filterResult.bindVarsMap; } let returnArgs = {}; if (returnResult && returnResult.bindVarsMap) { returnArgs = returnResult.bindVarsMap; } let limitArgs; if (_.isEmpty(limitQuery)) { limitArgs = {}; } else { if (!_.isNil(opts.limit)) { limitArgs = { limit: opts.limit }; if (!_.isNil(opts.offset)) { limitArgs = { offset: opts.offset, limit: opts.limit }; } } } varArgs = _.assign(varArgs, limitArgs); varArgs = _.assign(varArgs, returnArgs); bindVars = _.assign({ '@collection': collectionName }, varArgs); if (!_.isEmpty(customFilter) && opts.customArguments) { bindVars = _.assign(bindVars, { customArguments: opts.customArguments }); } let res; if (!searchQuery) { res = await (0, common_1.query)(this.db, collectionName, queryString, bindVars); } else { res = await this.db.query(queryString, bindVars); } const docs = await res.all(); // TODO: paginate return _.map(docs, common_1.sanitizeOutputFields); } /** * Find documents by id (_key). * * @param {String} collection Collection name * @param {String|array.String} ids A single ID or multiple IDs. * @return {Promise<any>} A list of found documents. */ async findByID(collectionName, ids) { if (_.isNil(collectionName) || !_.isString(collectionName) || _.isEmpty(collectionName)) { this.logger?.error('invalid or missing collection argument for findByID operation'); throw new Error('invalid or missing collection argument for findByID operation'); } if (_.isNil(ids)) { this.logger?.error('invalid or missing ids argument for findByID operation'); throw new Error('invalid or missing ids argument for findByID operation'); } if (!_.isArray(ids)) { ids = [ids]; } const filter = ids.map((id) => { return { id }; }); const filterResult = (0, common_1.buildFilter)(filter); const filterQuery = filterResult.q; let varArgs = {}; if (filterResult && filterResult.bindVarsMap) { varArgs = filterResult.bindVarsMap; } const queryString = `FOR node in @@collection FILTER ${filterQuery} RETURN node`; const bindVars = Object.assign({ '@collection': collectionName }, varArgs); const res = await (0, common_1.query)(this.db, collectionName, queryString, bindVars); const docs = await res.all(); return _.map(docs, common_1.sanitizeOutputFields); } /** * retreive the documents including the document handlers (_key, _id and _rev). * * @param {String} collectionName Collection name * @param {any} collection Collection Object * @param {any} documents list of documents * @param {string[]} idsArray list of document ids * @returns {Promise<any>} A list of documents including the document handlers */ async getDocumentHandlers(collectionName, collection, documents, idsArray) { let ids = []; if (documents && !_.isArray(documents)) { documents = [documents]; } if (documents && documents.length > 0) { for (const doc of documents) { ids.push(doc.id); } } if (!_.isEmpty(idsArray) && _.isArray(idsArray)) { ids = idsArray; } const queryString = (0, arangojs_1.aql) `FOR node in ${collection} FILTER node.id IN ${ids} return node`; const res = await (0, common_1.query)(this.db, collectionName, queryString); const docsWithSelector = await res.all(); return docsWithSelector; } /** * Find documents by filter and updates them with document. * * @param {String} collection Collection name * @param {Object} updateDocuments List of documents to update */ async update(collectionName, updateDocuments) { const documents = _.cloneDeep(updateDocuments); const updateDocsResponse = []; if (_.isNil(collectionName) || !_.isString(collectionName) || _.isEmpty(collectionName)) { this.logger?.error('invalid or missing collection argument for update operation'); throw new Error('invalid or missing collection argument for update operation'); } if (_.isNil(documents)) { this.logger?.error('invalid or missing document argument for update operation'); throw new Error('invalid or missing document argument for update operation'); } const collection = this.db.collection(collectionName); const collectionExists = await collection.exists(); if (!collectionExists) { this.logger?.error(`Collection ${collectionName} does not exist for update operation`); throw new Error(`Collection ${collectionName} does not exist for update operation`); } if (!_.isArray(documents)) { this.logger?.error(`Documents should be list for update operation`); throw new Error(`Documents should be list for update operation`); } const docsWithHandlers = await this.getDocumentHandlers(collectionName, collection, documents); // update _key for the input documents for (const document of documents) { let foundInDB = false; for (const docWithHandler of docsWithHandlers) { if (docWithHandler.id === document.id) { foundInDB = true; document._key = docWithHandler._key; break; } } if (!foundInDB) { // if document is not found in DB use the id itself as _key // this key will return an array in response since it does not exist document._key = document.id; } } const updatedDocs = await collection.updateAll(documents, { returnNew: true }); for (const doc of updatedDocs) { if ('new' in doc) { updateDocsResponse.push((0, common_1.sanitizeOutputFields)(doc?.new)); } else { updateDocsResponse.push(doc); } } return updateDocsResponse; } /** * Find each document based on it's key and update it. * If the document does not exist it will be created. * * @param {String} collectionName Collection name * @param {Object|Array.Object} documents */ async upsert(collectionName, documents) { if (_.isNil(collectionName) || !_.isString(collectionName) || _.isEmpty(collectionName)) { this.logger?.error('invalid or missing collection argument for upsert operation'); throw new Error('invalid or missing collection argument for upsert operation'); } if (_.isNil(documents)) { this.logger?.error('invalid or missing documents argument for upsert operation'); throw new Error('invalid or missing documents argument for upsert operation'); } let docs = _.cloneDeep(documents); if (!_.isArray(documents)) { docs = [documents]; } _.forEach(docs, (document, i) => { docs[i] = (0, common_1.sanitizeInputFields)(document); }); const upsertResponse = []; const collection = this.db.collection(collectionName); const collectionExists = await collection.exists(); if (!collectionExists) { this.logger?.error(`Collection ${collectionName} does not exist for upsert operation`); throw new Error(`Collection ${collectionName} does not exist for upsert operation`); } let upsertedDocs = await collection.saveAll(docs, { returnNew: true, overwriteMode: 'update' }); if (!_.isArray(upsertedDocs)) { upsertedDocs = [upsertedDocs]; } for (const doc of upsertedDocs) { if ('new' in doc) { upsertResponse.push((0, common_1.sanitizeOutputFields)(doc.new)); } else { upsertResponse.push(doc); } } return upsertResponse; } /** * Delete all documents with provided identifiers ids. * * @param {String} collection Collection name * @param {Object} ids list of document identifiers * @return {Promise<any>} delete response */ async delete(collectionName, ids) { if (_.isNil(collectionName) || !_.isString(collectionName) || _.isEmpty(collectionName)) { this.logger?.error('invalid or missing collection argument'); throw new Error('invalid or missing collection argument'); } if (_.isNil(ids) || _.isEmpty(ids)) { this.logger?.error('invalid or missing document IDs argument for delete operation'); throw new Error('invalid or missing document IDs argument for delete operation'); } const collection = this.db.collection(collectionName); const collectionExists = await collection.exists(); if (!collectionExists) { this.logger?.error(`Collection ${collectionName} does not exist for delete operation`); throw new Error(`Collection ${collectionName} does not exist for delete operation`); } // retreive _key for the give ids const docsWithHandlers = await this.getDocumentHandlers(collectionName, collection, null, ids); for (const id of ids) { // check if given id is present in docsWithHandlers let foundDocInDB = false; for (const doc of docsWithHandlers) { if (doc.id === id) { foundDocInDB = true; break; } } // if document is not found in DB use the id itself as _key // this key will return an array in response since it does not exist if (!foundDocInDB) { docsWithHandlers.push({ _key: id }); } } const deleteHandlerIds = []; for (const doc of docsWithHandlers) { deleteHandlerIds.push(doc._key); } return collection.removeAll(deleteHandlerIds); } /** * Count all documents selected by filter. * * @param {String} collection Collection name * @param {Object} filter */ async count(collectionName, filter) { if (_.isNil(collectionName) || !_.isString(collectionName) || _.isEmpty(collectionName)) { this.logger?.error('invalid or missing collection argument for count operation'); throw new Error('invalid or missing collection argument for count operation'); } let filterQuery = filter || {}; let filterResult; if (!_.isArray(filterQuery)) { filterQuery = [filterQuery]; } if (_.isEmpty(filterQuery[0])) { filterQuery = true; } else { filterResult = (0, common_1.buildFilter)(filterQuery); filterQuery = filterResult.q; } let varArgs = {}; if (filterResult && filterResult.bindVarsMap) { varArgs = filterResult.bindVarsMap; } const queryString = `FOR node in @@collection FILTER ${filterQuery} COLLECT WITH COUNT INTO length RETURN length`; const bindVars = Object.assign({ '@collection': collectionName }, varArgs); const res = await (0, common_1.query)(this.db, collectionName, queryString, bindVars); const nn = await res.all(); return nn[0]; } /** * When calling without a collection name, * delete all documents in all collections in the database. * When providing a collection name, * delete all documents in specified collection in the database. * @param [string] collection Collection name. */ async truncate(collection) { if (_.isNil(collection)) { const collections = await this.db.collections(); for (let i = 0; i < collections.length; i += 1) { const c = this.db.collection(collections[i].name); await c.truncate(); } } else { const c = this.db.collection(collection); await c.truncate(); } } /** * Drop view * @param string[] list of view names. */ async dropView(viewName) { const dropViewResponse = []; if (viewName.length > 0) { for (const view of viewName) { try { const response = await this.db.view(view).drop(); this.logger?.info(`View ${view} dropped successfully`, response); if (response === true) { dropViewResponse.push({ id: view, code: 200, message: `View ${view} dropped successfully` }); } } catch (err) { this.logger?.error(`Error dropping View ${view}`, { code: err.code, message: err.message, stack: err.stack }); dropViewResponse.push({ id: view, code: err.code, message: err.message }); } } } return dropViewResponse; } /** * Delete Analyzer * @param string[] list of analyzer names. */ async deleteAnalyzer(analyzerName) { const deleteResponse = []; if (analyzerName.length > 0) { for (const analyzer of analyzerName) { try { const response = await this.db.analyzer(analyzer).drop(); this.logger?.info(`Analyzer ${analyzer} deleted successfully`, response); if (response.code === 200 && response.error === false) { deleteResponse.push({ id: analyzer, code: response.code, message: `Analyzer ${analyzer} deleted successfully` }); } } catch (err) { this.logger?.error(`Error deleting analyzer ${analyzer}`, { code: err.code, message: err.message, stack: err.stack }); deleteResponse.push({ id: analyzer, code: err.code, message: err.message }); } } } return deleteResponse; } /** * Insert documents into database. * * @param {String} collection Collection name * @param {Object|array.Object} documents A single or multiple documents. */ async insert(collectionName, documents) { if (_.isNil(collectionName) || !_.isString(collectionName) || _.isEmpty(collectionName)) { this.logger?.error('invalid or missing collection argument for insert operation'); throw new Error('invalid or missing collection argument for insert operation'); } if (_.isNil(documents)) { this.logger?.error('invalid or missing documents argument for insert operation'); throw new Error('invalid or missing documents argument for insert operation'); } let docs = _.cloneDeep(documents); if (!_.isArray(documents)) { docs = [documents]; } _.forEach(docs, (document, i) => { docs[i] = (0, common_1.sanitizeInputFields)(document); }); const collection = this.db.collection(collectionName); const collectionExists = await collection.exists(); if (!collectionExists) { await collection.create(); } const insertResponse = []; let createdDocs = await collection.saveAll(docs, { returnNew: true }); if (!_.isArray(createdDocs)) { createdDocs = [createdDocs]; } for (const doc of createdDocs) { if ('new' in doc) { insertResponse.push((0, common_1.sanitizeOutputFields)(doc.new)); } else { insertResponse.push(doc); } } return insertResponse; } /** * Registers a custom AQL query. * * @param script * @param name */ registerCustomQuery(name, script, type) { this.customQueries.set(name, { code: script, type }); } /** * Unregisters a custom query. * @param name */ unregisterCustomQuery(name) { if (!this.customQueries.has(name)) { this.logger?.error('custom function not found'); throw new Error('custom function not found'); } this.customQueries.delete(name); } listCustomQueries() { return [...this.customQueries]; } async createAnalyzerAndView(viewConfig, collectionName) { if (!viewConfig.view.viewName || !viewConfig?.view?.options) { this.logger?.error(`View name or view configuration missing for ${collectionName}`); throw new Error(`View name or view configuration missing for ${collectionName}`); } if ((!viewConfig?.analyzers) || (viewConfig.analyzers.length === 0) || !(viewConfig.analyzerOptions)) { this.logger?.error(`Analyzer options or configuration missing for ${collectionName}`); throw new Error(`Analyzer options or configuration missing for ${collectionName}`); } // create analyzer if it does not exist for (const analyzerName of viewConfig.analyzers) { const analyzer = this.db.analyzer(analyzerName); if (!(await analyzer.exists())) { try { const analyzerCfg = viewConfig.analyzerOptions.filter((optionsCfg) => Object.keys(optionsCfg)[0] === analyzerName); if (analyzerCfg?.length === 1) { await analyzer.create(analyzerCfg[0][analyzerName]); this.logger?.info(`Analyzer ${analyzerName} created successfully`); } } catch (err) { this.logger?.error(`Error creating analyzer ${analyzerName}`, { code: err.code, message: err.message, stack: err.stack }); } } else { this.logger?.info(`Analyzer ${analyzerName} already exists`); } } // check if collection exits (before creating view) const collection = this.db.collection(collectionName); const collectionExists = await collection.exists(); try { if (!collectionExists) { await collection.create(); this.logger?.info(`Collection ${collectionName} created successfully`); } } catch (err) { if (err.message && err.message.indexOf('duplicate name') == -1) { this.logger?.error(`Error creating collection ${collectionName}`, { code: err.code, message: err.message, stack: err.stack }); throw err; } } // create view if it does not exist const view = this.db.view(viewConfig.view.viewName); const viewExists = await view.exists(); if (!viewExists) { try { await this.db.createView(viewConfig?.view?.viewName, viewConfig?.view?.options); this.logger?.info(`View ${viewConfig?.view?.viewName} created successfully`); } catch (err) { this.logger?.error(`Error creating View ${viewConfig?.view?.viewName}`, { code: err.code, message: err.message, stack: err.stack }); } } else { this.logger?.info(`View ${viewConfig?.view?.viewName} already exists`); } // map the collectionName with list of indexed fields, view name, analyzerslist, similarity threshold // to be used in find() const indexedFields = Object.keys(viewConfig.view.options.links[collectionName].fields); this.collectionNameAnalyzerMap.set(collectionName, { viewName: viewConfig.view.viewName, fields: indexedFields, analyzerOptions: viewConfig.analyzerOptions, similarityThreshold: viewConfig.view.similarityThreshold }); } } exports.Arango = Arango; //# sourceMappingURL=base.js.map