database-jones/Adapter/api/UserContext.js (1,595 lines of code) (raw):

/* Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. 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 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"; var stats = { "TableHandlerFactory" : 0, "TableHandler" : { "success" : 0, "idempotent" : 0, "cache_hit" : 0 }, "tables_created" : 0 }; var jonesConnections = {}; // a hash of connectionKey to Connection var util = require("util"), jones = require("./jones.js"), DBTableHandler = require(jones.common.DBTableHandler).DBTableHandler, apiSession = require("./Session.js"), sessionFactory = require("./SessionFactory.js"), query = require("./Query.js"), udebug = unified_debug.getLogger("UserContext.js"), TableMapping = require("./TableMapping.js"), meta = require("./Meta.js"), Promise = require("jones-promises"), stats_module = require(jones.api.stats); stats_module.register(stats, "api", "UserContext"); // serial may be useful for debugging var serial = 0; /** Create a function to manage the context of a user's asynchronous call. * All asynchronous user functions make a callback passing * the user's extra parameters from the original call as extra parameters * beyond the specified parameters of the call. For example, the persist function * is specified to take two parameters: the data object itself and the callback. * The result of persist is to call back with parameters of an error object, * and the same data object which was passed. * If extra parameters are passed to the persist function, the user's function * will be called with the specified parameters plus all extra parameters from * the original call. * The constructor remembers the original user callback function and the original * parameters of the function. * The user callback function is always the last required parameter of the function call. * Additional context is added as the function progresses. * @param user_arguments the original arguments as supplied by the user * @param required_parameter_count the number of required parameters * NOTE: the user callback function must be the last of the required parameters * @param returned_parameter_count the number of parameters returned to the callback * @param session the Session which may be null for SessionFactory functions * @param session_factory the SessionFactory which may be null for Session functions * @param execute (optional; defaults to true) whether to execute the operation immediately; * if execute is false, the operation is constructed and is available via the "operation" * property of the user context. */ exports.UserContext = function(user_arguments, required_parameter_count, returned_parameter_count, session, session_factory, execute) { this.serial = ++serial; this.execute = (typeof execute === 'boolean' ? execute : true); this.user_arguments = user_arguments; this.user_callback = user_arguments[required_parameter_count - 1]; if (this.user_callback && typeof this.user_callback !== 'function') { throw new Error('User callback is not a function.'); } this.required_parameter_count = required_parameter_count; this.extra_arguments_count = user_arguments.length - required_parameter_count; this.returned_parameter_count = returned_parameter_count; this.session = session; this.session_factory = session_factory; /* indicates that batch.clear was called before this context had executed */ this.clear = false; if (this.session) { this.autocommit = ! this.session.tx.isActive(); } this.errorMessages = ''; this.promise = new Promise(); this.tableSpecification = undefined; // for getTableHandler this.handlerCtor = undefined; // for getTableHandler }; exports.UserContext.prototype.appendErrorMessage = function(message) { this.errorMessages += '\n' + message; }; /** Get table metadata. * Delegate to DBConnectionPool.getTableMetadata. */ exports.UserContext.prototype.getTableMetadata = function() { var userContext = this; var err, databaseName, tableName, dbSession; function getTableMetadataOnTableMetadata(metadataErr, tableMetadata) { udebug.log('UserContext.getTableMetadata.getTableMetadataOnTableMetadata with err', metadataErr); userContext.applyCallback(metadataErr, tableMetadata); } // getTableMetadata starts here databaseName = userContext.user_arguments[0]; tableName = userContext.user_arguments[1]; if (typeof databaseName !== 'string' || typeof tableName !== 'string') { err = new Error('getTableMetadata(databaseName, tableName) illegal argument types (' + typeof databaseName + ', ' + typeof tableName + ')'); userContext.applyCallback(err, null); } else { dbSession = userContext.session.dbSession; userContext.session_factory.dbConnectionPool.getTableMetadata( databaseName, tableName, dbSession, getTableMetadataOnTableMetadata); } return userContext.promise; }; /** List all tables in the default database. * Delegate to DBConnectionPool.listTables. */ exports.UserContext.prototype.listTables = function() { var userContext = this; var listTablesOnTableList = function(err, tableList) { userContext.applyCallback(err, tableList); }; var databaseName = this.user_arguments[0]; var dbSession = (this.session)?this.session.dbSession:null; this.session_factory.dbConnectionPool.listTables(databaseName, dbSession, listTablesOnTableList); return userContext.promise; }; /** getOpenSessionFactories(): an IMMEDIATE call */ exports.UserContext.prototype.getOpenSessionFactories = function() { var result = []; var x, y; var factory; for (x in jonesConnections) { if (jonesConnections.hasOwnProperty(x)) { for (y in jonesConnections[x].factories) { if (jonesConnections[x].factories.hasOwnProperty(y)) { factory = jonesConnections[x].factories[y]; result.push(factory); } } } } return result; }; function createTableInternal(tableMapping, sessionFactory, session, callback) { var connectionPool = sessionFactory.dbConnectionPool; function createTableOnTableCreated(err) { if (err) { callback(err); } else { stats.tables_created++; callback(); } } // createTableInternal starts here connectionPool.createTable(tableMapping, session, createTableOnTableCreated); } /** Create schema from a table mapping. * promise = createTable(tableMapping, callback); */ exports.UserContext.prototype.createTable = function() { var userContext, tableMapping; userContext = this; tableMapping = this.user_arguments[0]; createTableInternal(tableMapping, this.session_factory, this.session, function(err) { if(err && err.sqlstate == "42S02") { userContext.applyCallback(); } else { userContext.applyCallback(err); } }); return userContext.promise; }; /** Drop table * */ exports.UserContext.prototype.dropTable = function() { var dbName, tableName, nameParts, userContext; if(typeof this.user_arguments[0] === 'string') { tableName = this.user_arguments[0]; nameParts = tableName.split("."); if(nameParts.length == 2) { // database.table dbName = nameParts[0]; tableName = nameParts[1]; } else { dbName = this.session_factory.properties.database; } } else if(this.user_arguments[0] && this.user_arguments[0].table) { dbName = this.user_arguments[0].database; tableName = this.user_arguments[0].table; } else { this.applyCallback(new Error('dropTable() illegal argument: ' + 'must be string table name or TableMapping')); } userContext = this; this.session_factory.dbConnectionPool.dropTable(dbName, tableName, this.session, function(err) { userContext.applyCallback(err); }); return this.promise; }; /** Drop and create schema from a table mapping. * promise = dropAndCreateTable(tableMapping, callback); */ exports.UserContext.prototype.dropAndCreateTable = function() { var userContext, tableMapping, dbName, tableName; userContext = this; function dropAndCreateTableOnCreateTable(createErr) { udebug.log('UserContext.dropAndCreateTable error on create table:', createErr); userContext.applyCallback(createErr); } function dropAndCreateTableOnDropTable(dropErr) { udebug.log('UserContext.dropAndCreateTable error on drop table:', dropErr); if(dropErr) { userContext.applyCallback(dropErr); } else { createTableInternal(tableMapping, userContext.session_factory, userContext.session, dropAndCreateTableOnCreateTable); } } // dropAndCreateTable starts here tableMapping = this.user_arguments[0]; if(tableMapping && tableMapping.table) { dbName = tableMapping.database; tableName = tableMapping.table; this.session_factory.dbConnectionPool.dropTable(dbName, tableName, this.session, dropAndCreateTableOnDropTable); } else { this.applyCallback(new Error('dropAndCreateTable() illegal argument: must be TableMapping')); } return userContext.promise; }; /** Resolve properties. Properties might be an object, a name, or null. * If null, use all default properties. If a name, use default properties * of the named service provider. Otherwise, return the properties object. */ // FIXME Should not default to NDB var resolveProperties = function(properties) { // Properties can be a string adapter name. It defaults to 'ndb'. if(typeof properties === 'string') { properties = jones.getDBServiceProvider(properties).getDefaultConnectionProperties(); } else if (properties === null) { properties = jones.getDBServiceProvider('ndb').getDefaultConnectionProperties(); } return properties; }; /** Construct an error by copying the first error in the user context object. * @param user context * @return a new Error with the first error reported */ function reportFirstError(userContext) { var err = null; var firstError = userContext.firstError; // if any errors during table mapping, report them if (userContext.errorMessages) { err = new Error(userContext.errorMessages); // fill in the Error detail from the first error err.sqlstate = firstError.sqlstate; err.code = firstError.code; err.classification = firstError.classification; err.status = firstError.status; } return err; } /** Get the table handler for a table name, constructor, or domain object. * Delegate this to SessionFactory. */ function getTableHandler(userContext, domainObjectTableNameOrConstructor, session, onTableHandler) { var param = domainObjectTableNameOrConstructor; session.sessionFactory.getTableHandler(userContext, param, session, onTableHandler); } function resolveTableMappings(userContext, factory, session, tableMappings, onMappingsResolvedCallback) { var mappings = []; var mappingBeingResolved = 0; var currentTableMapping, currentTableMappingType, currentTableMappingName, currentTableName; var message; function resolveTableMappingsOnTableHandler(err, tableHandler) { if(udebug.is_detail()) { udebug.log('UserContext.resolveTableMappinsgOnTableHandler got err', err, 'tableHandler', tableHandler, '\nfor', mappingBeingResolved + 1, 'of', mappings.length, '\n', mappings[mappingBeingResolved]); } if (err) { userContext.firstError = userContext.firstError || err; // what were we resolving? currentTableMapping = mappings[mappingBeingResolved]; currentTableMappingType = typeof currentTableMapping; currentTableName = currentTableMapping; message = currentTableName; if (currentTableMappingType === 'function') { currentTableMappingName = currentTableMapping.prototype.constructor.name; if (currentTableMapping.prototype.jones !== undefined) { currentTableName = currentTableMapping.prototype.jones.mapping.table; message = currentTableName + ' for domain object ' + currentTableMappingName; } } userContext.appendErrorMessage('Error resolving table ' + message + ': ' + util.inspect(err)); } if (++mappingBeingResolved === mappings.length || mappingBeingResolved > 20) { onMappingsResolvedCallback(); } else { // get the table handler for the next one, and so on until all are done getTableHandler(userContext, mappings[mappingBeingResolved], session, resolveTableMappingsOnTableHandler); } } function typeofTableMapping(mapping) { var type = typeof mapping; if (type == 'object') { if (Array.isArray(mapping)) { type = 'array'; } else if (tableMappings.constructor && tableMappings.constructor.name === 'TableMapping') { type = 'tablemapping'; } else { type = 'illegal'; } } return type; } // resolveTableMappings begins here var tableMappingsType = typeofTableMapping(tableMappings); var tableMapping; var tableMappingType; var m; switch (tableMappingsType) { case 'string': mappings.push(tableMappings); break; case 'function': mappings.push(tableMappings); break; case 'array': if (tableMappings.length) { for (m = 0; m < tableMappings.length; ++m) { tableMapping = tableMappings[m]; tableMappingType = typeofTableMapping(tableMapping); if (tableMappingType === 'function' || tableMappingType === 'string' || tableMappingType === 'tableMapping') { mappings.push(tableMapping); } else { userContext.appendErrorMessage('unknown table mapping' + util.inspect(tableMapping)); } } } else { userContext.appendErrorMessage('unknown table mappings' + util.inspect(tableMappings)); } break; case 'tablemapping': mappings.push(tableMappings); break; case 'illegal': break; default: userContext.appendErrorMessage('unknown table mappings' + util.inspect(tableMappings)); break; } if (mappings.length === 0) { if(udebug.is_detail()) { udebug.log('resolveTableMappingsOnSession no mappings!'); } onMappingsResolvedCallback(); } else { // get table handler for the first; the callback will then do the next one... if(udebug.is_detail()) { udebug.log('getSessionFactory resolving mappings:', mappings); } getTableHandler(userContext, mappings[0], session, resolveTableMappingsOnTableHandler); } } /** Try to find an existing session factory by looking up the connection string * and database name. Failing that, create a db connection pool and create a session factory. * Multiple session factories share the same db connection pool. * This function is used by both connect and openSession. */ var getSessionFactory = function(userContext, properties, tableMappings, callback) { var database; var dbServiceProvider; var connectionKey; var connection; var factory; var newSession; var sp; var i; function Connection(connectionKey) { this.connectionKey = connectionKey; this.factories = {}; this.count = 0; this.isConnecting = true; this.waitingForConnection = []; } function newConnection(connectionKey) { var c = new Connection(connectionKey); jonesConnections[connectionKey] = c; return c; } function getConnection(connectionKey) { return jonesConnections[connectionKey]; } function deleteFactory(key, database, callback) { udebug.log('deleteFactory for key', key, 'database', database); var c = jonesConnections[key]; var f = c.factories[database]; var dbConnectionPool = f.dbConnectionPool; delete c.factories[database]; if (--connection.count === 0) { // no more factories in this connection udebug.log('deleteFactory closing dbConnectionPool for key', key, 'database', database); dbConnectionPool.close(callback); dbConnectionPool = null; delete jonesConnections[key]; } else { callback(); } } function resolveTableMappingsAndCallback() { function onMappingsResolved() { // close the session the hard way (not using UserContext) newSession.dbSession.close(function(err) { if (err) { callback(err, null); } else { // now remove the session from the session factory's open connections newSession.sessionFactory.closeSession(newSession.index); // mark this session as unusable newSession.closed = true; // if any errors during table mapping, report them if (userContext.errorMessages) { err = reportFirstError(userContext); callback(err, null); } else { // no errors callback(null, factory); } } }); } // resolveTableMappingsAndCallback starts here if (!tableMappings) { callback(null, factory); } else { // get a session the hard way (not using UserContext) to resolve mappings var sessionSlot = factory.allocateSessionSlot(); factory.dbConnectionPool.getDBSession(userContext.session_index, function(err, dbSession) { if (err) { // report error userContext.appendErrorMessage(err); err = new Error(userContext.errorMessages); callback(err, null); } else { newSession = new apiSession.Session(sessionSlot, factory, dbSession); factory.sessions[sessionSlot] = newSession; resolveTableMappings(userContext, factory, newSession, tableMappings, onMappingsResolved); } }); } } function createFactory(dbConnectionPool) { var newFactory; udebug.log('connect createFactory creating factory for', connectionKey, 'database', database); newFactory = new sessionFactory.SessionFactory(connectionKey, dbConnectionPool, properties, tableMappings, deleteFactory); return newFactory; } function dbConnectionPoolCreated_callback(error, dbConnectionPool) { if (connection.isConnecting) { // the first requester for this connection connection.isConnecting = false; // remember the error condition connection.error = error; if (error) { callback(error, null); } else { udebug.log('dbConnectionPool created for', connectionKey, 'database', database); connection.dbConnectionPool = dbConnectionPool; factory = createFactory(dbConnectionPool); connection.factories[database] = factory; connection.count++; resolveTableMappingsAndCallback(); } // notify all others that the connection is now ready (or an error was signaled) for (i = 0; i < connection.waitingForConnection.length; ++i) { if(udebug.is_detail()) { udebug.log('dbConnectionPoolCreated_callback notifying...'); } connection.waitingForConnection[i](error, dbConnectionPool); } } else { // another user request created the dbConnectionPool and session factory if (error) { callback(error, null); } else { udebug.log('dbConnectionPoolCreated_callback', database, connection.factories); factory = connection.factories[database]; if (!factory) { factory = createFactory(dbConnectionPool); connection.factories[database] = factory; connection.count++; } resolveTableMappingsAndCallback(); } } } // getSessionFactory starts here database = properties.database; dbServiceProvider = jones.getDBServiceProvider(properties.implementation); connectionKey = dbServiceProvider.getFactoryKey(properties); connection = getConnection(connectionKey); if(connection === undefined) { // there is no connection yet using this connection key udebug.log('connect connection does not exist; creating factory for', connectionKey, 'database', database); connection = newConnection(connectionKey); sp = jones.getDBServiceProvider(properties.implementation); sp.connect(properties, dbConnectionPoolCreated_callback); } else { // there is a connection, but is it already connected? if (connection.isConnecting) { // wait until the first requester for this connection completes udebug.log('connect waiting for db connection by another for', connectionKey, 'database', database); connection.waitingForConnection.push(dbConnectionPoolCreated_callback); } else { // there is a connection, but is there a SessionFactory for this database? factory = connection.factories[database]; if ( factory === undefined) { if (!connection.dbConnectionPool) { // this connection is unusable due to failure reported in connection.error callback(connection.error); return; } // create a SessionFactory for the existing dbConnectionPool udebug.log('connect creating factory with existing', connectionKey, 'database', database); factory = createFactory(connection.dbConnectionPool); connection.factories[database] = factory; connection.count++; } // resolve all table mappings before returning resolveTableMappingsAndCallback(); } } }; exports.UserContext.prototype.connect = function() { var userContext = this; // properties might be null, a name, or a properties object this.user_arguments[0] = resolveProperties(this.user_arguments[0]); function connectOnSessionFactory(err, factory) { userContext.applyCallback(err, factory); } getSessionFactory(this, this.user_arguments[0], this.user_arguments[1], connectOnSessionFactory); return userContext.promise; }; function checkOperation(err, dbOperation) { var sqlstate, message, result, result_code; result = null; result_code = null; message = 'Unknown Error'; sqlstate = '22000'; if (err) { udebug.log('checkOperation returning existing err:', err); return err; } if (dbOperation.result.success !== true) { if(dbOperation.result.error) { sqlstate = dbOperation.result.error.sqlstate; message = dbOperation.result.error.message || 'Operation error'; result_code = dbOperation.result.error.code; } result = new Error(message); result.code = result_code; result.sqlstate = sqlstate; udebug.log('checkOperation returning new err:', result); } return result; } /** Create a sector object for a domain object in a projection. */ function Sector() { this.index = -1; // will be filled by createSector this.keyFields = []; // array of FieldMapping this.keyFieldNames = []; this.keyFieldCount = 0; this.nonKeyFields = []; // ? this.nonKeyFieldCount = 0; this.projection = null; this.offset = 0; this.tableHandler = null; this.parentFieldMapping = null; this.parentTableHandler = null; this.joinTableHandler = null; this.thisJoinColumns = []; this.otherJoinColumns = []; this.toManyRelationships = []; this.toOneRelationships = []; this.parentSectorIndex = 0; this.childSectorIndexes = []; } Sector.prototype[util.inspect.custom] = function() { var s = "Sector " + this.index + " for " + this.tableHandler.dbTable.name; if(this.joinTableHandler) { s += " with join table " + this.joinTableHandler.dbTable.name; } if(this.thisJoinColumns.length) { s += " where this." + this.thisJoinColumns.join(",") + "=" + this.parentTableHandler.dbTable.name + "." + this.otherJoinColumns.join(","); } else { s+= " with keys [" + this.keyFieldNames.join(",") + "]"; } s += " at offset " + this.offset; s += " with parent sector " + this.parentSectorIndex; return s; }; /** * Recursively create sector objects, each describing a projection in the context of parent projections. * The topmost outer loop projection's sectors are all created, creating one sector * for each inner loop projection, then the outer projection for the next level down. * For each inner loop projection, a sector is constructed * and then the sector for the included relationships are constructed by recursion. * * The sector contains a list of primary key fields and a list of non-primary key fields, * and if this is not the root sector, the name of the field and the field in the previous sector. * The fields are references to the field objects in DBTableHandler and contain names, types, * and converters. * This function is synchronous. When complete, this function returns to the caller. * After creating the sectors, the projection is read-only and each projection can independently * be used as the parameter of find or createQuery. * Example: * Customer projection contains relationships to ShoppingCart projection and Discount projection * ShoppingCart projection contains relationship to LineItem projection * * Projection #sectors sectors created * ------------ -------- ------------------------------------ * Customer 4 Customer, ShoppingCart, LineItem, and Discount * ShoppingCart 2 ShoppingCart and LineItem * LintItem 1 LineItem * Discount 1 Discount * * @param outerLoopProjections the array of projections that can be used for query and find operations. * The first element is the currently working projection for which to build the sectors * used to process results from the database. As related projections are encountered while * processing this projection, they are added to the end of the array. * The first time, this parameter is an array of exactly one projection, the parameter * for query and find operations. * @param innerLoopProjections the related projections, each of which is used to process part of * the results from the database. The first element is the current projection for which the * sector is being built, to be added to the sectors owned by the outerLoopProjection. * This parameter is modified by this function as related projections are found. * The first time, this parameter is an array of exactly one projection, the parameter * for query and find operations. * @param sectors the outer loop projection.sectors which will grow as createSector is called recursively * @param index the index into sectors for the sector being constructed * @param offset the number of fields in all sectors already processed */ function createSector(outerLoopProjections, innerLoopProjections, sectors, index, offset) { if (udebug.is_debug()) {udebug.log('createSector ' + outerLoopProjections[0].name + ' for ' + outerLoopProjections[0].domainObject.name + ' inner: ' + innerLoopProjections[0].name + ' for ' + innerLoopProjections[0].domainObject.name + ' index: ' + index + ' offset: ' + offset);} var sector = new Sector(); sector.index = index; var projection = innerLoopProjections.shift(); var outerNestedProjection; var tableHandler; var keyFieldCount; var fieldNames, field, candidateField; var indexHandler; var parentFieldMapping, parentSectorIndex, parentTableHandler, parentSectorProjection; var parentTargetFieldName, parentTargetField; var thisFieldMapping; var joinTable, joinTableHandler; var foreignKey, foreignKeyName; var i; var projectionRelationshipName; sector.projection = projection; sector.offset = offset; tableHandler = projection.domainObject.prototype.jones.dbTableHandler; sector.tableHandler = tableHandler; // parentFieldMapping is the field mapping for the parent sector // it contains the field in the parent sector and mapping information including join columns parentFieldMapping = projection.parentFieldMapping; sector.parentFieldMapping = parentFieldMapping; if (udebug.is_detail()) {udebug.log_detail('createSector for table handler', tableHandler.dbTable.name, 'thisDBTable name', tableHandler.dbTable.name);} if (parentFieldMapping && index !== 0) { // only perform related field mapping for nested projections // find the parent sector which will be somewhere between 0 and immediately to the left for (parentSectorIndex = 0; parentSectorIndex < index; ++parentSectorIndex) { parentSectorProjection = sectors[parentSectorIndex].projection; if (parentSectorProjection === projection.parentProjection) { // found it sector.parentSectorIndex = parentSectorIndex; sectors[parentSectorIndex].childSectorIndexes.push(index); break; } } if (parentSectorIndex == index) { projection.error += 'did not find parent sector for ' + projection.parentProjection.name; } else { parentTableHandler = projection.parentTableHandler; sector.parentTableHandler = parentTableHandler; // get this optional field mapping that corresponds to the parent field mapping // it may be needed to find the foreign key or join table parentTargetFieldName = parentFieldMapping.targetField; parentTargetField = sector.tableHandler.getFieldMapping(parentTargetFieldName); if (parentFieldMapping.toMany && parentFieldMapping.manyTo) { // this is a many-to-many relationship using a join table joinTable = parentFieldMapping.joinTable; // joinTableHandler is the DBTableHandler for the join table resolved during validateProjection if (joinTable) { // join table is defined on the related side joinTableHandler = parentFieldMapping.joinTableHandler; } else { // join table must be defined on this side thisFieldMapping = tableHandler.getFieldMapping(parentFieldMapping.targetField); joinTable = thisFieldMapping.joinTable; if (!joinTable) { // error; neither side defined the join table projection.error += '\nMappingError: ' + parentTableHandler.newObjectConstructor.name + ' field ' + parentFieldMapping.fieldName + ' neither side defined the join table.'; } joinTableHandler = thisFieldMapping.joinTableHandler; } sector.joinTableHandler = joinTableHandler; // many to many relationship has a join table with at least two foreign keys; // one to each table mapped to the two domain objects if (joinTable) { joinTableHandler.getForeignKeyNames().forEach(function(foreignKeyName) { foreignKey = joinTableHandler.getForeignKey(foreignKeyName); // is this foreign key for this table? if (foreignKey.targetDatabase === tableHandler.dbTable.database && foreignKey.targetTable === tableHandler.dbTable.name) { // this foreign key is for the other table parentFieldMapping.otherForeignKey = foreignKey; } if (foreignKey.targetDatabase === parentTableHandler.dbTable.database && foreignKey.targetTable === parentTableHandler.dbTable.name) { parentFieldMapping.thisForeignKey = foreignKey; } }); if (!(parentFieldMapping.thisForeignKey && parentFieldMapping.otherForeignKey)) { // error must have foreign keys to both this table and related table projection.error += '\nMappingError: ' + parentTableHandler.newObjectConstructor.name + ' field ' + parentFieldMapping.fieldName + ' join table must include foreign keys for both sides.'; } } } else { // this is a relationship using a foreign key // resolve the columns involved in the join to the related field // there is either a foreign key or a target field that has a foreign key // the related field mapping is the field mapping on the other side // the field mapping on this side is not used in this projection foreignKeyName = parentFieldMapping.foreignKey; if (foreignKeyName) { // foreign key is defined on the other side foreignKey = parentTableHandler.getForeignKey(foreignKeyName); sector.thisJoinColumns = foreignKey.targetColumnNames; sector.otherJoinColumns = foreignKey.columnNames; } else { // foreign key is defined on this side // get the fieldMapping for this relationship field parentTargetField = sector.tableHandler.getFieldMapping(parentTargetFieldName); foreignKeyName = parentTargetField.foreignKey; if (foreignKeyName) { foreignKey = tableHandler.getForeignKey(foreignKeyName); sector.thisJoinColumns = foreignKey.columnNames; sector.otherJoinColumns = foreignKey.targetColumnNames; } else { // error: neither side defined the foreign key projection.error += 'MappingError: ' + parentTableHandler.newObjectConstructor.name + ' field ' + parentFieldMapping.fieldName + ' neither side defined the foreign key.'; } } } } } // create relationship field list for object creation if (projection.relationships) { var relationship; // for each relationship add to either sector.toOneRelationships or sector.toManyRelationships for (projectionRelationshipName in projection.relationships) { if (projection.relationships.hasOwnProperty(projectionRelationshipName)) { relationship = projection.relationships[projectionRelationshipName]; // add this relationship to the list of projections to create a sector for innerLoopProjections.push(relationship); if (outerLoopProjections.indexOf(relationship) == -1) { outerLoopProjections.push(relationship); } // find field for relationship for (i = 0; i < projection.mapping.fields.length; ++i) { candidateField = projection.mapping.fields[i]; if (projectionRelationshipName === candidateField.fieldName) { if (candidateField.toMany) { sector.toManyRelationships.push(projectionRelationshipName); } else { sector.toOneRelationships.push(projectionRelationshipName); } } } } } } if (udebug.is_detail()) { udebug.log_detail('createSector for', projection.name, 'has toManyRelationships:', sector.toManyRelationships); udebug.log_detail('createSector for', projection.name, 'has toOneRelationships:', sector.toOneRelationships); } // create key fields from primary key index handler indexHandler = tableHandler.dbIndexHandlers[0]; keyFieldCount = indexHandler.getNumberOfFields(); sector.keyFieldCount = keyFieldCount; for (i = 0; i < keyFieldCount; ++i) { field = indexHandler.getField(i); sector.keyFields.push(field); sector.keyFieldNames.push(field.fieldName); } // create non-key fields from projection fields excluding key fields fieldNames = projection.fields; fieldNames.forEach(function(fieldName) { // is this field in key fields? if (sector.keyFieldNames.indexOf(fieldName) == -1) { // non-key field; add it to non-key fields field = tableHandler.getFieldMapping(fieldName); sector.nonKeyFields.push(field); } }); sector.nonKeyFieldCount = sector.nonKeyFields.length; udebug.log_detail('createSector created new sector for index', index, 'sector', sector); // the sector is now complete sectors.push(sector); // innerLoopProjections contains the array of sectors to create if (innerLoopProjections.length > 0) { createSector(outerLoopProjections, innerLoopProjections, sectors, index + 1, offset + keyFieldCount + sector.nonKeyFieldCount); } // we are done at this outer projection level; if (udebug.is_debug() && outerLoopProjections[0] && outerLoopProjections[0].name) { udebug.log('createSector for ' + outerLoopProjections[0].name + ' created ' + outerLoopProjections[0].sectors.length + ' sectors for ' + outerLoopProjections[0].domainObject.name); } // now go to the outer projection next level down and do it all over again outerLoopProjections.shift(); // get rid of the projection we just finished if (outerLoopProjections.length > 0) { outerNestedProjection = outerLoopProjections[0]; outerNestedProjection.sectors = []; createSector(outerLoopProjections, [outerNestedProjection], outerNestedProjection.sectors, 0, 0); } } /** Mark all projections reachable from this projection as validated. */ function markValidated(projections) { var projection, relationships, relationshipName; if (projections.length > 0) { // "pop" the top projection projection = projections.shift(); // mark the top projection validated projection.validated = true; // if any relationships, add them to the list of projections to validate relationships = projection.relationships; if (relationships) { for (relationshipName in relationships) { if (relationships.hasOwnProperty(relationshipName)) { projections.push(relationships[relationshipName]); } } } // recursively mark related projections markValidated(projections); } } /** Collect errors from all projections reachable from this projection */ function collectErrors(projections, errors) { var projection, relationships, relationshipName; if (projections.length > 0) { // "pop" the top projection projection = projections.shift(); // check the top projection for errors errors += projection.error; // if any relationships, add them to the list of projections to validate relationships = projection.relationships; if (relationships) { for (relationshipName in relationships) { if (relationships.hasOwnProperty(relationshipName)) { projections.push(relationships[relationshipName]); } } } } else { return errors; } return collectErrors(projections, errors); } /** Validate the projection for find and query operations on the domain object. * This function is the entry point from UserContext.find and UserContext.createQuery. * * this.user_arguments[0] contains the projection for this operation * (first parameter of find or createQuery). * Validation occurs in two phases. The first phase individually validates * each domain object associated with a projection. The second phase, * implemented as createSector, validates relationships among the domain objects. * * In the first phase, get the table handler for the domain object and validate * that it is mapped. Then validate each field in the projection against mapped column. * For relationships, validate the name of the relationship. Validate that there * is no projected domain object that would cause an infinite recursion. * If there is a join table that implements the relationship, validate that the * join table exists by loading its metadata. * Store the field mapping for this relationship in the related projection. * The related field mapping will be further processed in the second phase, * once the table metadata for both domain objects has been loaded. * Recursively validate the projection that is defined as the relationship. * During recursion, the value of projections will grow as projections are added * on the end. The value of index will change as projections are validated in * the first phase. Recursion will end once no new projections (from relationships) * are added. * * In the second phase, create the array of sectors for use when this projection * is used in an api call. Each projection will have its own array of sectors. * During the creation of each sector, validate that the relationship is mapped * with valid foreign keys and join tables in the database. * * After all projections have been validated, mark all projections as validated * and call the callback with any errors. */ exports.UserContext.prototype.validateProjection = function(callback) { var userContext = this; var session = userContext.session; var err; var domainObject, domainObjectName; var projections, projection; var mappingIds, mappingId; var relationships, childProjection; var fieldMapping; var index; var errors; var foreignKeyName; var toBeValidated; var domainObjectMynode; var joinTableRelationshipField, joinTableRelationshipFields = []; var continueValidation; function validateJoinTableOnTableHandler(err, joinTableHandler) { udebug.log_detail('validateJoinTableOnTableHandler for', joinTableRelationshipField.joinTable, 'err:', err); if (err) { // mark the projection as broken errors += '\nBad projection for ' + domainObjectName + ': field ' + joinTableRelationshipField.fieldName + ' join table ' + joinTableRelationshipField.joinTable + ' failed: ' + err.message; } else { // continue validating projections // we cannot do any more until both sides have their table handlers udebug.log_detail('validateJoinTableOnTableHandler resolved table handler for ', domainObjectName, ': field', joinTableRelationshipField.fieldName, 'join table', joinTableRelationshipField.joinTable); // store the join table handler in the related field mapping joinTableRelationshipField.joinTableHandler = joinTableHandler; } // finished this join table; continue with more join tables or more tables mapped to domain objects joinTableRelationshipField = joinTableRelationshipFields.shift(); if (joinTableRelationshipField) { getTableHandler(userContext, joinTableRelationshipField.joinTable, session, validateJoinTableOnTableHandler); } else { continueValidation(); } } function validateProjectionOnTableHandler(err, dbTableHandler) { // currently validating projections[index] with the tableHandler for the domain object projection = projections[index]; // keep track of how many times this projection has been changed so adapters know when to re-validate projection.id = (projection.id + 1) % (2^24); projection.childProjections = []; domainObject = projection.domainObject; domainObjectName = domainObject.prototype.constructor.name; domainObjectMynode = domainObject.prototype.jones; if (domainObjectMynode && domainObjectMynode.mapping.error) { // remember errors in mapping errors += domainObjectMynode.mapping.error; } if (!err) { projection.dbTableHandler = dbTableHandler; // validate projected fields against columns using table handler if (typeof domainObject === 'function' && typeof domainObject.prototype.jones === 'object' && typeof domainObject.prototype.jones.mapping === 'object') { projection.mapping = domainObject.prototype.jones.mapping; // good domainObject; have we seen this one before? mappingId = domainObject.prototype.jones.mappingId; if (mappingIds.indexOf(mappingId) === -1) { // have not seen this one before; add its mappingId to list of mappingIds to prevent cycles (recursion) mappingIds.push(mappingId); // validate all fields in projection are mapped if (projection.fields) { // field names projection.fields.forEach(function(fieldName) { fieldMapping = dbTableHandler.getFieldMapping(fieldName); if (fieldMapping) { if (fieldMapping.relationship) { errors += '\nBad projection for ' + domainObjectName + ': field' + fieldName + ' must not be a relationship'; } } else { // error: fields must be mapped errors += '\nBad projection for ' + domainObjectName + ': field ' + fieldName + ' is not mapped'; } }); } // validate all relationships in mapping regardless of whether they are in this projection dbTableHandler.relationshipFields.forEach(function(relationshipField) { // get the name and projection for each relationship foreignKeyName = relationshipField.foreignKey; if (foreignKeyName) { // make sure the foreign key exists if (!dbTableHandler.getForeignKey(foreignKeyName)) { errors += '\nBad relationship field mapping; foreign key ' + foreignKeyName + ' does not exist in table; possible foreign keys are: ' + dbTableHandler.getForeignKeyNames(); } } // remember this relationship in order to resolve table mapping for join table if (relationshipField.joinTable) { joinTableRelationshipFields.push(relationshipField); } }); // add relationship domain objects to the list of domain objects relationships = projection.relationships; if (relationships) { Object.keys(relationships).forEach(function(key) { // each key is the name of a relationship that must be a field in the table handler fieldMapping = dbTableHandler.getFieldMapping(key); if (fieldMapping) { if (fieldMapping.relationship) { childProjection = relationships[key]; if (childProjection.parentProjection && childProjection.parentProjection !== projection) { // this child projection is already being used by a different projection errors += '\nBad relationship for ' + domainObjectName + ': field ' + key + ' of type ' + childProjection.domainObject.name + ' is already in use by a different projection.'; } else { childProjection.parentTableHandler = dbTableHandler; childProjection.parentFieldMapping = fieldMapping; childProjection.parentProjection = projection; // add each relationship to the current list of projections to be validated projections.push(childProjection); projection.childProjections.push(childProjection); } } else { // error: field is not a relationship errors += '\nBad relationship for ' + domainObjectName + ': field ' + key + ' is not a relationship.'; } } else { // error: relationships must be mapped errors += '\nBad relationship for ' + domainObjectName + ': relationship ' + key + ' is not mapped.'; } }); } } else { // recursive projection errors += '\nRecursive projection for ' + domainObjectName; } } else { // domainObject was not mapped errors += '\nBad domain object: ' + domainObjectName + ' is not mapped.'; } } else { // table does not exist errors += '\nUnable to acquire tableHandler for ' + domainObjectName + ' : ' + err.message; } // finished validating this projection; do we have a join table to validate? if (joinTableRelationshipFields.length > 0) { // get the table handler for the first join table joinTableRelationshipField = joinTableRelationshipFields.shift(); getTableHandler(userContext, joinTableRelationshipField.joinTable, session, validateJoinTableOnTableHandler); } else { continueValidation(); } } // continue validation from either projection domain object or relationship join table continueValidation = function() { // are there any more? if (projections.length > ++index) { projection = projections[index]; if (projection.error != '') { udebug.log('continueValidation projection.error:', projection.error); // this projection is in error so don't process it any more errors += projection.error; // go on to the next projection continueValidation(); } else { // do the next projection; see if the domain object already has its table handler if (projections[index].domainObject.prototype.jones.dbTableHandler) { udebug.log('continueValidation with cached tableHandler for', projections[index].domainObject.name); validateProjectionOnTableHandler(null, projections[index].domainObject.prototype.jones.dbTableHandler); } else { // get the table handler the hard way (asynchronously) udebug.log('continueValidation with no cached tableHandler for', projections[index].domainObject.name); getTableHandler(userContext, projections[index].domainObject, session, validateProjectionOnTableHandler); } } } else { // there are no more projections to validate -- did another user finish table handling first? if (!userContext.user_arguments[0].validated) { // we are the first to validate table handling -- check for errors if (!errors) { projection = projections[0]; // no errors yet // we are done getting all of the table handlers for the projection; now create the sectors projection.sectors = []; // create the first sector; additional sectors will be created recursively // the first sector describes the top level projection; each subsequent sector // describes a nested projection in the top level projection createSector([projection], [projection], projection.sectors, /*index*/ 0, /*offset*/ 0); // now look for errors found during createSector errors = collectErrors([userContext.user_arguments[0]], ''); // mark all projections reachable from this projections as validated // projections will grow at the end as validated marking proceeds if (!errors) { // no errors in createSector toBeValidated = [userContext.user_arguments[0]]; markValidated(toBeValidated); udebug.log('validateProjection complete for', projections[0].domainObject.name); callback(null); return; } } // report errors and call back user if (errors) { udebug.log('validateProjection had errors:\n', errors); err = new Error(errors); err.sqlstate = 'HY000'; } } callback(err); } }; // validateProjection starts here // projection: { // domainObject:<constructor>, // fields: [field, field], // relationships: { // field: {projection}, // field: {projection // } // } // first check to see if the projection is already validated. If so, we are done. // the entire projection including all referenced relationships must be checked because a relationship // might have changed since it was last validated. // projections will grow at the end as validation checking proceeds // construct a new array which will grow as validation checking proceeds if (userContext.user_arguments[0].validated) { callback(null); } else { // set up to iteratively validate projection starting with the user parameter projections = [this.user_arguments[0]]; // projections will grow at the end as validation proceeds if (udebug.is_debug()) {udebug.log('validateProjection for', projections[0].name, 'for domain object:', projections[0].domainObject.prototype.constructor.name, 'with projection error:', projections[0].error);} index = 0; // index into projections for the projection being validated errors = projections[0].error; // initialize errors in validation mappingIds = []; // mapping ids seen so far // the projection is not already validated; check to see if the domain object already has its dbTableHandler domainObjectMynode = projections[0].domainObject.prototype.jones; if (domainObjectMynode && domainObjectMynode.dbTableHandler) { udebug.log('validateProjection with cached tableHandler for', projections[0].domainObject.name); validateProjectionOnTableHandler(null, domainObjectMynode.dbTableHandler); } else { // get the dbTableHandler the hard way udebug.log('validateProjection with no tableHandler for', projections[0].domainObject.name); getTableHandler(userContext, projections[index].domainObject, userContext.session, validateProjectionOnTableHandler); } } }; /** Use the projection to find a domain object. This is only valid in a session, not a batch. * Multiple operations may be needed to resolve the complete projection. * Take the user's projection and see if it has been resolved. For an unresolved projection, * load table mappings for all included domain objects and verify the projection against * the resolved table mappings. * Once the projection has been resolved, get the db index to use for the operation, * call db session to create a read with projection operation, and execute the operation. * The db session will process the projection to populate the result. */ exports.UserContext.prototype.findWithProjection = function() { var userContext = this; var session = userContext.session; var dbSession = session.dbSession; var projection = userContext.user_arguments[0]; var keys = userContext.user_arguments[1]; var dbTableHandler; var indexHandler; var transactionHandler; function findWithProjectionOnResult(err, dbOperation) { udebug.log('find.findWithProjectionOnResult'); var error = checkOperation(err, dbOperation); if (error && dbOperation.result.error.sqlstate !== '02000') { if (userContext.session.tx.isActive()) { userContext.session.tx.setRollbackOnly(); } userContext.applyCallback(err, null); } else { if(udebug.is_detail()) { udebug.log('findOnResult returning ', dbOperation.result.value); } userContext.applyCallback(null, dbOperation.result.value); } } function onValidatedProjection(err) { if (err) { udebug.log('UserContext.onValidatedProjection err: ', err); userContext.applyCallback(err, null); } else { dbTableHandler = projection.dbTableHandler; userContext.dbTableHandler = dbTableHandler; keys = userContext.user_arguments[1]; indexHandler = dbTableHandler.getUniqueIndexHandler(keys); if (indexHandler === null) { err = new Error('UserContext.find unable to get an index for ' + dbTableHandler.dbTable.name + ' to use with ' + JSON.stringify(keys)); userContext.applyCallback(err, null); } else { // create the find operation and execute it dbSession = userContext.session.dbSession; transactionHandler = dbSession.getTransactionHandler(); userContext.operation = dbSession.buildReadProjectionOperation(indexHandler, keys, projection, transactionHandler, findWithProjectionOnResult); if (userContext.execute) { transactionHandler.execute([userContext.operation], function() { if(udebug.is_detail()) { udebug.log('find transactionHandler.execute callback.'); } }); } else if (typeof(userContext.operationDefinedCallback) === 'function') { userContext.operationDefinedCallback(1); } } } } // findWithProjection starts here // validate the projection and construct the sectors userContext.validateProjection(onValidatedProjection); // the caller will return userContext.promise }; /** Find the object by key. * */ exports.UserContext.prototype.find = function() { var userContext = this; if (typeof this.user_arguments[0] === 'function') { userContext.domainObject = true; } function findOnResult(err, dbOperation) { udebug.log('find.findOnResult'); var error = checkOperation(err, dbOperation); if (error && dbOperation.result.error.sqlstate !== '02000') { if (userContext.session.tx.isActive()) { userContext.session.tx.setRollbackOnly(); } userContext.applyCallback(err, null); } else { if(udebug.is_detail()) { udebug.log('findOnResult returning ', dbOperation.result.value); } userContext.applyCallback(null, dbOperation.result.value); } } function findOnTableHandler(err, dbTableHandler) { var dbSession, keys, index, transactionHandler; if (userContext.clear) { // if batch has been cleared, user callback has already been called return; } if (err) { userContext.applyCallback(err, null); } else { userContext.dbTableHandler = dbTableHandler; keys = userContext.user_arguments[1]; index = dbTableHandler.getUniqueIndexHandler(keys); if (index === null) { err = new Error('UserContext.find unable to get an index to use for ' + JSON.stringify(keys)); userContext.applyCallback(err, null); } else { // create the find operation and execute it dbSession = userContext.session.dbSession; transactionHandler = dbSession.getTransactionHandler(); userContext.operation = dbSession.buildReadOperation(index, keys, transactionHandler, false, findOnResult); if (userContext.execute) { transactionHandler.execute([userContext.operation], function() { if(udebug.is_detail()) { udebug.log('find transactionHandler.execute callback.'); } }); } else if (typeof(userContext.operationDefinedCallback) === 'function') { userContext.operationDefinedCallback(1); } } } } // find starts here // session.find(projectionOrPrototypeOrTableName, key, callback) // validate first two parameters must be defined if (userContext.user_arguments[0] === undefined || userContext.user_arguments[1] === undefined) { userContext.applyCallback(new Error('User error: find must have at least two arguments.'), null); } else { if (userContext.user_arguments[0].constructor.name === 'Projection' && typeof userContext.user_arguments[0].constructor.prototype.addRelationship === 'function') { // this is a projection userContext.findWithProjection(); } else { // get DBTableHandler for prototype/tableName getTableHandler(userContext, userContext.user_arguments[0], userContext.session, findOnTableHandler); } } return userContext.promise; }; /** Create a query object. * */ exports.UserContext.prototype.createQuery = function() { var userContext = this; var p0 = userContext.user_arguments[0]; var queryDomainType; function createQueryOnTableHandler(err, dbTableHandler) { if (err) { userContext.applyCallback(err, null); } else { // create the query domain type and bind it to this session queryDomainType = new query.QueryDomainType(userContext.session, dbTableHandler, userContext.domainObject); if(udebug.is_detail()) { udebug.log('UserContext.createQuery queryDomainType:', queryDomainType); } userContext.applyCallback(null, queryDomainType); } } function createQueryOnValidateProjection(err) { udebug.log('UserContext.createQueryOnValidateProjection for projection', p0.name, 'returned error', err); if (err) { userContext.applyCallback(err, null); } else { queryDomainType = new query.QueryProjectionDomainType(userContext.session, p0); userContext.applyCallback(null, queryDomainType); } } // createQuery starts here // session.createQuery(constructorOrProjectionOrTableName, callback) // if the first parameter is a projection, resolve it if (p0.constructor && p0.constructor.name === 'Projection' && p0.domainObject && p0.validated !== undefined) { // we probably have a projection; validate it udebug.log('UserContext.createQuery for projection', p0.name); userContext.validateProjection(createQueryOnValidateProjection); return userContext.promise; } // if the first parameter is a query object then copy the interesting bits and create a new object if (this.user_arguments[0].jones_query_domain_type) { // TODO make sure this sessionFactory === other.sessionFactory queryDomainType = new query.QueryDomainType(userContext.session); } // if the first parameter is a table name the query results will be literals // if not (constructor or domain object) the query results will be domain objects userContext.domainObject = typeof this.user_arguments[0] !== 'string'; // get DBTableHandler for constructor/tableName getTableHandler(userContext, userContext.user_arguments[0], userContext.session, createQueryOnTableHandler); return userContext.promise; }; /** maximum skip and limit parameters are some large number */ var MAX_SKIP = Math.pow(2, 52); var MAX_LIMIT = Math.pow(2, 52); /** Execute a query. * */ exports.UserContext.prototype.executeQuery = function(queryDomainType) { var userContext = this; var dbSession, transactionHandler, queryType; userContext.queryDomainType = queryDomainType; // transform query result function executeQueryKeyOnResult(err, dbOperation) { udebug.log('executeQuery.executeQueryKeyOnResult'); var result, resultList = []; var error = checkOperation(err, dbOperation); if (error) { if (error.sqlstate === '02000') { // not found in the database userContext.applyCallback(null, []); } else { userContext.applyCallback(error, null); } } else { result = dbOperation.result.value; if (result !== null) { // TODO: filter in memory if the adapter didn't filter all conditions resultList = [result]; } userContext.applyCallback(null, resultList); } } // TODO: may be able to combine this with executeQueryKeyOnResult after looking at filter in memory function executeQueryKeyProjectionOnResult(err, dbOperation) { var result, resultList = []; if(udebug.is_detail()) { udebug.log( 'UserContext.executeQuery.executeKeyProjectionQueryOnResult err:', err, 'dbOperation:', dbOperation); } if (err) { if (err.sqlstate === '02000') { userContext.applyCallback(null, []); } else { userContext.applyCallback(err, null); } } else { result = dbOperation.result.value; if (result) { // TODO: filter in memory if the adapter didn't filter all conditions resultList = [result]; userContext.applyCallback(null, resultList); } } } // transform query result function executeQueryScanOnResult(err, dbOperation) { if(udebug.is_detail()) { udebug.log('executeQuery.executeQueryScanOnResult'); } var error = checkOperation(err, dbOperation); if (error) { if (err.sqlstate === '02000') { userContext.applyCallback(null, []); } else { userContext.applyCallback(err, null); } } else { if(udebug.is_detail()) { udebug.log('executeQuery.executeQueryScanOnResult', dbOperation.result.value); } // TODO: filter in memory if the adapter didn't filter all conditions userContext.applyCallback(null, dbOperation.result.value); } } // executeScanQuery is used by index scan and table scans for domain objects and projections var executeScanQuery = function() { // validate order, skip, and limit parameters var params = userContext.user_arguments[0]; var orderToUpperCase; var order = params.order, skip = params.skip, limit = params.limit; var error; if (limit !== undefined) { if (limit < 0 || limit > MAX_LIMIT) { // limit is out of valid range error = new Error('Bad limit parameter \'' + limit + '\'; limit must be >= 0 and <= ' + MAX_LIMIT + '.'); } } if (skip !== undefined) { if (skip < 0 || skip > MAX_SKIP) { // skip is out of valid range error = new Error('Bad skip parameter \'' + skip + '\'; skip must be >= 0 and <= ' + MAX_SKIP + '.'); } else { if (!order) { // skip is in range but order is not specified error = new Error('Bad skip parameter \'' + skip + '\'; if skip is specified, order must be specified.'); } } } if (order !== undefined) { if (typeof order !== 'string') { error = new Error('Bad order parameter \'' + order + '\'; order must be ignoreCase asc or desc.'); } else { orderToUpperCase = order.toUpperCase(); if (!(orderToUpperCase === 'ASC' || orderToUpperCase === 'DESC')) { error = new Error('Bad order parameter \'' + order + '\'; order must be ignoreCase asc or desc.'); } } } if (error) { userContext.applyCallback(error, null); } else { dbSession = userContext.session.dbSession; transactionHandler = dbSession.getTransactionHandler(); // TODO: should this also collect other pending operations? userContext.operation = dbSession.buildScanOperation( queryDomainType, userContext.user_arguments[0], transactionHandler, executeQueryScanOnResult); transactionHandler.execute([userContext.operation], function() { if(udebug.is_detail()) { udebug.log('executeScanQuery transactionHandler.execute callback.'); } }); } // TODO: this currently does not support batching // if (userContext.execute) { // transactionHandler.execute([userContext.operation], function() { // if(udebug.is_detail()) udebug.log('find transactionHandler.execute callback.'); // }); //} else if (typeof(userContext.operationDefinedCallback) === 'function') { // userContext.operationDefinedCallback(1); //} }; // executeKeyQuery is used by both primary key and unique key for projections and find operations var executeKeyQuery = function() { // create the find operation and execute it dbSession = userContext.session.dbSession; transactionHandler = dbSession.getTransactionHandler(); var dbIndexHandler = queryDomainType.jones_query_domain_type.queryHandler.dbIndexHandler; var keys = queryDomainType.jones_query_domain_type.queryHandler.getKeys(userContext.user_arguments[0]); if (queryDomainType.isQueryProjectionDomainType) { if(udebug.is_detail()) { udebug.log('UserContext.executeQuery.executeQueryKeyProjection indexHandler:', dbIndexHandler, 'keys:', keys); } userContext.operation = dbSession.buildReadProjectionOperation(dbIndexHandler, keys, queryDomainType.projection, transactionHandler, executeQueryKeyProjectionOnResult); } else { userContext.operation = dbSession.buildReadOperation(dbIndexHandler, keys, transactionHandler, false, executeQueryKeyOnResult); } transactionHandler.execute([userContext.operation], function() { if(udebug.is_detail()) { udebug.log('executeQueryPK transactionHandler.execute callback.'); } }); // TODO: this currently does not support batching // if (userContext.execute) { // transactionHandler.execute([userContext.operation], function() { // if(udebug.is_detail()) udebug.log('find transactionHandler.execute callback.'); // }); // } else if (typeof(userContext.operationDefinedCallback) === 'function') { // userContext.operationDefinedCallback(1); // } }; // executeQuery starts here // query.execute(parameters, callback) queryType = queryDomainType.jones_query_domain_type.queryType; udebug.log('QueryDomainType.execute', queryDomainType.jones_query_domain_type.predicate, 'with queryType', queryType, 'with parameters', userContext.user_arguments[0]); // execute the query and call back user switch(queryType) { case 0: // primary key executeKeyQuery(); break; case 1: // unique key executeKeyQuery(); break; case 2: // index scan executeScanQuery(); break; case 3: // table scan executeScanQuery(); break; default: throw new Error('FatalInternalException: queryType: ' + queryType + ' not supported'); } return userContext.promise; }; /** Persist the object. * */ exports.UserContext.prototype.persist = function() { var userContext = this; function persistOnResult(err, dbOperation) { udebug.log('persist.persistOnResult with err', err); // return any error code var error = checkOperation(err, dbOperation); if (error) { if (userContext.session.tx.isActive()) { userContext.session.tx.setRollbackOnly(); } userContext.applyCallback(error); } else { if (dbOperation.result.autoincrementValue) { // put returned autoincrement value into object userContext.dbTableHandler.setAutoincrement(userContext.values, dbOperation.result.autoincrementValue); } userContext.applyCallback(null); } } function persistOnTableHandler(err, dbTableHandler) { userContext.dbTableHandler = dbTableHandler; if(udebug.is_detail()){ udebug.log('UserContext.persist.persistOnTableHandler ' + err); } var transactionHandler; var dbSession = userContext.session.dbSession; if (userContext.clear) { // if batch has been cleared, user callback has already been called return; } if (err) { userContext.applyCallback(err); } else { transactionHandler = dbSession.getTransactionHandler(); userContext.operation = dbSession.buildInsertOperation(dbTableHandler, userContext.values, transactionHandler, persistOnResult); if (userContext.execute) { transactionHandler.execute([userContext.operation], function() { if(udebug.is_detail()) { udebug.log('persist transactionHandler.execute callback.'); } }); } else if (typeof(userContext.operationDefinedCallback) === 'function') { userContext.operationDefinedCallback(1); } } } // persist starts here if (userContext.required_parameter_count === 2) { // persist(object, callback) userContext.values = userContext.user_arguments[0]; } else if (userContext.required_parameter_count === 3) { // persist(tableNameOrConstructor, values, callback) userContext.values = userContext.user_arguments[1]; } else { throw new Error( 'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count); } // get DBTableHandler for table indicator (domain object, constructor, or table name) getTableHandler(userContext, userContext.user_arguments[0], userContext.session, persistOnTableHandler); return userContext.promise; }; /** Save the object. If the row already exists, overwrite non-pk columns. * */ exports.UserContext.prototype.save = function() { var userContext = this; var indexHandler; function saveOnResult(err, dbOperation) { // return any error code var error = checkOperation(err, dbOperation); if (error) { if (userContext.session.tx.isActive()) { userContext.session.tx.setRollbackOnly(); } userContext.applyCallback(error); } else { userContext.applyCallback(null); } } function saveOnTableHandler(err, dbTableHandler) { var transactionHandler; var dbSession = userContext.session.dbSession; if (userContext.clear) { // if batch has been cleared, user callback has already been called return; } if (err) { userContext.applyCallback(err); } else { transactionHandler = dbSession.getTransactionHandler(); indexHandler = dbTableHandler.getUniqueIndexHandler(userContext.values); if (!indexHandler.dbIndex.isPrimaryKey) { userContext.applyCallback( new Error('Illegal argument: parameter of save must include all primary key columns.')); return; } userContext.operation = dbSession.buildWriteOperation(indexHandler, userContext.values, transactionHandler, saveOnResult); if (userContext.execute) { transactionHandler.execute([userContext.operation], function() { }); } else if (typeof(userContext.operationDefinedCallback) === 'function') { userContext.operationDefinedCallback(1); } } } // save starts here if (userContext.required_parameter_count === 2) { // save(object, callback) userContext.values = userContext.user_arguments[0]; } else if (userContext.required_parameter_count === 3) { // save(tableNameOrConstructor, values, callback) userContext.values = userContext.user_arguments[1]; } else { throw new Error( 'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count); } // get DBTableHandler for table indicator (domain object, constructor, or table name) getTableHandler(userContext, userContext.user_arguments[0], userContext.session, saveOnTableHandler); return userContext.promise; }; /** Update the object. * */ exports.UserContext.prototype.update = function() { var userContext = this; var indexHandler; function updateOnResult(err, dbOperation) { // return any error code var error = checkOperation(err, dbOperation); if (error) { if (userContext.session.tx.isActive()) { userContext.session.tx.setRollbackOnly(); } userContext.applyCallback(error); } else { userContext.applyCallback(null); } } function updateOnTableHandler(err, dbTableHandler) { var transactionHandler; var dbSession = userContext.session.dbSession; if (userContext.clear) { // if batch has been cleared, user callback has already been called return; } if (err) { userContext.applyCallback(err); } else { transactionHandler = dbSession.getTransactionHandler(); indexHandler = dbTableHandler.getIndexHandler(userContext.keys); // for variant update(object, callback) the object must include all primary keys if (userContext.required_parameter_count === 2 && !indexHandler.dbIndex.isPrimaryKey) { userContext.applyCallback( new Error('Illegal argument: parameter of update must include all primary key columns.')); return; } userContext.operation = dbSession.buildUpdateOperation(indexHandler, userContext.keys, userContext.values, transactionHandler, updateOnResult); if (userContext.execute) { transactionHandler.execute([userContext.operation], function() { }); } else if (typeof(userContext.operationDefinedCallback) === 'function') { userContext.operationDefinedCallback(1); } } } // update starts here if (userContext.required_parameter_count === 2) { // update(object, callback) userContext.keys = userContext.user_arguments[0]; userContext.values = userContext.user_arguments[0]; } else if (userContext.required_parameter_count === 4) { // update(tableNameOrConstructor, keys, values, callback) userContext.keys = userContext.user_arguments[1]; userContext.values = userContext.user_arguments[2]; } else { throw new Error( 'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count); } // get DBTableHandler for table indicator (domain object, constructor, or table name) getTableHandler(userContext, userContext.user_arguments[0], userContext.session, updateOnTableHandler); return userContext.promise; }; /** Load the object. * */ exports.UserContext.prototype.load = function() { var userContext = this; function loadOnResult(err, dbOperation) { udebug.log('load.loadOnResult'); var error = checkOperation(err, dbOperation); if (error) { if (userContext.session.tx.isActive()) { userContext.session.tx.setRollbackOnly(); } userContext.applyCallback(err); return; } userContext.applyCallback(null); } function loadOnTableHandler(err, dbTableHandler) { var dbSession, keys, index, transactionHandler; if (userContext.clear) { // if batch has been cleared, user callback has already been called return; } if (err) { userContext.applyCallback(err); } else { userContext.dbTableHandler = dbTableHandler; // the domain object must provide PRIMARY or unique key keys = userContext.user_arguments[0]; index = dbTableHandler.getUniqueIndexHandler(keys); if (index === null) { err = new Error('Illegal argument: load unable to get a unique index to use for ' + JSON.stringify(keys)); userContext.applyCallback(err); } else { // create the load operation and execute it dbSession = userContext.session.dbSession; transactionHandler = dbSession.getTransactionHandler(); userContext.operation = dbSession.buildReadOperation(index, keys, transactionHandler, true, loadOnResult); if (userContext.execute) { transactionHandler.execute([userContext.operation], function() { if(udebug.is_detail()) { udebug.log('load transactionHandler.execute callback.'); } }); } else if (typeof(userContext.operationDefinedCallback) === 'function') { userContext.operationDefinedCallback(1); } } } } // load starts here // session.load(instance, callback) // get DBTableHandler for instance constructor if (typeof(userContext.user_arguments[0].jones) !== 'object') { userContext.applyCallback(new Error('Illegal argument: load requires a mapped domain object.')); return; } var ctor = userContext.user_arguments[0].jones.constructor; getTableHandler(userContext, ctor, userContext.session, loadOnTableHandler); return userContext.promise; }; /** Remove the object. * */ exports.UserContext.prototype.remove = function() { var userContext = this; function removeOnResult(err, dbOperation) { udebug.log('remove.removeOnResult'); // return any error code plus the original user object var error = checkOperation(err, dbOperation); if (error) { if (userContext.session.tx.isActive()) { userContext.session.tx.setRollbackOnly(); } userContext.applyCallback(error); } else { userContext.applyCallback(null); } } function removeOnTableHandler(err, dbTableHandler) { var transactionHandler, dbIndexHandler; var dbSession = userContext.session.dbSession; if (userContext.clear) { // if batch has been cleared, user callback has already been called return; } if (err) { userContext.applyCallback(err); } else { dbIndexHandler = dbTableHandler.getUniqueIndexHandler(userContext.keys); if (dbIndexHandler === null) { err = new Error('UserContext.remove unable to get an index to use for ' + JSON.stringify(userContext.keys)); userContext.applyCallback(err); } else { transactionHandler = dbSession.getTransactionHandler(); userContext.operation = dbSession.buildDeleteOperation( dbIndexHandler, userContext.keys, transactionHandler, removeOnResult); if (userContext.execute) { transactionHandler.execute([userContext.operation], function() { if(udebug.is_detail()) { udebug.log('remove transactionHandler.execute callback.'); } }); } else if (typeof(userContext.operationDefinedCallback) === 'function') { userContext.operationDefinedCallback(1); } } } } // remove starts here if (userContext.required_parameter_count === 2) { // remove(object, callback) userContext.keys = userContext.user_arguments[0]; } else if (userContext.required_parameter_count === 3) { // remove(tableNameOrConstructor, values, callback) userContext.keys = userContext.user_arguments[1]; } else { throw new Error( 'Fatal internal error; wrong required_parameter_count ' + userContext.required_parameter_count); } // get DBTableHandler for table indicator (domain object, constructor, or table name) getTableHandler(userContext, userContext.user_arguments[0], userContext.session, removeOnTableHandler); return userContext.promise; }; /** Get Mapping * */ exports.UserContext.prototype.getMapping = function() { var userContext = this; function getMappingOnTableHandler(err, dbTableHandler) { if (err) { userContext.applyCallback(err, null); return; } var mapping = dbTableHandler.getResolvedMapping(); userContext.applyCallback(null, mapping); } // getMapping starts here getTableHandler(userContext, userContext.user_arguments[0], userContext.session, getMappingOnTableHandler); return userContext.promise; }; /** Execute a batch * */ exports.UserContext.prototype.executeBatch = function(operationContexts) { var userContext = this; userContext.operationContexts = operationContexts; userContext.numberOfOperations = operationContexts.length; userContext.numberOfOperationsDefined = 0; // all operations have been executed and their user callbacks called // now call the Batch.execute callback var executeBatchOnExecute = function(err) { userContext.applyCallback(err); }; // wait here until all operations have been defined // if operations are not yet defined, the onTableHandler callback // will call this function after the operation is defined var executeBatchOnOperationDefined = function(definedOperationCount) { userContext.numberOfOperationsDefined += definedOperationCount; if(udebug.is_detail()) { udebug.log('UserContext.executeBatch expecting', userContext.numberOfOperations, 'operations with', userContext.numberOfOperationsDefined, 'already defined.'); } if (userContext.numberOfOperationsDefined === userContext.numberOfOperations) { var operations = []; // collect all operations from the operation contexts userContext.operationContexts.forEach(function(operationContext) { operations.push(operationContext.operation); }); // execute the batch var transactionHandler; var dbSession; dbSession = userContext.session.dbSession; transactionHandler = dbSession.getTransactionHandler(); transactionHandler.execute(operations, executeBatchOnExecute); } }; // executeBatch starts here // if no operations in the batch, just call the user callback if (operationContexts.length == 0) { executeBatchOnExecute(null); } else { // make sure all operations are defined operationContexts.forEach(function(operationContext) { // is the operation already defined? if (operationContext.operation !== undefined) { userContext.numberOfOperationsDefined++; } else { // the operation has not been defined yet; set a callback for when the operation is defined operationContext.operationDefinedCallback = executeBatchOnOperationDefined; } }); // now execute the operations executeBatchOnOperationDefined(0); } return userContext.promise; }; /** Commit an active transaction. * */ exports.UserContext.prototype.commit = function() { var userContext = this; var commitOnCommit = function(err) { udebug.log('UserContext.commitOnCommit.'); userContext.session.tx.setState(userContext.session.tx.idle); userContext.applyCallback(err); }; // commit begins here if (userContext.session.tx.isActive()) { udebug.log('UserContext.commit tx is active.'); userContext.session.dbSession.commit(commitOnCommit); } else { userContext.applyCallback( new Error('Fatal Internal Exception: UserContext.commit with no active transaction.')); } return userContext.promise; }; /** Roll back an active transaction. * */ exports.UserContext.prototype.rollback = function() { var userContext = this; var rollbackOnRollback = function(err) { udebug.log('UserContext.rollbackOnRollback.'); userContext.session.tx.setState(userContext.session.tx.idle); userContext.applyCallback(err); }; // rollback begins here if (userContext.session.tx.isActive()) { udebug.log('UserContext.rollback tx is active.'); var transactionHandler = userContext.session.dbSession.getTransactionHandler(); transactionHandler.rollback(rollbackOnRollback); } else { userContext.applyCallback( new Error('Fatal Internal Exception: UserContext.rollback with no active transaction.')); } return userContext.promise; }; /** Open a session. Allocate a slot in the session factory sessions array. * Call the DBConnectionPool to create a new DBSession. * Wrap the DBSession in a new Session and return it to the user. * This function is called by both jones.openSession (without a session factory) * and SessionFactory.openSession (with a session factory). */ exports.UserContext.prototype.openSession = function() { var userContext = this; var err; function openSessionOnResolvedMappings() { if (userContext.firstError) { err = reportFirstError(userContext); userContext.applyCallback(err, null); } else { userContext.applyCallback(null, userContext.session); } } function openSessionOnSession(err, dbSession) { if (err) { userContext.applyCallback(err, null); } else { userContext.session = new apiSession.Session(userContext.session_index, userContext.session_factory, dbSession); userContext.session_factory.sessions[userContext.session_index] = userContext.session; // if the user specified mappings, resolve them now if (userContext.cacheTableHandlerInSession && userContext.user_mappings) { resolveTableMappings(userContext, userContext.session_factory, userContext.session, userContext.user_mappings, openSessionOnResolvedMappings); } else { userContext.applyCallback(err, userContext.session); } } } var openSessionOnSessionFactory = function(err, factory) { if (err) { userContext.applyCallback(err, null); } else { userContext.session_factory = factory; // allocate a new session slot in sessions userContext.session_index = userContext.session_factory.allocateSessionSlot(); // get a new DBSession from the DBConnectionPool userContext.session_factory.dbConnectionPool.getDBSession(userContext.session_index, openSessionOnSession); } }; // openSession starts here if (userContext.session_factory) { openSessionOnSessionFactory(null, userContext.session_factory); } else { if(udebug.is_detail()) { udebug.log('openSession for', util.inspect(userContext)); } // properties might be null, a name, or a properties object userContext.user_arguments[0] = resolveProperties(userContext.user_arguments[0]); getSessionFactory(userContext, userContext.user_arguments[0], userContext.user_arguments[1], openSessionOnSessionFactory); } return userContext.promise; }; /** Close a session. Close the dbSession which might put the underlying connection * back into the connection pool. Then, remove the session from the session factory's * open connections. * */ exports.UserContext.prototype.closeSession = function() { var userContext = this; var closeSessionOnDBSessionClose = function(err) { // now remove the session from the session factory's open connections userContext.session_factory.closeSession(userContext.session.index); // mark this session as unusable userContext.session.closed = true; userContext.applyCallback(err); }; // first, close the dbSession userContext.session.dbSession.close(closeSessionOnDBSessionClose); return userContext.promise; }; /** Close all open SessionFactories * */ exports.UserContext.prototype.closeAllOpenSessionFactories = function() { udebug.log('UserContext.closeAllOpenSessionFactories'); var userContext, openFactories, nToClose; userContext = this; openFactories = jones.getOpenSessionFactories(); nToClose = openFactories.length; function onFactoryClose() { nToClose--; if(nToClose === 0) { userContext.applyCallback(null); } } if(nToClose > 0) { while(openFactories[0]) { openFactories[0].close(onFactoryClose); openFactories.shift(); } } else { userContext.applyCallback(null); } return userContext.promise; }; /** Complete the user function by calling back the user with the results of the function. * Apply the user callback using the current arguments and the extra parameters from the original function. * Create the args for the callback by copying the current arguments to this function. Then, copy * the extra parameters from the original function. Finally, call the user callback. * If there is no user callback, and there is an error (first argument to applyCallback) * throw the error. */ exports.UserContext.prototype.applyCallback = function(err, result) { if (arguments.length !== this.returned_parameter_count) { throw new Error( 'Fatal internal exception: wrong parameter count ' + arguments.length +' for UserContext applyCallback' + '; expected ' + this.returned_parameter_count); } // notify (either fulfill or reject) the promise if (err) { if(udebug.is_detail()) { udebug.log('UserContext.applyCallback.reject', err); } this.promise.reject(err); } else { if(udebug.is_detail()) { udebug.log('UserContext.applyCallback.fulfill', result); } this.promise.fulfill(result); } if (this.user_callback === undefined) { if(udebug.is_detail()) {udebug.log('UserContext.applyCallback with no user_callback.');} return; } var args = []; var i, j; for (i = 0; i < arguments.length; ++i) { args.push(arguments[i]); } for (j = this.required_parameter_count; j < this.user_arguments.length; ++j) { args.push(this.user_arguments[j]); } this.user_callback.apply(null, args); };