UNPKG

@knorm/knorm

Version:

A JavaScript ORM written using ES6 classes

1,294 lines (1,293 loc) 50.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_1 = require("lodash"); const sql_bricks_1 = __importDefault(require("sql-bricks")); const Model_1 = require("./Model"); const Connection_1 = require("./Connection"); const QueryError_1 = require("./QueryError"); const NoRowsError_1 = require("./NoRowsError"); const Where_1 = require("./Where"); const isArray = Array.isArray; const isObject = (value) => typeof value === 'object' && value !== null; const isString = (value) => typeof value === 'string'; const isEmpty = (value) => (isArray(value) && !value.length) || (isObject(value) && !Object.keys(value).length); /** * Creates and runs queries and parses any data returned. */ class Query { /** * Creates a new Query instance. * * @param {Model} model * * @todo use short table alias except in strict/debug mode */ constructor(model) { if (!model) { throw new this.constructor.QueryError('no model provided'); } if (!(model.prototype instanceof Model_1.Model)) { throw new this.constructor.QueryError('model should be a subclass of `Model`'); } const modelConfig = model.config; const table = modelConfig.table; if (!table) { throw new this.constructor.QueryError(`\`${model.name}.table\` is not set`); } const knorm = this.constructor.knorm; this.model = model; this.options = { debug: knorm.config.debug }; this.config = { table, index: 0, alias: table, schema: modelConfig.schema, primary: modelConfig.primary, fields: modelConfig.fields, fieldsToColumns: modelConfig.fieldsToColumns, fieldNames: modelConfig.fieldNames, notUpdated: modelConfig.notUpdated, unique: modelConfig.unique, }; } // TODO: add Query.prototype.reset // TODO: add Query.prototype.schema: set schema // TODO: add Query.prototype.alias: set alias - would be used by @knorm/relations clone() { const clone = new this.constructor(this.model); clone.config = Object.assign(clone.config, this.config); clone.options = Object.assign(clone.options, this.options); return clone; } setOption(option, value) { this.options[option] = value; return this; } addOption(option, value) { this.options[option] = this.options[option] || []; this.options[option].push(value); return this; } appendOption(option, value) { if (!isArray(value)) { value = [value]; } this.options[option] = this.options[option] || []; this.options[option] = this.options[option].concat(...value); return this; } unsetOption(option) { this.options[option] = undefined; return this; } unsetOptions(options) { options.forEach((option) => { this.options[option] = undefined; }); return this; } getOption(option) { return this.options[option]; } hasOption(option) { return this.options[option] !== undefined; } debug(debug = true) { return this.setOption('debug', !!debug); } require(require = true) { return this.setOption('require', !!require); } /** * Configures the batch-size for {@link Query#insert} and {@link Query#update} * (where batch updates are supported). When a batch-size is configured and * either of these operations is called with an array of data, multiple * queries will be sent to the database instead of a single one. If any data * is returned from the queries, it is merged into a single array instead of * returning multiple arrays. * * ::: tip INFO * The queries are sent to the database in parallel (i.e. via `Promise.all`). * Take that into consideration when deciding how many queries to send vs how * many items to have in a single query. * ::: * * ::: warning NOTE * When using this option, the order of the items in the array returned is * unlikely to match the order of the rows in the original array. This is * because the queries are sent in parallel and are not guaranteed to complete * in the same order. * ::: * * @param {number} batchSize The number of items to send in a single INSERT * or UPDATE query (where array updates are supported). * * @returns {Query} The same {@link Query} instance to allow chaining. */ batchSize(batchSize) { return this.setOption('batchSize', parseInt(batchSize)); } /** * Configures whether or not to return the first item in a result set from the * database from a {@link Query#fetch}, {@link Query#insert}, * {@link Query#update} or {@link Query#delete} operation, instead of * returning an array. This is handy when one is sure that there's only one * item in the rows returned from the database. * * @param {boolean} [first=true] If `true`, return the first item, else return * an array. * * @returns {Query} The same {@link Query} instance to allow chaining. */ first(first = true) { return this.setOption('first', !!first); } // TODO: use short field aliases expect in strict/debug mode // TODO: strict mode: throw if the field is not a valid model field addFields(fields) { if (fields[0] === false) { this.options.fields = false; return this; } this.options.fields = this.options.fields || {}; fields.forEach((field) => { if (isArray(field)) { field.forEach((field) => { this.options.fields[field] = field; }); } else if (isObject(field)) { this.options.fields = Object.assign(this.options.fields, field); } else if (isString(field)) { this.options.fields[field] = field; } }); return this; } // TODO: remove support for var-args // TODO: remove support for fields altogether? distinct(...fields) { if (fields[0] === false) { this.setOption('distinct', false); } else { this.setOption('distinct', true); } return this.addFields(fields); } /** * Configures what fields to return from a database call. * * ::: tip INFO * This is also aliased as {@link Query#returning}. * ::: * * @param {string|array|object|boolean} fields The fields to return. When * passed as an object, the keys are used as aliases while the values are used * in the query, which allows one to use raw SQL. When passed as `false`, no * fields will be returned from the database call. * * @example Using raw SQL for PostgreSQL: * ```js{10} * Model.insert( * { * firstName: 'Foo', * lastName: 'Bar', * }, * { * returning: { * firstName: 'firstName', * lastName: 'lastName', * fullNames: Model.query.sql(`"firstName" || ' ' || upper("lastName")`) * } * } * ); * ``` * * @returns {Query} The same {@link Query} instance to allow chaining. */ fields(...fields) { return this.addFields(fields); } /** * Configures what fields to return from a database call. * * ::: tip INFO * This is an alias for {@link Query#fields}. * ::: * * @param {string|array|object|boolean} fields The fields to return. * * @see {@link Query#fields} * @returns {Query} The same {@link Query} instance to allow chaining. */ returning(...fields) { return this.addFields(fields); } where(...args) { return this.addOption('where', args); } having(...args) { return this.addOption('having', args); } groupBy(...groupBy) { return this.addOption('groupBy', groupBy); } orderBy(...orderBy) { return this.addOption('orderBy', orderBy); } limit(limit) { return this.setOption('limit', parseInt(limit)); } offset(offset) { return this.setOption('offset', parseInt(offset)); } forUpdate() { return this.setOption('forUpdate', true); } of(...tables) { return this.appendOption('of', tables); } noWait() { return this.setOption('noWait', true); } // TODO: add support for multiple option values i.e. array that gets transformed to var args // TODO: strict mode: validate option against allowed options setOptions(options = {}) { Object.keys(options).forEach((option) => { if (typeof this[option] !== 'function') { throw new this.constructor.QueryError(`${this.model.name}: unknown option \`${option}\``); } this[option](options[option]); }); return this; } quote(value) { return value; } formatTable(table, { quote = true } = {}) { table = quote ? this.quote(table) : table; if (this.config.schema) { const schema = quote ? this.quote(this.config.schema) : this.config.schema; table = `${schema}.${table}`; } return table; } formatAlias(alias, { quote } = {}) { if (this.config.schema) { alias = `${this.config.schema}.${alias}`; } return quote ? this.quote(alias) : alias; } getTable({ format = true, quote = true } = {}) { const table = format ? this.formatTable(this.config.table, { quote }) : this.config.table; const alias = format ? this.formatAlias(this.config.alias, { quote }) : this.config.alias; return this.sql(`${table} AS ${alias}`); } getAlias({ format = true, quote = true } = {}) { const alias = format ? this.formatAlias(this.config.alias, { quote }) : this.config.alias; return alias; } formatColumn(column, { quote = true } = {}) { const alias = this.getAlias({ quote: true }); column = quote ? this.quote(column) : column; return `${alias}.${column}`; } // TODO: strict mode: for fetches, warn if field is unknown and is not a function // TODO: v2: default `quote` to `true` (breaking change - @knorm/paginate) // TODO: v2: drop support for sql functions as raw strings and only support // sql bricks instances - allows us to validate field-names getColumn(field, { format = true, quote } = {}) { if (field instanceof this.sql) { return field; } const column = this.config.fieldsToColumns[field]; if (!column) { return this.sql(field); } return format ? this.formatColumn(column, { quote }) : quote ? this.quote(column) : column; } // TODO: maybe use something other than the dot as a separator (breaking change - @knorm/relations) formatFieldAlias(alias, { quote = true } = {}) { alias = `${this.config.alias}.${alias}`; return quote ? this.quote(alias) : alias; } getColumns(fields) { return Object.entries(fields).reduce((aliased, [alias, field]) => { const column = this.getColumn(field); alias = this.formatFieldAlias(alias); aliased.push(this.sql(`${column} AS ${alias}`)); return aliased; }, []); } prepareGroupBy(sql, groupBy) { groupBy.forEach((fields) => { if (!isArray(fields)) { fields = [fields]; } sql.groupBy(fields.map(this.getColumn, this)); }); } // adds support for orderBy({ field: 1 }) and orderBy({ field: 'asc' }) // TODO: strict mode: throw if order direction is unknown prepareOrderBy(sql, orderBy) { const columns = []; orderBy.forEach((fields) => { if (isArray(fields)) { fields.forEach((field) => { columns.push(this.getColumn(field)); }); } else if (isObject(fields)) { Object.entries(fields).forEach(([field, direction]) => { if (direction === 1) { direction = 'asc'; } else if (direction === -1) { direction = 'desc'; } if (direction !== 'asc' && direction !== 'desc') { direction = 'asc'; } columns.push(this.sql(`${this.getColumn(field)} ${direction}`)); }); } else { columns.push(this.getColumn(fields)); } }); return sql.orderBy(columns); } // depended on by knorm-soft-delete isField(field) { return isString(field) && !!this.config.fieldsToColumns[field]; } // depended on by knorm-soft-delete isWhere(field) { return isString(field) && field.startsWith('_$_'); } getWhere(where, options = {}) { const [field, value, ...rest] = where; if (isString(field)) { const { type, forHaving } = options; if (value === undefined && type !== 'isNull' && type !== 'isNotNull') { throw new this.constructor.QueryError(`${this.model.name}: undefined "${forHaving ? 'having' : 'where'}" value passed for field \`${field}\``); } // TODO: upstream `in` with an empty array to sql-bricks if (type === 'in' && !value.length) { return this.sql('$1', false); } const column = this.getColumn(field); // TODO: upstream `between` with an array to sql-bricks if (type === 'between' && isArray(value)) { if (!value.length) { throw new this.constructor.QueryError(`${this.model.name}: empty array passed for "between" for field \`${field}\``); } return [column, ...value, ...rest]; } return [column, value, ...rest]; } const expressions = []; where.forEach((field) => { if (field instanceof this.sql) { expressions.push(field); } else if (isObject(field)) { Object.entries(field).forEach(([field, value]) => { if (this.isWhere(field)) { const type = field.slice(3); const where = this.getWhere(value, Object.assign(options, { type })); if (where instanceof this.sql) { expressions.push(where); } else { expressions.push(this.sql[type](...where)); } } else { const [column] = this.getWhere([field, value], options); expressions.push({ [column]: value }); } }); } else { expressions.push(this.sql('$1', field)); } }); return expressions; } prepareWhere(sql, fields) { return sql.where(...this.getWhere(fields, { forWhere: true })); } prepareHaving(sql, fields) { return sql.having(...this.getWhere(fields, { forHaving: true })); } // TODO: strict mode: warn if invalid options are used depending on the method // e.g. using `where` for inserts prepareSql(sql) { return __awaiter(this, void 0, void 0, function* () { if (this.config.forInsert) { return sql; } if (this.config.forFetch && this.options.fields) { const columns = this.getColumns(this.options.fields); const method = this.options.distinct ? 'distinct' : 'select'; sql[method](columns); } Object.entries(this.options).forEach(([option, values]) => { if (!values) { return; } if (option === 'forUpdate') { return sql.forUpdate(); } if (option === 'noWait') { return sql.noWait(); } if (!isArray(values) || !values.length) { return; } values.forEach((value) => { switch (option) { case 'of': return sql.of(this.quote(value)); case 'where': return this.prepareWhere(sql, value); case 'having': return this.prepareHaving(sql, value); case 'groupBy': return this.prepareGroupBy(sql, value); case 'orderBy': return this.prepareOrderBy(sql, value); } }); }); return sql; }); } // TODO: strict mode: throw if data is not an array (for inserts) nor an object // TODO: strict mode: throw if instance is not an instance of this.model getInstance(data) { if (data instanceof this.model) { // TODO: strict mode: validate that the instance is an instance of this.model return data; } return new this.model(data); // eslint-disable-line new-cap } // depended on by @knorm/postgres getRowValue({ value }) { return value; } // depended on by @knorm/postgres getCastFields(fields) { return fields; } // TODO: strict mode: throw if row is empty getRow(instance) { return __awaiter(this, void 0, void 0, function* () { let fields; if (this.config.forInsert) { instance.setDefaults(); const notInserted = []; if (instance[this.config.primary] === undefined) { notInserted.push(this.config.primary); } fields = lodash_1.difference(this.config.fieldNames, notInserted); } if (this.config.forUpdate) { const filledFields = this.config.fieldNames.filter((name) => instance[name] !== undefined); fields = lodash_1.difference(filledFields, this.config.notUpdated); } yield instance.validate({ fields }); const castFields = this.getCastFields(fields); if (castFields.length) { instance.cast({ fields: castFields, forSave: true }); } const data = instance.getFieldData({ fields }); return Object.entries(data).reduce((row, [field, value]) => { const column = this.getColumn(field, { format: false, quote: true }); row[column] = this.getRowValue({ field, column, value }); return row; }, {}); }); } // TODO: strict mode: warn/throw if no data is passed // TODO: strict mode: warn/throw if row is empty prepareData(data) { return __awaiter(this, void 0, void 0, function* () { if (!isArray(data)) { data = [data]; } const batches = []; const batchSize = this.options.batchSize; let currentBatch = []; let fieldCount; yield Promise.all(data.map((data) => __awaiter(this, void 0, void 0, function* () { const instance = this.getInstance(data); const row = yield this.getRow(instance); const rowFieldCount = Object.values(row).length; if (fieldCount === undefined) { fieldCount = rowFieldCount; } else if (fieldCount !== rowFieldCount) { throw new this.constructor.QueryError(`${this.model.name}: all objects for ${this.config.forUpdate ? 'update' : 'insert'} should have the same number of fields`); } if (batchSize) { if (currentBatch.length >= batchSize) { batches.push(currentBatch); currentBatch = [row]; } else { currentBatch.push(row); } } else { currentBatch.push(row); } }))); if (currentBatch.length) { batches.push(currentBatch); } return batches; }); } // TODO: make the default `fields` option configurable ensureFields() { if (this.options.fields === undefined) { this.options.fields = this.config.fieldNames.reduce((fields, field) => { fields[field] = field; return fields; }, {}); } } prepareInsert(data, options) { return __awaiter(this, void 0, void 0, function* () { this.setOptions(options); this.ensureFields(); this.config.forInsert = true; const batches = yield this.prepareData(data); return Promise.all(batches.map((batch) => __awaiter(this, void 0, void 0, function* () { const sql = this.sql.insert(this.getTable(), batch).values(); return this.prepareSql(sql); }))); }); } // TODO: v2: refactor prepareUpdateBatch => getUpdateBatch // depended on by knorm-postgres prepareUpdateBatch(batch) { return this.sql.update(this.getTable(), batch[0]); } prepareUpdate(data, options) { return __awaiter(this, void 0, void 0, function* () { this.setOptions(options); this.ensureFields(); this.config.forUpdate = true; const batches = yield this.prepareData(data); return Promise.all(batches.map((batch) => __awaiter(this, void 0, void 0, function* () { const sql = this.prepareUpdateBatch(batch); return this.prepareSql(sql); }))); }); } prepareDelete(options) { return __awaiter(this, void 0, void 0, function* () { this.setOptions(options); this.ensureFields(); this.config.forDelete = true; const sql = this.sql.delete(this.getTable()); return this.prepareSql(sql); }); } prepareFetch(options) { return __awaiter(this, void 0, void 0, function* () { this.setOptions(options); this.ensureFields(); this.config.forFetch = true; const from = this.getTable(); const sql = this.sql.select().from(from); return this.prepareSql(sql); }); } // depended on by knorm-relations throwFetchRequireError() { if (this.options.require) { throw new this.constructor.NoRowsFetchedError({ query: this }); } } // depended on by knorm-relations getParsedRow() { // eslint-disable-next-line new-cap return new this.model(); } // TODO: strict mode: warn if a row value is undefined // TODO: cast fields even when they are aliased (requires changes in Model) parseRow(row) { const parsedRow = this.getParsedRow(row); const fields = []; Object.keys(this.options.fields).forEach((alias) => { const value = row[this.formatFieldAlias(alias, { quote: false })]; if (value === undefined) { return; } if (this.isField(alias)) { fields.push(alias); } parsedRow[alias] = value; }); return parsedRow.cast({ fields, forFetch: true }); } // depended on by knorm-relations parseRows(rows) { if (this.options.fields === false) { const fakeRows = []; if (this.options.first) { fakeRows.push(null); } return fakeRows; } return rows.map(this.parseRow, this); } /** * Executes a query. This method calls, in order, {@link Query#connect} to * connect to the database, {@link Query#formatSql} to format the SQL to be * queried, {@link Query#query} to run the query against the database, and * finally, {@link Query#disconnect} to close the database connection. * * ::: tip INFO: Usage * This method is used internally by all {@link Query} methods i.e. * {@link Query#fetch}, {@link Query#insert}, {@link Query#update} and * {@link Query#delete}. * ::: * * ::: tip INFO: Query errors * If the promise from {@link Query#query} is rejected, the {@link QueryError} * is passed to {@link Query#disconnect}. * ::: * * ::: tip INFO: Transactions * When queries are executed in a transaction (that has not yet * {@link Transaction#ended}), this method does not connect to the database * via {@link Query#connect}. Instead, it connects via * {@link Transaction#connect}. The first query to be executed in the * transaction causes {@link Transaction#connect} and * {@link Transaction#begin} to be run, after which subsequent queries re-use * the transaction's connection. * * In addition, the database connection is not closed after executing the * query. This is deferred to be handled by {@link Transaction#commit} or * {@link Transaction#rollback}. * * If {@link Query#query} rejects with an error, the error is passed to * {@link Transaction#rollback}. * * **NOTE:** once the transaction has {@link Transaction#ended}, connections * are established and closed as usual, via {@link Query#connect} and * {@link Query#disconnect}. * ::: * * ::: tip INFO: Multiple queries * When the `sql` parameter is an array, a single database connection will be * created but {@link Query#formatSql} and {@link Query#query} will be called * for each item in the array. * * Also note that the queries are run in parallel (via `Promise.all`) and the * rows returned from each query are merged into a single array (via * `Array.prototype.concat`). * ::: * * @param {SqlBricks|object|string|array} sql The SQL to run. When passed as * an array, it can be an array of `SqlBricks` instances, objects or strings. * @param {string} sql.text The parameterized SQL string (with placeholders), * when `sql` is passed as an object. * @param {array} sql.values The values for the parameterized SQL string, when * `sql` is passed as an object. * * @returns {Promise} A `Promise` that is resolved with an array of rows * returned from running the query. * * ::: tip INFO * If {@link Query#query} rejects with an error, the SQL that caused the error * is attached to the error as an `sql` property. * ::: */ execute(sql) { return __awaiter(this, void 0, void 0, function* () { const sqls = isArray(sql) ? sql : [sql]; const transaction = this.transaction; const transactionActive = transaction && !transaction.ended; if (transactionActive) { // when running in an active transaction, use the transaction's connection if (!transaction.connection) { // in the course of a transaction, Query#execute is bound to be called // more than once, so ensure Transaction#connect and Transaction#begin // are only run once once. yield transaction.connect(); yield transaction.begin(); } this.connection = transaction.connection; } else { yield this.connect(); } let rows = []; let error; try { yield Promise.all(sqls.map((sql) => __awaiter(this, void 0, void 0, function* () { const formattedSql = this.formatSql(sql); let batchRows; try { batchRows = yield this.query(formattedSql); } catch (e) { const { text, values } = formattedSql; e.sql = this.options.debug ? { text, values } : { text }; error = e; throw e; } rows = rows.concat(batchRows); }))); } finally { if (transactionActive) { if (error) { // if there's an active transaction and an error occurs, roll back the // transaction and disconnect (via Transaction#rollback) yield transaction.rollback(error); } // if there's no error, do nothing. defer closing the connection till // the transaction is ended } else { yield (error ? this.disconnect(error) : this.disconnect()); } } return rows; }); } /** * Connects to the database, via {@link Connection#create}. This method is * called by {@link Query#execute}. * * @returns {Promise} The `Promise` from {@link Connection#create}, that is * resolved when a connection is established or rejected with a * {@link QueryError} on error. */ connect() { return __awaiter(this, void 0, void 0, function* () { try { this.connection = new this.constructor.Connection(); return yield this.connection.create(); } catch (e) { throw new this.constructor.QueryError(e); } }); } /** * Formats SQL before it's sent to the database. This method is called * by {@link Query#execute} and allows manipulating or changing the SQL * before it's run via {@link Query#query}. * * @param {SqlBricks|object|string} sql The SQL to be formatted. * @param {string} sql.text The parameterized SQL string (with placeholders), * when `sql` is passed as an object. * @param {array} sql.values The values for the parameterized SQL string, when * `sql` is passed as an object. * * ::: tip INFO * This method is called internally by {@link Query#execute}. * ::: * * @returns {object} An object with `text` and `values` properties. Note that * when `sql` is passed as a string, the object returned has no `values` * property. When an {@link SqlBricks} instance is passed, an object is * returned (via [`toParams`](https://csnw.github.io/sql-bricks/#toParams)). */ formatSql(sql) { if (typeof sql === 'string') { return { text: sql }; } if (sql instanceof this.sql.Statement) { return sql.toParams(); } const { text, values } = sql; return { text, values }; } /** * Runs a query against the database, via {@link Connection#query}. This * method is called by {@link Query#execute}. * * @param {object|string} sql The SQL to be run, after it's formatted via * {@link Query#formatSql}. * * @returns {Promise} The `Promise` from {@link Connection#query}, that is * resolved with the query result or rejected with a {@link QueryError} on * error. */ query(sql) { return __awaiter(this, void 0, void 0, function* () { try { return yield this.connection.query(sql); } catch (e) { throw new this.constructor.QueryError(e); } }); } /** * Closes the database connection after running the query, via * {@link Connection#close}. This method is called by {@link Query#execute}. * * @param {QueryError} [error] A {@link QueryError} from {@link Query#query}, * if one occurred. This error is then passed to {@link Connection#close}. * * @returns {Promise} The `Promise` from {@link Connection#close}, that is * resolved when the connection is closed or rejected with a * {@link QueryError} on error. */ disconnect(error) { return __awaiter(this, void 0, void 0, function* () { try { return yield (error ? this.connection.close(error) : this.connection.close()); } catch (e) { throw new this.constructor.QueryError(e); } }); } _attachErrorStack(error, stack) { if (!stack) { return error; } error.stack = error.stack.slice(0, error.stack.indexOf('\n') + 1) + stack.slice(stack.indexOf('\n') + 1); return error; } /** * Inserts data into the database. * * @param {Model|object|array} data The data to insert. Can be a plain object, * a {@link Model} instance or an array of objects or {@link Model} instances. * @param {object} [options] {@link Query} options * * ::: tip INFO * When the {@link Query#batchSize} option is set, multiple insert batches are * created and multiple queries are sent to the database, but on the same * database connection. * ::: * * @returns {Promise} the promise is resolved with an array of the model's * instances, expect in the following cases: * * - if the {@link Query#first} query option was set to `true`, then the * promise is resolved with a single model instance or `null` if no rows * were inserted. * - if no rows were inserted, then the array will be empty. If the * {@link Query#require} query option was set to `true`, then the `Promise` * is rejected with a {@link Query.NoRowsInsertedError} instead. * - if the insert query failed, then the `Promise` is rejected with a * {@link Query.InsertError} instead. * * @todo Add support for inserting joined models (via * [@knorm/relations](https://github.com/knorm/relations)) * @todo debug/strict mode: throw/warn if data is empty */ insert(data, options) { return __awaiter(this, void 0, void 0, function* () { const stack = this.options.debug ? new Error().stack : undefined; let rows = []; if (!isEmpty(data)) { const sqls = yield this.prepareInsert(data, options); rows = yield this.execute(sqls).catch((error) => { throw this._attachErrorStack(new this.constructor.InsertError({ error, query: this }), stack); }); } if (!rows.length) { if (this.options.require) { throw new this.constructor.NoRowsInsertedError({ query: this }); } return this.options.first ? null : []; } const parsedRows = this.parseRows(rows); return this.options.first ? parsedRows[0] : parsedRows; }); } /** * Updates data in the database. * * ::: warning NOTE * When the `data` param is a single object or {@link Model} instance and the * {@link Query#where} option is not set, **ALL rows in the table will be * updated!** This mimics the behaviour of `UPDATE` queries. However, if the * primary field is set in the data, then only the row matching the primary * field is updated. * ::: * * ::: tip INFO * The `data` param only works as an array in conjunction with plugins that * support updating multiple (ideally, in a single query) e.g. * [@knorm/postgres](https://github.com/knorm/postgres). * ::: * * ::: tip INFO * When the {@link Query#batchSize} option is set, multiple update batches are * created and multiple queries are sent to the database, but on the same * database connection. * ::: * * @param {Model|object|array} data The data to update. Can be a plain object, * a {@link Model} instance or an array of objects or instances. * @param {object} [options] {@link Query} options * * @returns {Promise} the promise is resolved with an array of the model's * instances, expect in the following cases: * * - if the {@link Query#first} query option was set to `true`, then the * promise is resolved with a single model instance or `null` if no rows * were inserted. * - if no rows were updated, then the array will be empty. If the * {@link Query#require} query option was set to `true`, then the `Promise` * is rejected with a {@link Query.NoRowsUpdatedError} instead. * - if the update query failed, then the `Promise` is rejected with a * {@link Query.UpdateError} instead. * * @todo Add support for updating joined models (via * [@knorm/relations](https://github.com/knorm/relations)) * @todo Update a single row when unique fields are set (in addition to * the primary field being set) * @todo debug/strict mode: throw/warn if data is empty */ update(data, options) { return __awaiter(this, void 0, void 0, function* () { const stack = this.options.debug ? new Error().stack : undefined; let rows = []; if (!isEmpty(data)) { if (isObject(data)) { const primary = data[this.config.primary]; if (primary !== undefined) { this.where({ [this.config.primary]: primary }); } } const sqls = yield this.prepareUpdate(data, options); rows = yield this.execute(sqls).catch((error) => { throw this._attachErrorStack(new this.constructor.UpdateError({ error, query: this }), stack); }); } if (!rows.length) { if (this.options.require) { throw new this.constructor.NoRowsUpdatedError({ query: this }); } return this.options.first ? null : []; } const parsedRows = this.parseRows(rows); return this.options.first ? parsedRows[0] : parsedRows; }); } /** * Either inserts or updates data in the database. * * ::: warning NOTE * When the `data` param is a single object or {@link Model} instance and the * {@link Query#where} option is not set, **ALL rows in the table will be * updated!** This mimics the behaviour of `UPDATE` queries. However, if the * primary field is set in the data, then only the row matching the primary * field is updated. * ::: * * ::: tip INFO * - when the `data` param is an array, this method proxies to * {@link Query#insert}. * - when the `data` param is an object and the primary field is **not** set, * this method proxies to {@link Query#insert}. However, if the primary * field is set, then the method proxies to {@link Query#update}. * ::: * * @param {Model|object|array} data The data to update. Can be a plain object, * a {@link Model} instance or an array of objects or instances. * @param {object} [options] {@link Query} options */ save(data, options) { return __awaiter(this, void 0, void 0, function* () { if (Array.isArray(data) || data[this.config.primary] === undefined) { return this.insert(data, options); } return this.update(data, options); }); } /** * Fetches data from the database. * * @param {object} [options] {@link Query} options * * @returns {Promise} the promise is resolved with an array of the model's * instances, expect in the following cases: * * - if the {@link Query#first} query option was set to `true`, then the * promise is resolved with a single model instance or `null` if no rows * were inserted. * - if no rows were updated, then the array will be empty. If the * {@link Query#require} query option was set to `true`, then the `Promise` * is rejected with a {@link Query.NoRowsFetchedError} instead. * - if the fetch query failed, then the `Promise` is rejected with a * {@link Query.FetchError} instead. * * @todo [@knorm/relations](https://github.com/knorm/relations)): throw if a * fetch is attempted from a joined query * @todo [@knorm/relations](https://github.com/knorm/relations)): add support * for limit and offset options in joined queries (probably with a subquery) */ fetch(options) { return __awaiter(this, void 0, void 0, function* () { const stack = this.options.debug ? new Error().stack : undefined; const sql = yield this.prepareFetch(options); const rows = yield this.execute(sql).catch((error) => { throw this._attachErrorStack(new this.constructor.FetchError({ error, query: this }), stack); }); if (!rows.length) { this.throwFetchRequireError(); return this.options.first ? null : []; } const parsedRows = this.parseRows(rows); return this.options.first ? parsedRows[0] : parsedRows; }); } /** * Deletes data from the database. * * ::: warning NOTE * If the {@link Query#where} option is not set, **ALL rows in the table will * be deleted!** This mimics the behaviour of `DELETE` queries. * ::: * * @param {object} [options] {@link Query} options * * @returns {Promise} the promise is resolved with an array of the model's * instances, expect in the following cases: * * - if the {@link Query#first} query option was set to `true`, then the * promise is resolved with a single model instance or `null` if no rows * were inserted. * - if no rows were updated, then the array will be empty. If the * {@link Query#require} query option was set to `true`, then the `Promise` * is rejected with a {@link Query.NoRowsDeletedError} instead. * - if the delete query failed, then the `Promise` is rejected with a * {@link Query.DeleteError} instead. * * @todo [@knorm/relations](https://github.com/knorm/relations)): add support * for deleting joined queries */ delete(options) { return __awaiter(this, void 0, void 0, function* () { const stack = this.options.debug ? new Error().stack : undefined; const sql = yield this.prepareDelete(options); const rows = yield this.execute(sql).catch((error) => { throw this._attachErrorStack(new this.constructor.DeleteError({ error, query: this }), stack); }); if (!rows.length) { if (this.options.require) { throw new this.constructor.NoRowsDeletedError({ query: this }); } return this.options.first ? null : []; } const parsedRows = this.parseRows(rows); return this.options.first ? parsedRows[0] : parsedRows; }); } static get where() { return new this.Where(); } } exports.Query = Query; /** @type {typeof import("sql-bricks")} */ Where_1.Where.prototype.sql = sql_bricks_1.default; /** @type {typeof import("sql-bricks")} */ Query.prototype.sql = sql_bricks_1.default; Query.Where = Where_1.Where; /** * A reference to {@link Connection}, for use within {@link Query}. */ Query.Connection = Connection_1.Connection; /** * Alias for {@link Query#fields}, improves code readability when configuring a * single field. * * ::: tip INFO * This is an alias for {@link Query#fields}. * ::: * * @param {string|array|object|boolean} fields The field to return. * * @see {@link Query#fields} * * @returns {Query} The same {@link Query} instance to allow chaining. */ Query.prototype.field = Query.prototype.fields; /** * The base error that all errors thrown by {@link Query} inherit from. */ Query.QueryError = QueryError_1.QueryError; /** * The rejection error from {@link Query#fetch} on error. * * @extends {Query.QueryError} */ Query.FetchError = class FetchError extends Query.QueryError { }; /** * The rejection error from {@link Query#insert} on error. * * @extends {Query.QueryError} */ Query.InsertError = class InsertError extends Query.QueryError { }; /** * The rejection error from {@link Query#update} on error. * * @extends {Query.QueryError} */ Query.UpdateError = class UpdateError extends Query.QueryError { }; /** * The rejection error from {@link Query#delete} on error. * * @extends {Query.QueryError} */ Query.DeleteError = class DeleteError extends Query.QueryError { }; /** * The base error for all errors thrown by {@link Query} when the * {@link Query#require} option is set. */ Query.NoRowsError = NoRowsError_1.NoRowsError; /** * The rejection error from {@link Query#fetch} when no rows are fetched and the * {@link Query#require} option was set. * * @extends {Query.NoRowsError} */ Query.NoRowsFetchedError = class NoRowsFetchedError extends Query.NoRowsError { }; /** * The rejection error from {@link Query#insert} when no rows are inserted and * the {@link Query#require} option was set. * * @extends {Query.NoRowsError} */ Query.NoRowsInsertedError = class NoRowsInsertedError extends Query.NoRowsError { }; /** * The rejection error from {@link Query#update} when no rows are updated and * the {@link Query#require} option was set. * * @extends {Query.NoRowsError} */ Query.NoRowsUpdatedError = class NoRowsUpdatedError extends Query.NoRowsError { }; /** * The rejection error from {@link Query#delete} when no rows are deleted and * the {@link Query#require} option was set. * * @extends {Query.NoRowsError} */ Query.NoRowsDeletedError = class NoRowsDeletedError extends Query.NoRowsError { }; /** * A reference to the {@link Knorm} instance. * * ::: tip * This is the same instance assigned to the {@link Query.knorm} static * property, just added as a convenience for use in instance methods. * ::: */ Query.prototype.knorm = null; /** * A reference to the {@link Knorm} instance. * * ::: tip * This is the same instance assigned to the {@link Query#knorm} instance * property, just added as a convenience for use in static methods. * ::: */ Query.knorm = null; /** * The model registry. This is an object containing all the models added to the * ORM, keyed by name. See [model registry](/guides/models.md#model-registry) * for more info. * * ::: tip * This is the same object assigned to the {@link Query.models} static property, * just added as a convenience for use in instance methods. * ::: * * @type {object} */ Query.prototype.models = {}; /** * The model registry. This is an object containing all the models added to the * ORM, keyed by name. See [model registry](/guides/models.md#model-registry) * for more info. * * ::: tip * This is the same object assigned to the {@link Query#models} instance * property, just added as a convenience for use in static methods. * ::: * * @type {object} */ Query.models = {}; /** * For queries run within a transaction, this is reference to the * {@link Transaction} instance. * * ::: warning NOTE * This is only set for {@link Query} instances that are run within a * transaction, otherwise it's set to `null`. * ::: * * ::: tip * This is the same instance assigned to the {@link Query.transaction} static * property, just added as a convenience for use in static methods. * ::: * */ Query.prototype.transaction = null; /** * For queries run within a transaction, this is reference to the * {@link Transaction} instance. * * ::: warning NOTE * This is only set for {@link Query} classes within a transaction, otherwise * it's set to `null`. * ::: * * ::: tip * This is the same instance a