UNPKG

@mysql/xdevapi

Version:

MySQL Connector/Node.js - A Node.js driver for MySQL using the X Protocol and X DevAPI.

511 lines (466 loc) 22.9 kB
/* * Copyright (c) 2015, 2022, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, as * published by the Free Software Foundation. * * This program is also distributed with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, * as designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an * additional permission to link the program and your derivative works * with the separately licensed software that they have included with * MySQL. * * Without limiting anything contained in the foregoing, this file, * which is part of MySQL Connector/Node.js, is also subject to the * Universal FOSS Exception, version 1.0, a copy of which can be found at * http://oss.oracle.com/licenses/universal-foss-exception. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ 'use strict'; const Expr = require('./Expr'); const collectionAdd = require('./CollectionAdd'); const collectionFind = require('./CollectionFind'); const collectionModify = require('./CollectionModify'); const collectionRemove = require('./CollectionRemove'); const DatabaseObject = require('./DatabaseObject'); const errors = require('../constants/errors'); const escapeQuotes = require('./Util/escapeQuotes'); const sqlExecute = require('./SqlExecute'); const table = require('./Table'); /** * Factory function that creates a DevAPI Collection instance. This instance * is used to create and/or execute various CRUD-like statements. * @module Collection * @mixes DatabaseObject * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/crud-ebnf-collection-crud-functions.html|Collection CRUD Functions} */ /** * @private * @alias module:Collection * @param {module:Connection} [connection] - The instance of the current * database connection. * @param {module:CollectionFind} [getOneStatement] - A potentially cached * CollectionFind statement which is prepared once and used every time the * "getOne" method is executed. * @param {module:CollectionRemove} [removeOneStatement] - A potentially cached * CollectionRemove statement which is prepared once and used every time the * "removeOne" method is executed. * @param {module:Schema} [schema] - The instance of the schema where statements * will be executed. * @param {string} [tableName] - The name of the underlying database table. * @returns {module:Collection} */ function Collection ({ connection, getOneStatement, removeOneStatement, replaceOneStatement, schema, tableName } = {}) { return { ...DatabaseObject(connection), /** * Creates a statement that adds one or more documents to the * collection. * This method does not cause the statement to be executed. * @function * @name module:Collection#add * @param {...DocumentsOrJSON} documentsOrJSON - One * or more plain JavaScript objects, JSON strings or X DevAPI * expressions that represent document definitions provided as an * array or as multiple arguments. * @example * // arguments as single documents * collection.add({ foo: 'baz' }, { bar: 'qux' }) * * // array of documents * collection.add([{ foo: 'baz' }, { bar: 'qux' }]) * @returns {module:CollectionAdd} A new instance of a statement * containing the documents which will be added to the collection. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/crud-ebnf-collection-crud-functions.html#crud-ebnf-collectionaddfunction|CollectionAddFunction} */ add (...documentsOrJSON) { // Since each argument can be a single document or a list of // documents, the spread operator is used to wrap them in an array // which is then flattened. return collectionAdd({ connection, schema, tableName }).add(documentsOrJSON.flat()); }, /** * Creates a document with a given id and set of fields and values or * replace one if it already exists. * This method executes a statement in the database. * @function * @name module:Collection#addOrReplaceOne * @param {string} id - The id of the document. * @param {Object} data - An Object containing the document fields and * corresponding values. * @example * collection.addOrReplaceOne('foo', { prop1: 'bar', prop2: 'baz' }) * @throws Object contains an <code>_id</code> field whose value is * different then the given document id from the first argument. * @returns {Promise<module:Result>} A <code>Promise</code> that resolves to * an object containing the details reported by the server. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/collection-single-document-operations.html|Single Document Operations} */ addOrReplaceOne (id, data = {}) { // If the reference id does not match a potential replacement // document id, the operation should be aborted. if (typeof data._id !== 'undefined' && data._id !== id) { return Promise.reject(new Error(errors.MESSAGES.ER_DEVAPI_DOCUMENT_ID_MISMATCH)); } return collectionAdd({ connection, schema, tableName, upsert: true }) .add({ ...data, _id: escapeQuotes(id) }) .execute(); }, /** * Retrieves the total number of documents in the collection. * This method executes a statement in the database. * @function * @name module:Collection#count * @returns {Promise<number>} A <code>Promise</code> that resolves to * the number of documents in the collection. */ count () { const schemaName = table.escapeIdentifier(schema.getName()); const collection = table.escapeIdentifier(tableName); let count = 0; const callback = row => { count = row[0]; }; return sqlExecute(connection, `SELECT COUNT(*) FROM ${schemaName}.${collection}`) .execute(callback) .then(() => count) .catch(err => { // The server error message does not make the distinction // between collections and tables, so we need to update it. // Maybe this should become the job of the plugin at some // point in the future. const message = err.message.replace('Table', 'Collection').replace('table', 'collection'); err.message = err.info.msg = message; throw err; }); }, /** * A field is identified by a given document path, has a given * datatype, is or is not required and can include geo-related options. * @typedef {Object} FieldDefinition * @prop {string} field - The full document path of the field. * @prop {string} type - The index datatype (see example). * @prop {boolean} [required=false] - Allow <code>NULL</code> column * values. * @prop {number} [options] - Describes how to handle GeoJSON documents * that contain geometries with coordinate dimensions higher than 2. * @prop {number} [srid] - Unique value used to unambiguously identify * projected, unprojected, and local spatial coordinate system * definitions. * @example * INT [UNSIGNED] * TINYINT [UNSIGNED] * SMALLINT [UNSIGNED] * MEDIUMINT [UNSIGNED] * INTEGER [UNSIGNED] * BIGINT [UNSIGNED] * REAL [UNSIGNED] * FLOAT [UNSIGNED] * DOUBLE [UNSIGNED] * DECIMAL [UNSIGNED] * NUMERIC [UNSIGNED] * DATE * TIME * TIMESTAMP * DATETIME * TEXT[(length)] * GEOJSON (extra options: options, srid) */ /** * A collection index has a given type and a specific set of * properties for each document field that is covered by the index. * @typedef {Object} IndexDefinition * @prop {string} [type=INDEX] - The index type (INDEX or SPATIAL). * @prop {FieldDefinition[]} fields - The list of definitions for each * of the index fields. */ /** * Creates an index with the given name and properties in the * collection. * This method executes a statement in the database. * @function * @name module:Collection#createIndex * @param {string} name - The name of the index. * @param {IndexDefinition} constraint - An object containing the * index definition. * @throws Index name is not a valid string. * @throws Index definition does not include a valid field list. * @throws Index definition includes an empty field list. * @throws Index definition includes an invalid field. * @throws Index definition is a missing field. * @throws Index definition includes a field without a datatype. * @throws Index with the given name already exists. * @throws Index is supposed to ensure uniqueness. * @returns {Promise<boolean>} A <code>Promise</code> that always * resolves to <code>true</code>. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/collection-indexing.html#collection-creating-index|Creating an Index} */ createIndex (name, constraint) { constraint = Object.assign({ fields: [] }, constraint); if (typeof name !== 'string' || !name.trim().length) { return Promise.reject(new Error(errors.MESSAGES.ER_DEVAPI_BAD_INDEX_NAME)); } const isValidDefinition = Array.isArray(constraint.fields) && constraint.fields.length && constraint.fields.every((field) => { return typeof field.field === 'string' && typeof field.type === 'string'; }); if (!isValidDefinition) { return Promise.reject(new Error(errors.MESSAGES.ER_DEVAPI_BAD_INDEX_DEFINITION)); } if (constraint.unique === true) { return Promise.reject(new Error(errors.MESSAGES.ER_DEVAPI_NO_UNIQUE_INDEX)); } const args = [{ name: name, schema: schema.getName(), collection: tableName, unique: false, type: constraint.type || 'INDEX', constraint: constraint.fields.map(item => { // 'field' property is renamed to 'member' to avoid an x-plugin incompatibility. const data = Object.assign({ array: item.array || false, member: item.field, required: false }, item); delete data.field; return data; }) }]; return sqlExecute(connection, 'create_collection_index', args, sqlExecute.Namespace.X_PLUGIN) .execute() .then(() => true); }, /** * Removes an index with the given name that has been previously * created in the collection. * This method executes a statement in the database and does not fail * if the index does not exist. * @function * @name module:Collection#dropIndex * @param {string} name - The name of the index. * @throws Index name is not a valid string. * @returns {Promise<boolean>} A <code>Promise</code> that resolves to * a boolean value which indicates whether the index was removed or not * (i.e. it did not exist). * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/collection-indexing.html#collection-creating-index|Creating an Index} */ dropIndex (name) { if (typeof name !== 'string' || !name.trim().length) { return Promise.reject(new Error(errors.MESSAGES.ER_DEVAPI_BAD_INDEX_NAME)); } const args = [{ name: name, schema: schema.getName(), collection: tableName }]; return sqlExecute(connection, 'drop_collection_index', args, sqlExecute.Namespace.X_PLUGIN).execute() .then(() => true) .catch(err => { if (!err.info || err.info.code !== errors.ER_CANT_DROP_FIELD_OR_KEY) { throw err; } return false; }); }, /** * Checks if this collection exists in the database. * This method executes a statement in the database. * @function * @name module:Collection#existsInDatabase * @returns {Promise<boolean>} A <code>Promise</code> that resolves to * a boolean value which indicates whether the collection exists or * not. */ existsInDatabase () { const args = [{ schema: schema.getName(), pattern: tableName }]; return sqlExecute(connection, 'list_objects', args, sqlExecute.Namespace.X_PLUGIN) .execute() .then(res => { return res.fetchAll().some(record => record[1] === 'COLLECTION'); }); }, /** * Creates a statement that looks for one or more documents in the * collection which match an optional filtering criteria. All * documents will be part of an eventual result set if no filtering * criteria is provided. * @function * @name module:Collection#find * @param {SearchConditionStr} [searchConditionStr] - An optional * filtering criteria specified as a string or an X DevAPI expression. * @returns {module:CollectionFind} A new instance of a statement * containing the filtering criteria which will be used to perform * the lookup. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/crud-ebnf-collection-crud-functions.html#crud-ebnf-collectionfindfunction|CollectionFindFunction} */ find (searchConditionStr) { const criteria = Expr({ value: searchConditionStr }).getValue(); return collectionFind({ connection, criteria, schema, tableName }); }, /** * Retrieves the collection name. * This method works with the local collection instance and does not * execute any statement in the database. * @function * @name module:Collection#getName * @returns {string} The name of the collection. */ getName () { return tableName; }, /** * Retrieves a single document with the given id. * This method executes a statement in the database. * @function * @name module:Collection#getOne * @param {string} id - The id of the document. * @example * collection.getOne('1') * @returns {Object} An object representing a local instance of the * document in the database. If the document does not exist in the * database, the object will be <code>null</code>. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/collection-single-document-operations.html|Single Document Operations} */ getOne (id) { let instance = null; // if the id is not provided, there is no need to even ask the server if (typeof id === 'undefined') { return Promise.resolve(instance); } getOneStatement = getOneStatement || this.find('_id = :id'); return getOneStatement.bind('id', id) .execute(doc => { instance = doc; }) .then(() => instance); }, /** * Retrieves the instance of the schema where the collection lives * under. * This method works with the local collection instance and does not * execute any statement in the database. * @function * @name module:Collection#getSchema * @returns {module:Schema} The instance of the schema where statements * will be executed. */ getSchema () { return schema; }, /** * Retrieve the collection metadata. * This method works with the local collection instance and does not * execute any statement in the database. * @function * @name module:Collection#inspect * @returns {Object} An object containing metadata about the * collection. */ inspect () { return { schema: schema.getName(), collection: tableName }; }, /** * Creates a statement to modify one or more documents in the * collection that match a given filtering criteria. The filtering * criteria is required, and needs to match a truthy value (e.g. * '1', 'true') to modify all documents in the collection. * This method does not cause the statement to be executed. * @function * @name module:Collection#modify * @param {SearchConditionStr} searchConditionStr - The required * filtering criteria specified as a string or as an X DevAPI * expression. * @example * // update all documents in a collection * collection.modify('true').set('name', 'bar') * * // update documents that match a given condition * collection.modify('name = "foo"').set('name', 'bar') * @returns {module:CollectionModify} A new instance of a statement * containing the filtering criteria which will be used for * determining which documents will be modified. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/crud-ebnf-collection-crud-functions.html#crud-ebnf-collectionmodifyfunction|CollectionModifyFunction} */ modify (searchConditionStr) { const criteria = Expr({ value: searchConditionStr }).getValue(); return collectionModify({ connection, criteria, schema, tableName }); }, /** * Creates a statement to remove one or more documents from the * collection that match a given filtering criteria. The filtering * criteria is required, and needs to match a truthy value (e.g. * '1', 'true') when the goal is to remove all documents from the * collection. * This method does not cause the statement to be executed. * @function * @name module:Collection#remove * @param {SearchConditionStr} searchConditionStr - The required * filtering criteria specified as a string or as an X DevAPI * expression. * @example * // remove all documents from a collection * collection.remove('true') * * // remove documents that match a given condition * collection.remove('name = "foobar"') * @returns {module:CollectionRemove} A new instance of a statement * containing the filtering criteria which will be used for * determining which documents will be removed. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/crud-ebnf-collection-crud-functions.html#crud-ebnf-collectionremovefunction|CollectionRemoveFunction} */ remove (searchConditionStr) { const criteria = Expr({ value: searchConditionStr }).getValue(); return collectionRemove({ connection, criteria, schema, tableName }); }, /** * Removes a single document with the given id. * This method executes a statement in the database. * @function * @name module:Collection#removeOne * @param {string} id - The id of the dcoument. * @example * collection.removeOne('1') * @returns {Promise<module:Result>} A <code>Promise</code> that * resolves to an object containing the details reported by the server. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/collection-single-document-operations.html|Single Document Operations} */ removeOne (id) { removeOneStatement = removeOneStatement || this.remove('_id = :id'); return removeOneStatement.bind('id', id).execute(); }, /** * Replaces a document that matches a given id with the set of field names * and values defined by a given object. * This method executes a statement in the database. * @function * @name module:Collection#replaceOne * @param {string} id - The id of the document. * @param {Object} data - An object containing the document fields and * corresponding values. * @example * collection.replaceOne('foo', { prop1: 'bar', prop2: 'baz' }) * @throws Object contains an <code>_id</code> field whose value is * different then the given document id from the first argument. * @returns {Promise<module:Result>} A <code>Promise</code> that * resolves to an object containing the details reported by the server. * @see {@link https://dev.mysql.com/doc/x-devapi-userguide/en/collection-single-document-operations.html|Single Document Operations} */ replaceOne (id, data = {}) { // If the reference id does not match a potential replacement // document id, the operation should be aborted. if (typeof data._id !== 'undefined' && data._id !== id) { return Promise.reject(new Error(errors.MESSAGES.ER_DEVAPI_DOCUMENT_ID_MISMATCH)); } return this.modify('_id = :id') .bind('id', id) .set('$', data) .execute(); } }; } module.exports = Collection;