UNPKG

massive

Version:

A small query tool for Postgres that embraces json and makes life simpler

247 lines (208 loc) 8.78 kB
'use strict'; const _ = require('lodash'); const util = require('util'); const Readable = require('./readable'); const Delete = require('./statement/delete'); const Insert = require('./statement/insert'); const Update = require('./statement/update'); const Statement = require('./statement/statement'); /** * A database table or other writable object. * * @class * @extends Entity * @extends Readable * @param {Object} spec - An {@linkcode Entity} specification representing a table: * @param {Object} spec.db - A {@linkcode Database}. * @param {String} spec.name - The table or view's name. * @param {String} spec.schema - The name of the schema owning the table. * @param {String} spec.pk - The table's primary key column. */ const Writable = function (spec) { Readable.apply(this, arguments); this.pk = spec.pk ? _.castArray(spec.pk) : undefined; this.fks = spec.fks || undefined; this.insertable = spec.is_insertable_into || true; }; util.inherits(Writable, Readable); /** * Attempts to assemble primary key criteria for a record object representing a * row in this table. The criteria must include the full primary key, and must * not invoke any operations. * * @param {Object} record - The record to evaluate. * @return {Object} The successfully assembled criteria, or null if primary key * information is incomplete or invalid. */ Writable.prototype.getPkCriteria = function (record) { let missing = false; const criteria = this.pk.reduce((obj, pkColumn) => { if (Object.prototype.hasOwnProperty.call(record, pkColumn)) { obj[pkColumn] = record[pkColumn]; } else { missing = true; } return obj; }, {}); return missing ? null : criteria; }; /** * Insert a record or records into the table. * * @param {Object|Array} data - A record or records to insert. * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Insert options}. * @return {Promise} If passed a record object, the record as inserted (with * default or autogenerated values set); if passed an array, an array * containing the inserted records. */ Writable.prototype.insert = function (data, options) { if (!this.insertable) { return this.db.$p.reject(new Error(`${this.name} is not writable`)); } else if (!data) { return this.db.$p.reject(new Error('Must provide data to insert')); } const insert = new Insert(this, data, options); if (insert.params.length === 0) { // just return empty arrays so bulk inserting variable-length lists is more friendly return this.db.$p.resolve([]); } return this.db.query(insert); }; /** * Update a record with a criteria object and a map of changed fields to their * new values. * * @param {String|Number|Object} criteria - Primary key of the record, or a * criteria object. * @param {Object} changes - A map of columns to their new values. * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Update options}. * @return {Promise} If updating a single record by its primary key, the * modified record; otherwise, an array containing any modified records. */ Writable.prototype.update = function (criteria, changes, options = {}) { if (!this.insertable) { return this.db.$p.reject(new Error(`${this.name} is not writable`)); } else if (!_.isObjectLike(changes) || _.isArray(changes)) { return this.db.$p.reject(new Error('Update requires a hash of fields=>values to update to')); } else if (_.isEmpty(changes)) { // there's nothing to update, so just return the matching records return this.find(criteria, options); } const update = new Update(this, changes, criteria, options); return this.db.query(update); }; /** * Saves an object. If the object does not include a value for the table's * primary key, this will emit an INSERT to create a new record; if it does * contain the primary key it will emit an UPDATE for the existing record. * * Either way, the newest available version of the record will be returned. * * This is not a true Postgres upsert! If you need the behavior of ON CONFLICT * DO UPDATE, look into the {@link https://massivejs.org/docs/options-objects|onConflictUpdate option}. * * @param {Object} record - The record to upsert. * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Insert/update options}. * @return {Promise} The inserted or updated record object. */ Writable.prototype.save = function (record, options = {}) { if (!this.pk) { return this.db.$p.reject(new Error(`${this.name} has no primary key, use insert or update to write to this table`)); } else if (!_.isObjectLike(record) || _.isArray(record)) { return this.db.$p.reject(new Error('Must provide an object with all fields being modified and the primary key if updating')); } const keys = _.keys(record); if (_.intersection(keys, this.pk).length === this.pk.length) { const criteria = this.getPkCriteria(record); record = _.omitBy(record, (value, key) => { return _.isFunction(record[key]) || this.pk.indexOf(key) > -1; }); options.single = true; // prevent options from being read as changes in the bulk update format return this.update(criteria, record, options); } return this.insert.apply(this, arguments); }; /** * Delete a record or records. * * @param {Object} criteria - A criteria object or primary key. * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Delete options}. * @return {Promise} For a primary key, the deleted record; for a criteria * object, an array containing all deleted records. */ Writable.prototype.destroy = function (criteria, options) { return this.db.query(new Delete(this, criteria, options)); }; /** * Save a document to the database. This function will create or replace the * entire document body. * * @param {Object} doc - The document to persist. * @return {Promise} The updated document. */ Writable.prototype.saveDoc = function (doc) { if (!_.isObjectLike(doc) || _.isArray(doc)) { return this.db.$p.reject(new Error('Please pass in the document for saving as an object. Include the primary key for an UPDATE.')); } const options = {single: true, document: true}; const criteria = this.getPkCriteria(doc); if (criteria) { const update = new Update( this, { body: _.omit(doc, this.pk, 'created_at', 'updated_at') }, criteria, options ); return this.db.query(update); } return this.db.query(new Insert(this, {body: doc}, options)); }; /** * Save documents to the database. This function will create or replace the * entire document body for each document. * * @param {Object} docs - The documents to persist. * @return {Promise} The updated documents. */ Writable.prototype.saveDocs = function (docs) { if (!_.isArray(docs)) { return this.db.$p.reject(new Error('Please pass in the documents as an array of objects.')); } if (!docs.every(_.isObjectLike)) { return this.db.$p.reject(new Error('Please pass in valid documents. Include the primary key for an UPDATE.')); } return this.db.$p.all(docs.map(this.saveDoc.bind(this))); }; /** * Update a document, adding new information and changing existing information. * This function can be used with any JSON field, not just document tables; * however, only document tables can use criteria objects which directly * reference document fields. * * If calling updateDoc with a criteria object for a non-document table, the * criteria will be tested against the entire row (as opposed to the document * body as it is for document tables). To test elements of the JSON field in a * non-document table with a criteria object, use a JSON path string. * * @param {String|Number|Object} criteria - Primary key of the document, or a * criteria object. * @param {Object} changes - Changes to apply. * @param {Object} [options] - {@link https://massivejs.org/docs/options-objects|Update options}. * @param {String} [options.body] - Override the "body" JSON field to affect. * @return {Promise} If modifying a document table, the document; otherwise, the * modified row. */ Writable.prototype.updateDoc = function (criteria, changes, options = {}) { if (!Object.prototype.hasOwnProperty.call(options, 'body')) { options.body = 'body'; options.document = true; } const statement = new Statement(this, options); statement.setCriteria(criteria, [JSON.stringify(changes)]); const sql = `UPDATE ${this.delimitedFullName} SET "${options.body}" = COALESCE("${options.body}", '{}'::jsonb) || $1 WHERE ${statement.predicate} RETURNING *;`; return this.db.query(sql, statement.params, statement); }; module.exports = Writable;