UNPKG

qldb-serialiser

Version:
938 lines (890 loc) 37.3 kB
const { Agent } = require('https'); const { QldbDriver, RetryConfig } = require('amazon-qldb-driver-nodejs'); const {Operators} = require('./qldb.operators'); const { arrayStringify } = require('./utils'); class qldbConnect { /** * Initiates the QLDB driver * * @param ledgerName * @param serviceConfigOptions */ constructor(ledgerName, serviceConfigOptions) { const { retryLimit, maxConcurrentTransactions, timeoutMillis, ...qldbClientOptions } = serviceConfigOptions //Reuse connections with keepAlive const agentForQldb = new Agent({ keepAlive: true, maxSockets: maxConcurrentTransactions }); qldbClientOptions.httpOptions = { agent: agentForQldb }; const retryConfig = new RetryConfig(retryLimit); this.qldbDriver = new QldbDriver(ledgerName, qldbClientOptions, maxConcurrentTransactions, retryConfig); this.qldbSession = null; this.tableNames = null; this.getTableNames = this.getTableNames.bind(this); this.create = this.create.bind(this); this.buildDoc = this.buildDoc.bind(this); } /** * The tableNames function of the QLDB driver is called to test if the ledger is available, the credentials are okay * and general access to the tables is granted. * * @returns {Promise<null>} */ async getTableNames() { this.tableNames = await this.qldbDriver.getTableNames().then((result) => { if (result) { return result.toString().split(","); } return null; }); return this.tableNames; } /** * Gets created indexes list of specified table * * @param tableName * @returns {Promise<Result>} */ async getIndexes(tableName) { const statement = `SELECT indexes FROM information_schema.user_tables WHERE name = '${tableName}'`; return this.executeStatement(statement).then((result) => { if ((result) && (result.length > 0)) { return this.createObjectFromResults(result, true); } return null; }) } /** * Checks if a index exists in specified table and if not, creates that index. limit is 5 indexes per table. *The function works but slows down the add operation and may results in timeouts if you specify 5 or more indexes on model fields * @param tableName * @param indexName * @returns {Promise<Result>} */ async checkIfIndexExists(tableName, indexName) { try { const tableIndexes = await this.getIndexes(tableName); if (tableIndexes) { const { indexes } = tableIndexes; const indexList = indexes.map(val => { return val.expr.toUpperCase(); }) if (indexList.indexOf(`[${indexName.toUpperCase()}]`) == -1) { const statement = 'CREATE INDEX ON ' + tableName + '(' + indexName + ')'; return this.executeStatement(statement).then((result) => { if ((result) && (result.length > 0)) { return this.createObjectFromResults(result, true); } return false; }); } return true } return false } catch (err) { throw new Error(`Failed to create index on table: ${err}`) } } /** * Create a new record based on the model. The model holds the structure and all the data needed. * * @param tableName * @param model * @returns {Promise<Result>} */ async create(tableName,model) { try { return this.buildSqlInsert(tableName, model).then((sqlBuilder) => { return this.executeStatement(sqlBuilder).then((result) => { if (result) { return this.createObjectFromResults(result); } return false; }) }); } catch (err) { throw new Error('Create record failed', err); } finally { await this.closeSession() } } /** * Update an existing record with the data in the model * * @param tableName * @param model * @returns {Promise<Result>} */ async update(tableName, args, model) { return this.buildSqlUpdate(tableName, args, model).then((sqlBuilder) => { return this.executeStatement(sqlBuilder).then((result) => { if (result) { return this.createObjectFromResults(result); } return false; }) }); } /** * Finds one record by the supplied arguments. The param args consists of args.fields and args.where. The arg.fields variable * holds the names of the fields to be retrieved. The variable args.where holds the conditions that need to be met. * * @param tableName * @param model * @param args * @returns {Promise<Result>} */ async findOneBy(tableName, model, args) { return this.buildSqlSelect(tableName, model, args).then((sqlBuilder) => { return this.executeStatement(sqlBuilder).then((result) => { if ((result) && (result.length > 0)){ return this.createObjectFromResults(result, true); } return false; }) }); } /** * Find records by the supplied arguments. The param args consists of args.fields and args.where. The arg.fields variable * holds the names of the fields to be retrieved. The variable args.where holds the conditions that need to be met. * * @param tableName * @param model * @param args * @returns {Promise<Result>} */ async findBy(tableName, model, args) { return this.buildSqlSelect(tableName, model, args).then((sqlBuilder) => { return this.executeStatement(sqlBuilder).then((result) => { if (result) { let objectResult = this.createObjectFromResults(result); if (args.order) { objectResult = this.fakeOrdering(objectResult, args); } if (args.limit) { objectResult = this.fakePagination(objectResult, args); } return objectResult; } return false; }) }); } /** * Delete a record from the QLDB ledger. The option {recursive: true} in the arguments will delete linked entries * that are defined in the model as LEDGER. * * @param tableName * @param model * @param args * @returns {Promise<Reader[]>} */ async delete(tableName, model, args) { return this.buildSqlDelete(tableName, model, args).then((sqlBuilder) => { if (sqlBuilder == false) { return 'unsafe delete with empty where statement.'; } let results = []; sqlBuilder.split(';').forEach(sqlStatement => { if (sqlStatement.length == 0) { return; } let result = this.executeStatement(sqlStatement).then((result) => { return result; }) results.push(result); }) return results; }); } /** * Get data from the committed table data. The tableName is automatically prepended with the '_ql_committed_'. The args * have a similar build as the regular findBy and findOneBy functions. * * @param tableName * @param args * @returns {Promise<Result>} */ async findCommittedData(tableName, args) { return this.buildMetaSqlSelect(tableName, args).then((sqlBuilder) => { return this.executeStatement(sqlBuilder).then((result) => { if (result) { return this.createObjectFromResults(result); } return false; }) }); } /** * Get document revision from the committed table with metadata and hashes filter. The tableName is automatically prepended with the '_ql_committed_'. The args * have a similar build as the regular findBy and findOneBy functions. * * @param tableName * @param args * @returns {Promise<Result>} */ async findCommittedDocument(tableName, args) { return this.buildCommittedDocumentMetaSqlSelect(tableName, args).then((sqlBuilder) => { return this.executeStatement(sqlBuilder).then((result) => { if (result) { return this.createObjectFromResults(result); } return false; }) }); } /** * Get the historic data and changes for any document. This function is called by getHistoryByDocumentId and * getHistoryByPk. The args section defines if the query is fun on the metadat or on the data of the history. * const args = { * where: whereArgs, * useMetaData: false, * startDate: startDate, * endDate: endDate * } * Where the whereArgs are {<primary_key_name>: id} or {id: <document_id>>} * The value useMetaData indicates if the search is done in the data or metaData fields * * @param tableName * @param args * @returns {Promise<Reader[]>} */ async getHistory(tableName, args ) { return this.buildHistorySqlSelect(tableName, args).then((sqlBuilder) => { return this.executeStatement(sqlBuilder).then((result) => { if (result) { let objectResult = this.createObjectFromResults(result); objectResult.sort((a, b) => { return b.metadata.version - a.metadata.version }) if (args.limit) { objectResult = this.fakePagination(objectResult, args); } return objectResult; } return false; }) }); } /** * Builds the SQL query for selects. Based on the model Joins are created and the selected fields are build. * Set `useDocumentId: true` to get documentId in result and search records with documentId by adding documentId in where clause. * * @param tableName * @param model * @param args object with a .where that holds the search/filter information and a .fields array that folds the fields * that should be returned. * @returns string} */ async buildSqlSelect(tableName, model, args) { // Build the SELECT part of the query const selectNames = await this.prepareNames(tableName, model, (args.fields) ? args.fields : null); let sqlSelectFields = selectNames.fieldNames.join(', '); let onStatement = ''; if (selectNames.joinStatement.length > 0) { onStatement = selectNames.joinStatement.join(' ') } // Get the WHERE part for the query const sqlWhere = await this.createSqlWhere(args, tableName, model); if (args.useDocumentId) { sqlSelectFields += ', documentId' return 'SELECT ' + sqlSelectFields + ' FROM ' + tableName + ' BY documentId ' + onStatement + sqlWhere + ';'; } return 'SELECT ' + sqlSelectFields + ' FROM ' + tableName + onStatement + sqlWhere + ';'; } /** * Build the UPDATE part of the query * * @param tableName * @param args object with a .where that holds the filter conditions and a .fields array that folds the fields * that should be updated. There is no check if the updated data if is different from the existing one. * @returns string} */ async buildSqlUpdateRecursive(tableName, args, model) { let sqlUpdate = ''; let sqlLinked = ''; for (const [fieldName, fieldValue] of Object.entries(args.fields)) { if (!model[fieldName]) { throw new Error(`Field name '${fieldName}' is not defined for table '${tableName}'`); } // Test if the update field is a linked ledger let fieldType = model[fieldName].type.name.toLowerCase() if (fieldType == 'ledger') { const pkValue = await this.getLinkedLedgerPkValue(tableName, fieldName, args.where); const pkName = model[fieldName].model.primaryKey; const subArgs = { fields: fieldValue, where: {} } subArgs.where[pkName] = pkValue; sqlLinked = await this.buildSqlUpdate(model[fieldName].model.tableName, subArgs, model[fieldName].model.model); this.executeRawSQLAndForget(sqlLinked); } else if ((Array.isArray(model[fieldName].value)) || (fieldType == 'json')) { const newValue = arrayStringify(fieldValue); sqlUpdate = sqlUpdate + tableName + "." + fieldName + " = " + newValue + " , "; } else if ((fieldType == 'number') || (fieldType == 'int')) { sqlUpdate = sqlUpdate + tableName + "." + fieldName + " = " + fieldValue + " , "; } else if (fieldType == 'object') { const currentLayer = tableName + "." + fieldName; const sqlRecursive = await this.buildSqlUpdateRecursive(currentLayer, {fields: fieldValue}, model[fieldName].model); sqlUpdate = sqlUpdate + sqlRecursive; } else { sqlUpdate = sqlUpdate + tableName + "." + fieldName + " = '" + fieldValue + "' , "; } } return sqlUpdate } /** * Builds the SQL query for updates. Based on the model, no nested updates supported. * Set `useDocumentId: true` to update records with documentId and add `documentId` attribute in where clause. * * @param tableName * @param args object with a .where that holds the filter conditions and a .fields array that folds the fields * that should be updated. There is no check if the updated data if is different from the existing one. * @returns string} */ async buildSqlUpdate(tableName, args, model) { const sqlUpdate = await this.buildSqlUpdateRecursive(tableName, args, model); // Create the WHERE condition for the update const sqlWhere = await this.createSqlWhere(args, tableName, model); if (args.useDocumentId) { return 'UPDATE ' + tableName + ' BY documentId' + ' SET ' + sqlUpdate.substr(0, sqlUpdate.length - 2) + sqlWhere + ";"; } return 'UPDATE ' + tableName + ' SET ' + sqlUpdate.substr(0, sqlUpdate.length - 2) + sqlWhere + ";"; } /** * Builds the SQL for the deletion of records. * Set `useDocumentId: true` to delete records with documentId and add `documentId` attribute in where clause. * * @param tableName * @param args * @param model * @returns {Promise<string>} */ async buildSqlDelete(tableName, model, args) { const recursive = (args.recursive === true) ? true : false; // Get the WHERE part for the query const sqlWhere = await this.createSqlWhere(args, tableName, model); if (sqlWhere.length == 0) { return false; } let deleteSql = 'DELETE FROM ' + tableName + sqlWhere + ';'; if (args.useDocumentId) { deleteSql = 'DELETE FROM ' + tableName + ' BY documentId' + sqlWhere + ';'; } if (recursive === false) { return deleteSql; } for (const [fieldName, fieldValue] of Object.entries(model)) { // Test if the update field is a linked ledger let fieldType = model[fieldName].type.name.toLowerCase(); if (fieldType == 'ledger') { // Get the PK-name and PK-value of the linked ledger, use the where args only from the original call. const pkValue = await this.getLinkedLedgerPkValue(tableName, fieldName, args.where); const pkName = model[fieldName].model.primaryKey; let subArgs = { recursive: recursive, where: [] }; subArgs.where[pkName] = pkValue; const subSQL = await this.buildSqlDelete(model[fieldName].model.tableName, model[fieldName].model.model, subArgs); deleteSql += subSQL; } } return deleteSql; } async getLinkedLedgerPkValue(tableName, pkFieldName, args) { let sqlWhere = ''; for (const [fieldName, fieldValue] of Object.entries(args)) { sqlWhere += fieldName + '= \''+ fieldValue +'\' AND '; } let sql = 'SELECT '+ pkFieldName +' FROM '+ tableName +' WHERE '+ sqlWhere.substr(0, sqlWhere.length - 4) +' ;'; const linkedLedger = await this.executeRawSQL(sql); return linkedLedger[0][pkFieldName]; } async executeRawSQL(sql) { return this.executeStatement(sql).then((result) => { if (result) { return this.createObjectFromResults(result); } return false; }) } async executeRawSQLAndForget(sql) { return this.executeStatement(sql).then((result) => { return true; }) } /** * Build the WHERE part and the ionize part for the query * * @param args * @param tableName * @param model * @returns {string} */ createSqlWhere(args, tableName, model) { // Build the WHERE part and the ionize part for the query let sqlWhereOR = ''; let sqlWhere = ''; const operatorKeys = Operators.getOperatorNames(); // creating sqlwhere by documentId if (args.where && args.where.documentId) { sqlWhere = `documentId = '${args.where.documentId}' AND `; delete args.where.documentId; } // Adding WHERE Clause with OR logical operator if (args.where && args.where.OR) { // Loop over args to see if there are keys that are a ledger and if so combine the table and field(s) name for that key. for (let [key, value] of Object.entries(args.where.OR)) { if ((model[key].type.name.toLowerCase() == 'ledger') && (typeof (value) == 'object')) { for (let [subKey, subValue] of Object.entries(value)) { args.where.OR[(model[key].model.tableName + '.' + subKey).toString()] = subValue; } } else { args.where.OR[(tableName + '.' + key).toString()] = value; } delete args.where.OR[key]; } for (let [key, value] of Object.entries(args.where.OR)) { let operator = Operators.EQ; if ((typeof (value) == 'array') || (typeof (value) == 'object')) { if ((typeof (value[0]) == 'object') && (operatorKeys.indexOf(value[0].name.toUpperCase()) >= 0)) { operator = Operators[value[0].name.toUpperCase()]; value = value[1]; } else { operator = Operators.IN; } } if ((typeof (value) == 'array') || (typeof (value) == 'object')) { let sendValues = []; value.forEach(item => { if (typeof (item) == 'string') { sendValues.push("'" + item + "'"); } else { sendValues.push(item); } }); sqlWhereOR = sqlWhereOR + key + operator.operator + "[" + sendValues.join(',') + "] OR "; } else if (typeof (value) === 'number' || typeof (value) === 'boolean') { sqlWhereOR = sqlWhereOR + key + operator.operator + value + " OR "; } else { sqlWhereOR = sqlWhereOR + key + operator.operator + "'" + value + "' OR "; } } delete args.where.OR } // WHERE Clause with Default AND logical operator if (args.where) { // Loop over args to see if there are keys that are a ledger and if so combine the table and field(s) name for that key. for (let [key, value] of Object.entries(args.where)) { if ((model[key].type.name.toLowerCase() == 'ledger') && (typeof (value) == 'object')) { for (let [subKey, subValue] of Object.entries(value)) { args.where[(model[key].model.tableName + '.' + subKey).toString()] = subValue; } } else { args.where[(tableName + '.' + key).toString()] = value; } delete args.where[key]; } for (let [key, value] of Object.entries(args.where)) { let operator = Operators.EQ; if ((typeof (value) == 'array') || (typeof (value) == 'object')) { if ((typeof (value[0]) == 'object') && (operatorKeys.indexOf(value[0].name.toUpperCase()) >= 0)) { operator = Operators[value[0].name.toUpperCase()]; value = value[1]; } else { operator = Operators.IN; } } if ((typeof (value) == 'array') || (typeof (value) == 'object')) { let sendValues = []; value.forEach(item => { if (typeof (item) == 'string') { sendValues.push("'" + item + "'"); } else { sendValues.push(item); } }); sqlWhere = sqlWhere + key + operator.operator + "[" + sendValues.join(',') + "] AND "; } else if (typeof (value) === 'number' || typeof (value) === 'boolean') { sqlWhere = sqlWhere + key + operator.operator + value + " AND "; } else { sqlWhere = sqlWhere + key + operator.operator + "'" + value + "' AND "; } } sqlWhere = sqlWhere + sqlWhereOR; } if (sqlWhere.length == 0) { sqlWhere = ' WHERE 1 = 1'; // << Functions as a "SELECT ALL" } else { sqlWhere = ' WHERE ' + sqlWhere.substr(0, sqlWhere.length - 4); } return sqlWhere; } /** * NOTE: Not to be used, ORDER BY is not yet supported by PartiQL * create the 'ORDER BY' part of the query. * * @param args */ createSqlOrder (args, tableName) { let sqlOrder = ''; const operatorKeys = Operators.getOperatorNames(); if (args.order) { for (let [key, value] of Object.entries(args.order)) { sqlOrder += tableName + "." + key + ' ' + value + ', ' } sqlOrder = ' ORDER BY ' + sqlOrder.substr(0, sqlOrder.length - 2); } return sqlOrder; } /** * Builds the SQL for the select on the committed data in the QLDB. * * @param tableName * @param args * @returns string */ async buildMetaSqlSelect(tableName, args) { let sqlWhere = ''; if (args.where) { for (const [key, value] of Object.entries(args.where)) { sqlWhere = sqlWhere + key + " = '"+ value +"' AND "; } if (sqlWhere.length == 0) { sqlWhere = ' WHERE 1 = ?'; // << Functions as a "SELECT ALL" } else { sqlWhere = ' WHERE ' + sqlWhere.substr(0, sqlWhere.length - 4); } } return 'SELECT * FROM _ql_committed_' + tableName + sqlWhere + ';'; } /** * Builds the SQL for the select on the committed document with metadata and hashes filtering in the QLDB. * * @param tableName * @param args * @returns string */ async buildCommittedDocumentMetaSqlSelect(tableName, args) { let sqlWhere = ''; if (args.where) { if (args.where.metadata) { for (const [key, value] of Object.entries(args.where.metadata)) { sqlWhere = key === 'version' ? `${sqlWhere} metadata.version = ${value} AND ` : `${sqlWhere} metadata.${key} = '${value}' AND ` } } if (args.where.hash) { sqlWhere = sqlWhere + 'hash = `{{' + args.where.hash + '}}` AND ' } if (args.where.data) { for (const [key, value] of Object.entries(args.where.data)) { sqlWhere = typeof (value) === 'string' ? `${sqlWhere} data.${key} = '${value}' AND ` : `${sqlWhere} data.${key} = ${value} AND `; } } if (sqlWhere.length == 0) { sqlWhere = ' WHERE 1 = 1'; // << Functions as a "SELECT ALL" } else { sqlWhere = ' WHERE ' + sqlWhere.substr(0, sqlWhere.length - 4); } } return 'SELECT * FROM _ql_committed_' + tableName + sqlWhere + ';'; } /** * Builds the SQL for the select on the historic data in the QLDB. * * @param tableName * @param args * @returns string */ async buildHistorySqlSelect(tableName, args) { let sqlWhere = ''; if (args.where && args.where.documentId && args.useDocumentId) { sqlWhere += `documentId = '${args.where.documentId}' AND ` delete args.where.documentId; } if (args.where) { let fieldPrefix = 'h.data.' if (args.useMetaData) { fieldPrefix = 'h.metadata.' } for (const [key, value] of Object.entries(args.where)) { sqlWhere = sqlWhere + fieldPrefix + key + " = '"+ value +"' AND "; } if (sqlWhere.length == 0) { sqlWhere = ' '; // << Functions as a "SELECT ALL" } else { sqlWhere = ' WHERE ' + sqlWhere.substr(0, sqlWhere.length - 4); } } let fromSection = tableName; if (args.startDate) { fromSection += ', `' + args.startDate +'T`'; } if (args.endDate) { fromSection += ', `' + args.endDate +'T`'; } if (args.useDocumentId) { return 'SELECT * FROM history( ' + fromSection + ') AS h BY documentId' + sqlWhere + ';'; } return 'SELECT * FROM history( '+ fromSection +') AS h' + sqlWhere + ';'; } /** * Builds the InsertSQL query. Straight forward insert as nested inserts are processed elsewhere. * * @param tableName * @param model * @returns {Promise string} */ async buildSqlInsert(tableName, model) { // FIXME Works but creates a timeout // if (!this.checkTableExistence(tableName)) { // console.log('table not found and not created'); // } const doc = await this.buildDoc(model); const insertValues = JSON.stringify(doc).replace(/"/ig, "'") return 'INSERT INTO ' + tableName + ' VALUE ' + insertValues ; } /** * Mainly used by the buildSqlInsert function to build the query. Nested/linked tables defined as LEDGER in the model * are inserted if they are new. Existing documentId's of referenced tables are already replaced from the entered data * in the model when the format and data is being checked. The data is not checked again in this function and relies * on the model to do that in the mapDataToModel function. * * @param model * @param data * @returns {Promise<{}>} */ async buildDoc(model, data = false) { let doc = {}; for (const [fieldName, fieldOptions] of Object.entries(model)) { let fieldType = fieldOptions.type.name.toLowerCase() if (!data) { data = model[fieldName].value; } if (fieldType == 'ledger') { // process the values in the linked model doc[fieldName] = await this.processLedgerData(fieldOptions.model, model[fieldName].value); } else if (fieldType == 'object') { doc[fieldName] = await this.buildDoc(fieldOptions.model, data); } else if (Array.isArray(model[fieldName].value)) { let arrayValue = []; for (const element of model[fieldName].value) { // Set the values for each entry in the array let model = null; let result = null; // Enable an array of linked ledger models if ((fieldOptions.model) && (fieldOptions.model.constructor) && (fieldOptions.model.constructor.name.toLowerCase() == 'ledger')) { for (const [key, value] of Object.entries(fieldOptions.model.model)) { if (typeof(element[key]) != 'undefined') { fieldOptions.model.model[key].value = element[key]; } } result = await this.processLedgerData(fieldOptions.model, element); } else if (fieldOptions.model) { for (const [key, value] of Object.entries(fieldOptions.model)) { fieldOptions.model[key].value = element[key]; } result = await this.buildDoc(fieldOptions.model, element); } else { result = element; } arrayValue.push(result); result = null; } doc[fieldName] = arrayValue; } else { if (model[fieldName].value != null) { doc[fieldName] = model[fieldName].value; } } }; return doc; } /** * Process the ledger type elements in the buildDoc process * * @param ledgerModel * @param value * @returns {Promise<*>} */ async processLedgerData(ledgerModel, value) { if (typeof(value) != 'object') { return value; } const result = await this.create(ledgerModel.tableName, ledgerModel.model); return ledgerModel.model[ledgerModel.primaryKey].value; } /** * Checks if a table exists and if not creates that table. The function works but slows down the QLDB connection * resulting in timeouts in further calls. * * @param tableName * @returns {Promise<boolean|Result>} */ async checkTableExistence(tableName) { await this.getTableNames(); //Converting Table Names to uppercase and checking if it exists or not because QLDB Table names are case sensitive const tableNamesUppercase = this.tableNames.map(val => val.toUpperCase()); if (tableNamesUppercase.indexOf(tableName.toUpperCase()) == -1) { await this.qldbDriver.executeLambda(async (txn) => { return await txn.execute('CREATE TABLE ' + tableName).then((result) => { if (result) { return true; } return false; }); }); } return true; } /** * Create the names needed for the SQL query. Field names are prepended with the table name. Optional JOIN statements * are created and joined based on the primary key of the linked ledger. The model contains all the needed * information to create these SQL statement parts. * * @param tableName * @param model * @returns {Promise<{fieldnames: [], joinStatement: []}>} */ async prepareNames(tableName, model, fields = null) { let fieldNames = []; let joinStatement = []; for (const [fieldName, fieldOptions] of Object.entries(model)) { //Check if any of the requested fields is a nested one (DataType:LEDGER) if ((fields == null) || (fields.indexOf(fieldName) != -1)) { if ((fieldOptions.type) && (fieldOptions.type.name.toLowerCase() == 'ledger')) { joinStatement.push(' JOIN ' + fieldOptions.model.tableName + ' ON ' + tableName + '.' + fieldName + '=' + fieldOptions.model.tableName + '.' + fieldOptions.model.primaryKey); fieldNames.push(fieldOptions.model.tableName + ' AS ' + fieldName); } else { fieldNames.push(tableName + '.' + fieldName); } } }; return { fieldNames: fieldNames, joinStatement: joinStatement }; } /** * Creates a human readable object from the results that the QLDB returns. The results is in a 'getResultList' format. * If there is only one result-object only this first object is returned. Else an array of objects is returned. * * @param results * @returns {*} */ createObjectFromResults(results, singleResultReturn = false) { let returnObject = []; if (singleResultReturn) { return JSON.parse(JSON.stringify(results[0])); } results.forEach(result=>{ let record = JSON.parse(JSON.stringify(result)); returnObject.push(record) }); return returnObject; } /** * Unified faster way to get the active session * * @returns {Promise<QldbSession>} */ async getSession() { if (!this.qldbDriver) throw new Error('Ledger init failed'); this.qldbSession = await this.qldbDriver.getSession() return this.qldbSession; } /** * Close the current session * * @returns {Promise<void>} */ async closeSession() { if (null != this.qldbSession) { this.qldbSession.closeSession(); } } /** * Executes the query and returns a binary result list * * @param query * @returns {Promise<Reader[]>} */ async executeStatement(query) { let result; let response; //Handling session till the bug fixed for v2.0 driver. getSession method creates new session if expired before making transactions. hence invalid token exception may not occur. //await this.getSession(); await this.qldbDriver.executeLambda(async (txn) => { result = await txn.execute(query).then((result) => { return result; }); response = result.getResultList(); }); //closing the session after transaction executed await this.closeSession(); return response; } /** * Fake ordering since PartiQL does not support it yet. * * @param object * @param fieldName * @returns {Promise<void>} */ fakeOrdering(object, args) { const fieldNames = Object.keys(args.order); const fieldName = fieldNames[0]; const direction = (args.order[fieldName] == 'asc') ? -1 : 1; object.sort((a, b) => { //sorting numbered fields if (!isNaN(a[fieldName])) { if (args.order[fieldName] === 'asc') { return a[fieldName] - b[fieldName]; } return b[fieldName] - a[fieldName] } //sorting by createdAt/ updatedAt if (fieldName === 'createdAt' || fieldName === 'updatedAt') { if (args.order[fieldName] === 'asc') { return new Date(a[fieldName]) - new Date(b[fieldName]); } return new Date(b[fieldName]) - new Date(a[fieldName]); } else { //sorting alphabet fields let fieldA = a[fieldName].toUpperCase(); // ignore upper and lowercase let fieldB = b[fieldName].toUpperCase(); // ignore upper and lowercase if (fieldA < fieldB) { return direction; } if (fieldA > fieldB) { return direction * -1; } // names must be equal return 0; } }); return object; } /** * Fake pagination since PartiQL does not support it yet. * * @param object * @param args * @returns {*} */ fakePagination(object, args) { const {offset, limit} = args; const results = object.length; let page = object.slice(offset, offset + limit); page.rows = results; return page; } } module.exports = { qldbConnect, }