UNPKG

database-jones

Version:

Lightweight database mapping

1,174 lines (1,096 loc) 90.7 kB
/* 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 cre