UNPKG

futoin-database

Version:

Neutral database interface with powerful Query and revolution Transaction builders

407 lines (350 loc) 12 kB
'use strict'; /** * @file * * Copyright 2017 FutoIn Project (https://futoin.org) * Copyright 2017 Andrey Galkin <andrey@futoin.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const _cloneDeep = require( 'lodash/cloneDeep' ); const QueryBuilder = require( './QueryBuilder' ); /** * @class * @name QueryOptions * @property {integer|boolean|null} affected - affected rows constaint * @property {integer|boolean|null} selected - selected rows constaint * @property {boolean|null} return - return result in response */ /** * Version of QueryBuilder which forbids direct execution. */ class XferQueryBuilder extends QueryBuilder { execute( as, _unsafe_dml=false ) { throw new Error( 'Please use XferBuilder.execute()' ); } clone() { throw new Error( 'Cloning is not allowed' ); } /** * Get transaction back reference expression * @param {XferQueryBuilder} xqb - any previous transaction * query builder instances. * @param {string} field - field to reference by name * @param {boolean} [multi=false] - reference single result row or multiple * @returns {Expression} with DB-specific escape sequence */ backref( xqb, field, multi=false ) { this._state.template = true; return this.getDriver().backref( xqb._seq_id, field, multi ); } _forClause( mode ) { if ( this._state.type !== 'SELECT' ) { throw new Error( 'FOR clause is supported only for SELECT' ); } if ( this._state.forClause ) { throw new Error( 'FOR clause is already set' ); } this._state.forClause = mode; } /** * Mark select FOR UPDATE * @returns {XferQueryBuilder} self */ forUpdate() { this._forClause( 'UPDATE' ); return this; } /** * Mark select FOR SHARED READ * @returns {XferQueryBuilder} self */ forSharedRead() { this._forClause( 'SHARE' ); return this; } } /** * Transction builder. * * Overall concept is build inividual queries to be executed without delay. * It's possible to add result constraints to each query for intermediate checks: * - affected - integer or boolean to check DML result * - selected - integer or boolean to check DQL result * - result - mark query result to be returned in response list */ class XferBuilder { constructor( xb_or_lface, db_type=null, iso_level=null ) { if ( xb_or_lface instanceof XferBuilder ) { this._lface = xb_or_lface._lface; this._db_type = xb_or_lface._db_type; this._iso_level = xb_or_lface._iso_level; this._query_list = _cloneDeep( xb_or_lface._query_list ); } else { this._lface = xb_or_lface; this._db_type = db_type; this._iso_level = iso_level; this._query_list = []; } } /** * Get a copy of XferBuilder for independent processing. * @returns {XferBuilder} transaction builder instance */ clone() { return new XferBuilder( this ); } /** * Get related QV driver * @returns {IDriver} actual implementation of query builder driver */ getDriver() { return QueryBuilder.getDriver( this._db_type ); } /** * Escape value for embedding into raw query * @param {*} value - value, array or sub-query to escape * @returns {string} driver-specific escape */ escape( value ) { return this.helpers().escape( value ); } /** * Escape identifier for embedding into raw query * @param {string} name - raw identifier to escape * @returns {string} driver-specific escape */ identifier( name ) { return this.helpers().identifier( name ); } /** * Wrap raw expression to prevent escaping. * @param {string} expr - expression to wrap * @return {Expression} wrapped expression */ expr( expr ) { return this.helpers().expr( expr ); } /** * Wrap parameter name to prevent escaping. * @param {string} name - name to wrap * @return {Expression} wrapped expression */ param( name ) { return this.helpers().expr( `:${name}` ); } /** * Get additional helpers * @returns {Helpers} - db-specific helpers object */ helpers() { return this.getDriver().helpers; } /** * Get reference to L2 interface. Valid use case - sub-queries. * @returns {L2Face} - associated L2 interface implementation */ lface() { return this._lface; } /** * Get generic query builder * @param {string} type - query type * @param {string|null} entity - man subject * @param {QueryOptions} [query_options={}] - constraints * @returns {XferQueryBuilder} individual query builder instance */ query( type, entity, query_options={} ) { const qb = this._newBuilder( type, entity ); for ( let qo in query_options ) { switch( qo ) { case 'affected': case 'result': case 'selected': break; default: throw new Error( `Invalid query option: ${qo}` ); } } const item = _cloneDeep( query_options ); item.q = qb; this._query_list.push( item ); return qb; } /** * Get DELETE query builder * @param {string|null} entity - man subject * @param {QueryOptions} [query_options={}] - constraints * @returns {XferQueryBuilder} individual query builder instance */ delete( entity, query_options={} ) { return this.query( 'DELETE', entity, query_options ); } /** * Get INSERT query builder * @param {string|null} entity - man subject * @param {QueryOptions} [query_options={}] - constraints * @returns {XferQueryBuilder} individual query builder instance */ insert( entity, query_options={} ) { return this.query( 'INSERT', entity, query_options ); } /** * Get UPDATE query builder * @param {string|null} entity - man subject * @param {QueryOptions} [query_options={}] - constraints * @returns {XferQueryBuilder} individual query builder instance */ update( entity, query_options={} ) { return this.query( 'UPDATE', entity, query_options ); } /** * Get SELECT query builder * @param {string|null} entity - man subject * @param {QueryOptions} [query_options={}] - constraints * @returns {XferQueryBuilder} individual query builder instance */ select( entity, query_options={} ) { return this.query( 'SELECT', entity, query_options ); } /** * Add CALL query * @param {string} name - stored procedure name * @param {array} [args=[]] - positional arguments * @param {QueryOptions} [query_options={}] - constraints */ call( name, args=[], query_options={} ) { const qb = this._newBuilder( 'CALL', name ); qb._callParams( args ); const item = _cloneDeep( query_options ); item.q = qb._toQuery(); this._query_list.push( item ); } /** * Execute raw query * @param {string} q - raw query * @param {object} [params=null] - named argument=>value pairs * @param {QueryOptions} [query_options={}] - constraints * @note Pass null in {@p params}, if you want to use prepare() */ raw( q, params=null, query_options={} ) { const item = _cloneDeep( query_options ); if ( params ) { const helpers = this.helpers(); item.q = QueryBuilder._replaceParams( helpers, q, params ); } else { item.q = q; } this._query_list.push( item ); } /** * Complete query and execute through associated interface. * @param {AsyncSteps} as - steps interface * @param {Boolean} unsafe_dml - raise error, if DML without conditions * @see L1Face.query */ execute( as, unsafe_dml=false ) { const ql = this._query_list; if ( ql.length > 0 ) { this._prepareQueryList( ql, unsafe_dml ); this._lface.xfer( as, ql, this._iso_level ); } else { as.add( ( as ) => as.success( [] ) ); } } /** * Complete query and execute through associated interface. * @param {AsyncSteps} as - steps interface * @param {Boolean} unsafe_dml - raise error, if DML without conditions * @see L1Face.query * @see L1Face.associateResult */ executeAssoc( as, unsafe_dml=false ) { this.execute( as, unsafe_dml ); as.add( this.constructor._assocResult( this._lface ) ); } /** * Prepare statement for efficient execution multiple times * @param {Boolean} unsafe_dml - raise error, if DML without conditions * @returns {ExecPrepared} closue with prepared statement */ prepare( unsafe_dml=false ) { const ql = _cloneDeep( this._query_list ); const isol = this._iso_level; const db_type = this._db_type; const iface = this._lface; this._prepareQueryList( ql, unsafe_dml ); return new class extends QueryBuilder.Prepared { execute( as, params=null ) { if ( params ) { const helpers = QueryBuilder.getDriver( db_type ).helpers; const pql = _cloneDeep( ql ); const used_params = {}; pql.forEach( ( v ) => { v.q = QueryBuilder._replaceParams( helpers, v.q, params, used_params ); } ); for ( let p in params ) { if ( !used_params[p] ) { throw new Error( `Unused param "${p}"` ); } } iface.xfer( as, pql, isol ); } else { iface.xfer( as, ql, isol ); } } executeAssoc( as, params ) { this.execute( as, params ); as.add( XferBuilder._assocResult( iface ) ); } }; } _newBuilder( type, entity=null ) { const res = new XferQueryBuilder( this._lface, this._db_type, type, entity ); res._seq_id = this._query_list.length; return res; } _prepareQueryList( ql, unsafe_dml ) { ql.forEach( ( v ) => { const qb = v.q; if ( qb instanceof XferQueryBuilder ) { if ( qb._state.template ) { v.template = true; qb._state.template = undefined; } v.q = qb._toQuery( unsafe_dml ); // damage on purpose qb._state = null; } } ); } static _assocResult( iface ) { return function( as, res ) { const assoc_res = res.map( ( v ) => { const rows = iface.associateResult( v ); return { rows, affected: v.affected, }; } ); as.success( assoc_res ); }; } } module.exports = XferBuilder;