UNPKG

docorm

Version:

Persistence layer with ORM features for JSON documents

697 lines 35.5 kB
import _ from 'lodash'; import { arrayToDottedPath } from 'schema-fun'; import { Transform } from 'stream'; import { v4 as uuidv4 } from 'uuid'; import * as db from './db.js'; import { PersistenceError } from '../errors.js'; import { queryClauseIsAnd, queryClauseIsFullTextSearch, queryClauseIsNot, queryClauseIsOr, queryExpressionIsCoalesce, queryExpressionIsConstant, queryExpressionIsConstantList, queryExpressionIsFullText, queryExpressionIsFunction, queryExpressionIsOperator, queryExpressionIsPath, queryExpressionIsRange } from '../queries.js'; const ALLOWED_OPERATORS = ['AND', 'OR', 'NOT']; const SQL_TYPES = ['boolean']; export function fetchResultsIsArray(x) { return _.isArray(x); } export function fetchResultsIsStream(x) { return !_.isArray(x); } function sqlExpressionFromQueryExpression(expression, mapping, parameterCount = 0) { if (queryExpressionIsConstant(expression) || queryExpressionIsConstantList(expression)) { if (expression.constant == null) { return { expression: 'NULL', parameterValues: [] }; } else { return { expression: `$${parameterCount + 1}`, parameterValues: [expression.constant] }; } } else if (queryExpressionIsPath(expression)) { return { expression: coerceType(sqlColumnFromPath(expression.path, mapping), expression.sqlType), parameterValues: [] }; } else if (queryExpressionIsFullText(expression)) { // TODO Support named full-text search contexts, like {text: 'default'}, {text: 'ids'}, {text: 'comments'} if (!mapping.jsonColumn) { throw new PersistenceError(`Full-text search cannot be applied to an entity type that lacks a JSON column).`); } return { expression: `${mapping.jsonColumn}::text`, parameterValues: [] }; } else if (queryExpressionIsFunction(expression)) { const subexpressions = []; const parameterValues = []; for (const jsonSubexpression of expression.parameters || []) { const { expression: subexpression, parameterValues: subexpressionParameterValues } = sqlExpressionFromQueryExpression(jsonSubexpression, mapping, parameterCount); if (subexpression) { subexpressions.push(subexpression); } if (subexpressionParameterValues) { Array.prototype.push.apply(parameterValues, subexpressionParameterValues); parameterCount += subexpressionParameterValues.length; } } return { expression: `${expression.function}(${subexpressions.join(', ')})`, parameterValues }; } else if (queryExpressionIsCoalesce(expression)) { const subexpressions = []; const parameterValues = []; for (const jsonSubexpression of expression.coalesce) { const { expression: subexpression, parameterValues: subexpressionParameterValues } = sqlExpressionFromQueryExpression(jsonSubexpression, mapping, parameterCount); if (subexpression) { subexpressions.push(subexpression); } if (subexpressionParameterValues) { Array.prototype.push.apply(parameterValues, subexpressionParameterValues); parameterCount += subexpressionParameterValues.length; } } return { expression: `COALESCE(${subexpressions.join(', ')})`, parameterValues }; } else if (queryExpressionIsOperator(expression)) { if (!ALLOWED_OPERATORS.includes(expression.operator.toUpperCase())) { throw new PersistenceError('Bad JSON query expression: Unknown operator', { expression, operator: expression.operator }); } const subexpressions = []; const parameterValues = []; for (const operatorParameter of expression.parameters || []) { const { expression: subexpression, parameterValues: subexpressionParameterValues } = sqlExpressionFromQueryExpression(operatorParameter, mapping, parameterCount); if (subexpression) { subexpressions.push(subexpression); } if (subexpressionParameterValues) { Array.prototype.push.apply(parameterValues, subexpressionParameterValues); parameterCount += subexpressionParameterValues.length; } } switch (expression.operator.toUpperCase()) { case 'NOT': return { expression: `(${expression.operator.toUpperCase()} ${subexpressions[0]})`, parameterValues }; default: return { // TODO Check whether we need the parentheses here. expression: `(${subexpressions.join(` ${expression.operator.toUpperCase()} `)})`, parameterValues }; } } else if (queryExpressionIsRange(expression)) { if (!_.isArray(expression.range) || expression.range.length != 2) { throw new PersistenceError('Bad JSON query expression', { expression }); } const subexpressions = []; const parameterValues = []; for (const rangePart of expression.range) { const { expression: subexpression, parameterValues: subexpressionParameterValues } = sqlExpressionFromQueryExpression(rangePart, mapping, parameterCount); if (subexpression) { subexpressions.push(subexpression); } if (subexpressionParameterValues) { Array.prototype.push.apply(parameterValues, subexpressionParameterValues); parameterCount += subexpressionParameterValues.length; } } return { expression: `${subexpressions.join(' AND ')}`, parameterValues }; } else { throw new PersistenceError('Bad JSON query expression', { expression }); /* return { expression: null, parameterValues: [] }*/ } } function propertyPathIsString(path) { return typeof (path) == 'string'; } function propertyBlacklistElementFromPath(path) { const pathArray = propertyPathIsString(path) ? propertyPathStringToArray(path, false) : path; return `'{${pathArray.join(',')}}'`; } function propertyBlacklistToPhrase(propertyBlacklist) { return (propertyBlacklist && propertyBlacklist.length > 0) ? propertyBlacklist.map((path) => ` #- ${propertyBlacklistElementFromPath(path)}`).join('') : ''; } function propertyPathStringToArray(pathStr, quoteNonIndexElements) { return _.flatten(pathStr.split('.').map((component) => { const arrayIndices = []; let match = component.match(/^(.*)\[(\d+)\]$/); while (match != null) { component = match[1]; arrayIndices.unshift(_.parseInt(match[2])); match = component.match(/^(.*)\[(\d+)\]$/); } return [quoteNonIndexElements ? `'${component}'` : component, ...arrayIndices]; })); } // TODO Perhaps support arrays etc. in paths. function sqlColumnFromPath(path, mapping) { const propertyMapping = (mapping.propertyMappings || []).find((m) => m.propertyPath == path); if (propertyMapping) { return propertyMapping.column; } if (path == '_id') { return 'id'; } if (!mapping.jsonColumn) { throw new PersistenceError(`Entity type has unmapped properties and lacks a JSON column.`, { propertyPath: path }); } else { const postgresComponents = propertyPathStringToArray(path, true); postgresComponents.unshift(`"${mapping.jsonColumn}"`); return postgresComponents.slice(0, -1).join('->') + '->>' + _.last(postgresComponents); } } function coerceType(sqlExpression, sqlType) { if (sqlType && SQL_TYPES.includes(sqlType.toLowerCase())) { return `(${sqlExpression})::${sqlType.toLowerCase()}`; } return sqlExpression; } function sqlQueryCriteriaClauseFromQueryClause(clause, mapping, parameterCount = 0) { if (clause === false) { // We don't even run the query in this case, but for completeness we will produce correct SQL. return { sqlClause: '0 = 1', parameterValues: [] }; } else if (clause == null || clause === true) { return { sqlClause: null, parameterValues: [] }; } else if (queryClauseIsAnd(clause) || queryClauseIsOr(clause)) { const subclauses = queryClauseIsAnd(clause) ? clause.and : clause.or; const joinOperator = queryClauseIsAnd(clause) ? 'AND' : 'OR'; const sqlSubclauses = []; const parameterValues = []; for (const subclause of subclauses) { const { sqlClause: sqlSubclause, parameterValues: subclauseParameterValues } = sqlQueryCriteriaClauseFromQueryClause(subclause, mapping, parameterCount); sqlSubclauses.push(sqlSubclause); if (subclauseParameterValues) { Array.prototype.push.apply(parameterValues, subclauseParameterValues); parameterCount += subclauseParameterValues.length; } } if (sqlSubclauses.length == 0) { if (queryClauseIsAnd(clause)) { // Always true return { sqlClause: null, parameterValues: [] }; } else { // Always false return { sqlClause: '0 = 1', parameterValues: [] }; } } else if (sqlSubclauses.includes(null) && queryClauseIsOr(clause)) { // Always true return { sqlClause: null, parameterValues: [] }; } else { return { sqlClause: '(' + sqlSubclauses.filter((c) => c != null).join(`) ${joinOperator} (`) + ')', parameterValues }; } } else if (queryClauseIsNot(clause)) { const subclause = clause.not; const { sqlClause: sqlSubclause, parameterValues } = sqlQueryCriteriaClauseFromQueryClause(subclause, mapping, parameterCount); if (sqlSubclause) { return { sqlClause: `NOT (${sqlSubclause})`, parameterValues }; } else { // Always false return { sqlClause: '0 = 1', parameterValues: [] }; } //} else if (queryClauseIsFullTextSearch(clause)) { } else { if (!queryClauseIsFullTextSearch(clause)) { // TODO The condition below used to be queryExpressionIsConstant(clause.l) && !queryExpressionIsConstant(clause.r). if (queryExpressionIsConstant(clause.l) && queryExpressionIsPath(clause.r)) { const swapTemp = clause.l; clause.l = clause.r; clause.r = swapTemp; } } const { expression: leftExpression = null, parameterValues: leftExpressionParameterValues = null } = clause.l ? sqlExpressionFromQueryExpression(clause.l, mapping, parameterCount) : {}; parameterCount += leftExpressionParameterValues ? leftExpressionParameterValues.length : 0; const { expression: rightExpression = null, parameterValues: rightExpressionParameterValues = null } = clause.r ? sqlExpressionFromQueryExpression(clause.r, mapping, parameterCount) : {}; parameterCount += rightExpressionParameterValues ? rightExpressionParameterValues.length : 0; let operator = null; let rightWrapper = _.identity; let clauseWrapper = _.identity; if (leftExpression && rightExpression) { switch (clause.operator) { case 'contains': if ((queryExpressionIsPath(clause.l) || queryExpressionIsFullText(clause.l)) && queryExpressionIsConstant(clause.r) && (clause.r.constant != null)) { operator = 'ILIKE'; rightWrapper = (expr) => `'%' || regexp_replace(${expr}, '([%_])', '\\\\\\1', 'g') || '%'`; } // TODO Maybe we can support cases where the left side is constant: '"CONSTANTSTRING" contains column' break; case 'like': operator = 'LIKE'; break; case '=': case undefined: operator = '='; if (queryExpressionIsPath(clause.l) && queryExpressionIsConstant(clause.r) && (clause.r.constant == null)) { operator = 'IS'; /* // We don't really need to use @? to allow the case where the path doesn't exist, and using @? slows the query // unless we have a GIN index on the JSON column. clauseWrapper = (clause) => `(NOT data @? '$.${jsonClause.l.path}') OR (${clause})` */ } if (queryExpressionIsPath(clause.r) && queryExpressionIsConstant(clause.l) && (clause.l.constant == null)) { operator = 'IS'; /* // We don't really need to use @? to allow the case where the path doesn't exist, and using @? slows the query // unless we have a GIN index on the JSON column. clauseWrapper = (clause) => `(NOT data @? '$.${jsonClause.r.path}') OR (${clause})` */ } break; case '!=': operator = clause.operator; if (queryExpressionIsPath(clause.l) && queryExpressionIsConstant(clause.r) && (clause.r.constant == null)) { operator = 'IS NOT'; /* // We don't really need to use @? to check that the path exists, and using @? slows the query unless we have a // GIN index on the JSON column. clauseWrapper = (clause) => `(data @? '$.${jsonClause.l.path}') AND (${clause})` */ } break; case '~': case '>=': case '>': case '<': case '<=': operator = clause.operator; break; case 'in': operator = '='; rightWrapper = (expr) => `ANY (${expr})`; // if (jsonExpressionIsPath(jsonClause.l) && if (queryExpressionIsConstantList(clause.r) && clause.r.constant.includes(null)) { clauseWrapper = (sqlClause) => `((${leftExpression} IS NULL) OR (${sqlClause}))`; /* // We don't really need to use @? to allow the case where the path doesn't exist, and using @? slows the query // unless we have a GIN index on the JSON column. clauseWrapper = (clause) => `((NOT data @? '$.${jsonClause.l.path}') OR (${leftExpression} IS NULL) OR (${clause}))` */ } break; case 'between': if (!queryExpressionIsRange(clause.r)) { // TODO Use TypeScript type predicate on the whole clause to rule this case out. throw new PersistenceError('Bad JSON query clause', { clause }); } operator = 'BETWEEN'; break; default: operator = null; } } // TODO Before, we only checked if (operator). if (operator && leftExpression && rightExpression) { return { sqlClause: clauseWrapper(`${leftExpression} ${operator} ${rightWrapper(rightExpression)}`), parameterValues: [].concat(leftExpressionParameterValues || []).concat(rightExpressionParameterValues || []) }; } else { return { sqlClause: null, parameterValues: [] }; } } } function makeSqlQueryCriteriaClauses(entityType, query) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; const entityTypeClause = mapping.jsonColumn ? `"${mapping.jsonColumn}"->>\'_type\' = $1` : undefined; const parameterValues = mapping.jsonColumn ? [entityType.name] : []; const { sqlClause: querySqlClause = null, parameterValues: queryParameterValues } = sqlQueryCriteriaClauseFromQueryClause(query, entityType.mapping, parameterValues.length); Array.prototype.push.apply(parameterValues, queryParameterValues); const clauses = [entityTypeClause, querySqlClause].filter(Boolean); return { sqlClause: clauses.length > 0 ? '(' + clauses.join(') AND (') + ')' : null, parameterValues }; } function makeSqlQueryCriteriaClausesFromRawWhereClause(entityType, whereClause, whereClauseParameters) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; const entityTypeClause = mapping.jsonColumn ? `"${mapping.jsonColumn}"->>\'_type\' = $${whereClauseParameters.length + 1}` : undefined; const parameterValues = [...whereClauseParameters, ...mapping.jsonColumn ? [entityType.name] : []]; return { sqlClause: '(' + [entityTypeClause, whereClause].filter(Boolean).join(') AND (') + ')', parameterValues }; } function makeQueryOrderPhrase(entityType, order) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if ((order == null) || (order.length == 0)) { return ''; } const orderPhrases = _.map(order, (orderElement) => { const path = _.isArray(orderElement) ? orderElement[0] : orderElement; const direction = (_.isArray(orderElement) && (orderElement.length > 1) && ['asc', 'desc'].includes(orderElement[1].toString().toLowerCase())) ? orderElement[1].toString().toLowerCase() : 'asc'; return `${sqlExpressionFromQueryExpression(path, mapping).expression} ${direction}`; }); return ` ORDER BY ${orderPhrases.join(', ')}`; } function getMappedQueryColumns(mapping, propertiesToExclude) { const propertyPathsToExclude = propertiesToExclude.map((p) => propertyPathIsString(p) ? p : arrayToDottedPath(p)); return (mapping.propertyMappings || []) .filter((m) => !propertyPathsToExclude.includes(m.propertyPath)) .map((m) => m.column); } function sqlFetchColumnList(mapping, propertiesToExclude) { const propertyBlacklistPhrase = propertyBlacklistToPhrase(propertiesToExclude); return [ mapping.idColumn, ...mapping.jsonColumn ? [`${mapping.jsonColumn}${propertyBlacklistPhrase} AS _docorm_data`] : [], ...getMappedQueryColumns(mapping, propertiesToExclude) ].join(', '); } function rowToEntity(row, mapping) { const entity = { ...row._docorm_data || {}, _id: row[mapping.idColumn] }; for (const m of mapping.propertyMappings || []) { _.set(entity, m.propertyPath, row[m.column]); } return entity; } /** Default options for calls to fetch. */ const FETCH_DEFAULT_OPTIONS = { idsOnly: false }; const makeRawDao = function (entityType) { return { entityType, count: async function (query, options = {}) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`count failed because type "${entityType.name} has no table.`); } const { client } = options; if (query === false) { return 0; } const { sqlClause: clause, parameterValues } = makeSqlQueryCriteriaClauses(entityType, query); const whereClause = clause ? ` WHERE ${clause}` : ''; const { rows } = await db.query(`SELECT count(*) AS count FROM "${mapping.table}"${whereClause}`, parameterValues, client); return rows[0].count; }, fetch: async function (query, options = FETCH_DEFAULT_OPTIONS) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`fetch failed because type "${entityType.name}" has no table.`); } if (query === false) { return []; } const columns = sqlFetchColumnList(mapping, options.propertyBlacklist || []); const offsetPhrase = options.offset ? ` OFFSET ${options.offset}` : ''; const limitPhrase = options.limit ? ` LIMIT ${options.limit}` : ''; const { sqlClause: clause, parameterValues } = makeSqlQueryCriteriaClauses(entityType, query); const whereClause = clause ? ` WHERE ${clause}` : ''; const orderPhrase = makeQueryOrderPhrase(entityType, options.order); if (options.stream) { const rowToItem = new Transform({ objectMode: true, transform: (row, _, callback) => callback(null, rowToEntity(row, mapping)) }); const queryStream = await db.queryStream(`SELECT ${columns} FROM "${mapping.table}"${whereClause}${orderPhrase}${offsetPhrase}${limitPhrase}`, parameterValues, options.client); return { run: queryStream.run, stream: queryStream.stream.pipe(rowToItem) }; // return resultsStream.pipe(rowToItem) } else { const { rows } = await db.query(`SELECT ${columns} FROM "${mapping.table}"${whereClause}${orderPhrase}${offsetPhrase}${limitPhrase}`, parameterValues, options.client); return rows.map((row) => rowToEntity(row, mapping)); } }, fetchWithSql: async function (whereClauseSql = null, whereClauseParameters = [], options = FETCH_DEFAULT_OPTIONS) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`fetchWithSql failed because type "${entityType.name} has no table.`); } const columns = sqlFetchColumnList(mapping, options.propertyBlacklist || []); const offsetPhrase = options.offset ? ` OFFSET ${options.offset}` : ''; const limitPhrase = options.limit ? ` LIMIT ${options.limit}` : ''; const { sqlClause: clause, parameterValues } = makeSqlQueryCriteriaClausesFromRawWhereClause(entityType, whereClauseSql, whereClauseParameters); const whereClause = clause ? ` WHERE ${clause}` : ''; const orderPhrase = makeQueryOrderPhrase(entityType, options.order); const { rows } = await db.query(`SELECT ${columns} FROM "${mapping.table}"${whereClause}${orderPhrase}${offsetPhrase}${limitPhrase}`, parameterValues, options.client); return rows.map((row) => rowToEntity(row, mapping)); }, fetchAll: async function (options = FETCH_DEFAULT_OPTIONS) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`fetchAll failed because type "${entityType.name} has no table.`); } const columns = sqlFetchColumnList(mapping, options.propertyBlacklist || []); const offsetPhrase = options.offset ? ` OFFSET ${options.offset}` : ''; const limitPhrase = options.limit ? ` LIMIT ${options.limit}` : ''; const whereClause = mapping.jsonColumn ? ` WHERE "${mapping.jsonColumn}"->>\'_type\' = $1` : ''; const parameterValues = mapping.jsonColumn ? [entityType.name] : []; const orderPhrase = makeQueryOrderPhrase(entityType, options.order); if (options.stream) { const rowToItem = new Transform({ objectMode: true, transform: (row, _, callback) => callback(null, rowToEntity(row, mapping)) }); const queryStream = await db.queryStream(`SELECT ${columns} FROM "${mapping.table}"${whereClause}${orderPhrase}${offsetPhrase}${limitPhrase}`, parameterValues, options.client); return { run: queryStream.run, stream: queryStream.stream.pipe(rowToItem) }; } else { const { rows } = await db.query(`SELECT ${columns} FROM "${mapping.table}"${whereClause}${orderPhrase}${offsetPhrase}${limitPhrase}`, parameterValues, options.client); return rows.map((row) => rowToEntity(row, mapping)); } }, fetchById: async function (ids, options = FETCH_DEFAULT_OPTIONS) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`fetchById failed because type "${entityType.name} has no table.`); } const columns = sqlFetchColumnList(mapping, options.propertyBlacklist || []); const offsetPhrase = options.offset ? ` OFFSET ${options.offset}` : ''; const limitPhrase = options.limit ? ` LIMIT ${options.limit}` : ''; const orderPhrase = makeQueryOrderPhrase(entityType, options.order); const { rows } = await db.query(`SELECT ${columns} FROM "${mapping.table}" WHERE "${mapping.idColumn}" = ANY($1)` + (mapping.jsonColumn ? ` AND "${mapping.jsonColumn}"->>\'_type\' = $2` : '') + `${orderPhrase}${offsetPhrase}${limitPhrase}`, [ids, ...mapping.jsonColumn ? [entityType.name] : []], options.client); return rows.map((row) => rowToEntity(row, mapping)); }, fetchOneById: async function (id, options = {}) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`fetchOneById failed because type "${entityType.name} has no table.`); } const columns = sqlFetchColumnList(mapping, options.propertyBlacklist || []); const { rows } = await db.query(`SELECT ${columns} FROM "${mapping.table}" WHERE "${mapping.idColumn}" = $1` + (mapping.jsonColumn ? ` AND "${mapping.jsonColumn}"->>\'_type\' = $2` : ''), [id, ...mapping.jsonColumn ? [entityType.name] : []], options.client); if (rows.length == 1) { return rowToEntity(rows[0], mapping); } else { return null; } }, // TODO Add support for mapped columns. insertMultipleItems: async function (items) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`insertMultipleItems failed because type "${entityType.name} has no table.`); } if (items.length > 0) { const columns = [ mapping.idColumn, ...mapping.jsonColumn ? [mapping.jsonColumn] : [] ]; const rows = items.map((item) => { if (item._id) { return { [mapping.idColumn]: item._id, ...mapping.jsonColumn ? { [mapping.jsonColumn]: _.merge(_.omit(item, '_id'), { _type: entityType.name }) } : {} }; } else { return { [mapping.idColumn]: uuidv4(), ...mapping.jsonColumn ? { [mapping.jsonColumn]: _.merge(item, { _type: entityType.name }) } : {} }; } }); await db.insertMultipleRows(mapping.table, columns, rows); } }, // TODO Add support for mapped columns. insert: async function (item, options = {}) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`insert failed because type "${entityType.name} has no table.`); } if (!item._id) { item._id = uuidv4(); } const id = item._id; item._id = undefined; item._type = entityType.name; const columns = [ mapping.idColumn, ...mapping.jsonColumn ? [mapping.jsonColumn] : [] ]; const parameters = [ '$1', ...mapping.jsonColumn ? ['$2'] : [] ]; const parameterValues = [ id, ...mapping.jsonColumn ? [item] : [] ]; await db.query(`INSERT INTO "${mapping.table}"` + ` (${columns.map((c) => `"${c}"`).join(', ')})` + ` VALUES (${parameters.join(', ')})`, parameterValues, options.client); item._id = id; return item; }, // TODO Add support for mapped columns. update: async function (item, options = {}) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`update failed because type "${entityType.name} has no table.`); } const id = item._id; item._id = undefined; item._type = entityType.name; const parameterValues = [ id, ...mapping.jsonColumn ? [item, entityType.name] : [] ]; if (mapping.jsonColumn) { await db.query(`UPDATE "${mapping.table}" SET "${mapping.jsonColumn}" = $2` + ` WHERE "${mapping.idColumn}" = $1 AND "${mapping.jsonColumn}"->>'_type' = $3`, parameterValues, options.client); } item._id = id; return item; }, // TODO Add support for mapped columns. updateMultipleItems: async function (items) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`updateMultipleItems failed because type "${entityType.name} has no table.`); } const columnsToUpdate = [ ...mapping.jsonColumn ? [mapping.jsonColumn] : [] ]; const rows = items.map((item) => { if (item._id) { return { [mapping.idColumn]: item._id, ...mapping.jsonColumn ? { [mapping.jsonColumn]: _.merge(_.omit(item, '_id'), { _type: entityType.name }) } : {} }; } else { return null; } }).filter((row) => row !== null); await db.updateMultipleRows(mapping.table, mapping.idColumn, columnsToUpdate, rows); }, deleteOneById: async function (id, options = {}) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`deleteById failed because type "${entityType.name} has no table.`); } const parameterValues = [ id, ...mapping.jsonColumn ? [entityType.name] : [] ]; if (mapping.jsonColumn) { await db.query(`DELETE FROM "${mapping.table}" WHERE "${mapping.idColumn}" = $1 and "${mapping.jsonColumn}"->>'_type' = $2`, parameterValues, options.client); } }, delete: async function (query, options = {}) { if (!entityType.mapping) { throw new PersistenceError(`Cannot make SQL query for an unmapped entity type (${entityType.name})).`, { entityTypeName: entityType.name }); } const mapping = entityType.mapping; if (!mapping.table) { throw new PersistenceError(`delete failed because type "${entityType.name} has no table.`); } const { sqlClause: clause, parameterValues } = makeSqlQueryCriteriaClauses(entityType, query); const whereClause = clause ? ` WHERE ${clause}` : ''; if (whereClause.length < 1) { throw Error(`Attempt to delete all records from table ${mapping.table}`); } await db.query(`DELETE FROM "${mapping.table}"${whereClause}`, parameterValues, options.client); } }; }; export default makeRawDao; //# sourceMappingURL=raw-dao.js.map