2262 lines
94 KiB
JavaScript

/*
Copyright (c) 2013, 2014, 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
}
};
var util = require("util"),
mynode = require("./mynode.js"),
DBTableHandler = require(mynode.common.DBTableHandler).DBTableHandler,
apiSession = require("./Session.js"),
sessionFactory = require("./SessionFactory.js"),
query = require("./Query.js"),
spi = require(mynode.spi),
udebug = unified_debug.getLogger("UserContext.js"),
stats_module = require(mynode.api.stats);
stats_module.register(stats, "api", "UserContext");
function Promise() {
// implement Promises/A+ http://promises-aplus.github.io/promises-spec/
// until then is called, this is an empty promise with no performance impact
}
function emptyFulfilledCallback(result) {
return result;
}
function emptyRejectedCallback(err) {
throw err;
}
/** Fulfill or reject the original promise via "The Promise Resolution Procedure".
* original_promise is the Promise from this implementation on which "then" was called
* new_promise is the Promise from this implementation returned by "then"
* if the fulfilled or rejected callback provided by "then" returns a promise, wire the new_result (thenable)
* to fulfill the new_promise when new_result is fulfilled
* or reject the new_promise when new_result is rejected
* otherwise, if the callback provided by "then" returns a value, fulfill the new_promise with that value
* if the callback provided by "then" throws an Error, reject the new_promise with that Error
*/
var thenPromiseFulfilledOrRejected = function(original_promise, fulfilled_or_rejected_callback, new_promise, result, isRejected) {
var new_result;
try {
if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected before'); }
if (fulfilled_or_rejected_callback) {
new_result = fulfilled_or_rejected_callback.call(undefined, result);
} else {
if (isRejected) {
// 2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason.
new_promise.reject(result);
} else {
// 2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value.
new_promise.fulfill(result);
}
return;
}
if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected after', new_result); }
var new_result_type = typeof new_result;
if ((new_result_type === 'object' && new_result_type != null) | new_result_type === 'function') {
// 2.3.3 if result is an object or function
// 2.3 The Promise Resolution Procedure
// 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
if (new_result === original_promise) {
throw new Error('TypeError: Promise Resolution Procedure 2.3.1');
}
// 2.3.2 If x is a promise, adopt its state; but we don't care since it's also a thenable
var then;
try {
then = new_result.then;
} catch (thenE) {
// 2.2.3.2 If retrieving the property x.then results in a thrown exception e,
// reject promise with e as the reason.
new_promise.reject(thenE);
return;
}
if (typeof then === 'function') {
// 2.3.3.3 If then is a function, call it with x as this, first argument resolvePromise,
// and second argument rejectPromise
// 2.3.3.3.3 If both resolvePromise and rejectPromise are called,
// or multiple calls to the same argument are made, the first call takes precedence,
// and any further calls are ignored.
try {
then.call(new_result,
// 2.3.3.3.1 If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
function(result) {
if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected deferred fulfill callback', new_result); }
if (!new_promise.resolved) {
new_promise.fulfill(result);
}
},
// 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
function(err) {
if(udebug.is_detail()) { udebug.log(original_promise.name, 'thenPromiseFulfilledOrRejected deferred reject callback', new_result); }
if (!new_promise.resolved) {
new_promise.reject(err);
}
}
);
} catch (callE) {
// 2.3.3.3.4 If calling then throws an exception e,
// 2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
if (!new_promise.resolved) {
// 2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
new_promise.reject(callE);
}
}
} else {
// 2.3.3.4 If then is not a function, fulfill promise with x.
new_promise.fulfill(new_result);
}
} else {
// 2.3.4 If x is not an object or function, fulfill promise with x.
new_promise.fulfill(new_result);
}
} catch (fulfillE) {
// 2.2.7.2 If either onFulfilled or onRejected throws an exception e,
// promise2 must be rejected with e as the reason.
new_promise.reject(fulfillE);
}
};
Promise.prototype.then = function(fulfilled_callback, rejected_callback, progress_callback) {
var self = this;
if (!self) udebug.log('Promise.then called with no this');
// create a new promise to return from the "then" method
var new_promise = new Promise();
if (typeof self.fulfilled_callbacks === 'undefined') {
self.fulfilled_callbacks = [];
self.rejected_callbacks = [];
self.progress_callbacks = [];
}
if (self.resolved) {
var resolved_result;
if(udebug.is_detail()) { udebug.log(this.name, 'UserContext.Promise.then resolved; err:', self.err); }
if (self.err) {
// this promise was already rejected
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling (delayed) rejected_callback', rejected_callback); }
global.setImmediate(function() {
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling rejected_callback', fulfilled_callback); }
thenPromiseFulfilledOrRejected(self, rejected_callback, new_promise, self.err, true);
});
} else {
// this promise was already fulfilled, possibly with a null or undefined result
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling (delayed) fulfilled_callback', fulfilled_callback); }
global.setImmediate(function() {
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then resolved calling fulfilled_callback', fulfilled_callback); }
thenPromiseFulfilledOrRejected(self, fulfilled_callback, new_promise, self.result);
});
}
return new_promise;
}
// create a closure for each fulfilled_callback
// the closure is a function that when called, calls setImmediate to call the fulfilled_callback with the result
if (typeof fulfilled_callback === 'function') {
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with fulfilled_callback', fulfilled_callback); }
// the following function closes (this, fulfilled_callback, new_promise)
// and is called asynchronously when this promise is fulfilled
this.fulfilled_callbacks.push(function(result) {
global.setImmediate(function() {
thenPromiseFulfilledOrRejected(self, fulfilled_callback, new_promise, result);
});
});
} else {
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with no fulfilled_callback'); }
// create a dummy function for a missing fulfilled callback per 2.2.7.3
// If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value.
this.fulfilled_callbacks.push(function(result) {
global.setImmediate(function() {
thenPromiseFulfilledOrRejected(self, emptyFulfilledCallback, new_promise, result);
});
});
}
// create a closure for each rejected_callback
// the closure is a function that when called, calls setImmediate to call the rejected_callback with the error
if (typeof rejected_callback === 'function') {
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with rejected_callback', rejected_callback); }
this.rejected_callbacks.push(function(err) {
global.setImmediate(function() {
thenPromiseFulfilledOrRejected(self, rejected_callback, new_promise, err);
});
});
} else {
if(udebug.is_detail()) { udebug.log(self.name, 'UserContext.Promise.then with no rejected_callback'); }
// create a dummy function for a missing rejected callback per 2.2.7.4
// If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason.
this.rejected_callbacks.push(function(err) {
global.setImmediate(function() {
thenPromiseFulfilledOrRejected(self, emptyRejectedCallback, new_promise, err);
});
});
}
// todo: progress_callbacks
if (typeof progress_callback === 'function') {
this.progress_callbacks.push(progress_callback);
}
return new_promise;
};
Promise.prototype.fulfill = function(result) {
var name = this?this.name: 'no this';
if (udebug.is_detail()) {
udebug.log_detail(new Error(name, 'Promise.fulfill').stack);
}
if (this.resolved) {
throw new Error('Fatal User Exception: fulfill called after fulfill or reject');
}
if(udebug.is_detail()) {
udebug.log(name, 'Promise.fulfill with result', result, 'fulfilled_callbacks length:',
this.fulfilled_callbacks? this.fulfilled_callbacks.length: 0);
}
this.resolved = true;
this.result = result;
var fulfilled_callback;
if (this.fulfilled_callbacks) {
while(this.fulfilled_callbacks.length > 0) {
fulfilled_callback = this.fulfilled_callbacks.shift();
if(udebug.is_detail()) { udebug.log('Promise.fulfill for', result); }
fulfilled_callback(result);
}
}
};
Promise.prototype.reject = function(err) {
if (this.resolved) {
throw new Error('Fatal User Exception: reject called after fulfill or reject');
}
var name = this?this.name: 'no this';
if(udebug.is_detail()) {
udebug.log(name, 'Promise.reject with err', err, 'rejected_callbacks length:',
this.rejected_callbacks? this.rejected_callbacks.length: 0);
}
this.resolved = true;
this.err = err;
var rejected_callback;
if (this.rejected_callbacks) {
while(this.rejected_callbacks.length > 0) {
rejected_callback = this.rejected_callbacks.shift();
if(udebug.is_detail()) { udebug.log('Promise.reject for', err); }
rejected_callback(err);
}
}
// throw err;
};
/** 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 required 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.execute = (typeof execute === 'boolean' ? execute : true);
this.user_arguments = user_arguments;
this.user_callback = user_arguments[required_parameter_count - 1];
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();
};
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;
var getTableMetadataOnTableMetadata = function(metadataErr, tableMetadata) {
udebug.log('UserContext.getTableMetadata.getTableMetadataOnTableMetadata with err', metadataErr);
userContext.applyCallback(metadataErr, tableMetadata);
};
databaseName = userContext.user_arguments[0];
tableName = userContext.user_arguments[1];
dbSession = (userContext.session)?userContext.session.dbSession:null;
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 {
this.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;
};
/** Create schema from a table mapping.
* promise = createTable(tableMapping, callback);
*/
exports.UserContext.prototype.createTable = function() {
var userContext = this;
var tableMapping, dbSession;
function createTableOnTableCreated(err) {
userContext.applyCallback(err);
}
// createTable starts here
tableMapping = userContext.user_arguments[0];
userContext.session_factory.dbConnectionPool.createTable(tableMapping, userContext.session, userContext.session_factory,
createTableOnTableCreated);
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.
*/
var resolveProperties = function(properties) {
// Properties can be a string adapter name. It defaults to 'ndb'.
if(typeof properties === 'string') {
properties = spi.getDBServiceProvider(properties).getDefaultConnectionProperties();
}
else if (properties === null) {
properties = spi.getDBServiceProvider('ndb').getDefaultConnectionProperties();
}
return properties;
};
function getTableSpecification(defaultDatabaseName, tableName) {
var split = tableName.split('\.');
var result = {};
if (split.length == 2) {
result.dbName = split[0];
result.unqualifiedTableName = split[1];
result.qualifiedTableName = tableName;
} else {
// if split.length is not 1 then this error will be caught later
result.dbName = defaultDatabaseName;
result.unqualifiedTableName = tableName;
result.qualifiedTableName = defaultDatabaseName + '.' + tableName;
}
udebug.log_detail('getTableSpecification for', defaultDatabaseName, ',', tableName, 'returned', result);
return result;
}
/** Construct the table name from possibly empty database name and table name.
*/
function constructDatabaseDotTable(databaseName, tableName) {
var result = databaseName ? databaseName + '.' + tableName : tableName;
return result;
}
/** Get the table handler for a domain object, table name, or constructor.
*/
var getTableHandler = function(domainObjectTableNameOrConstructor, session, onTableHandler) {
// the table name might be qualified if the mapping specified a qualified table name
// if unqualified, use sessionFactory.properties.database to qualify the table name
var TableHandlerFactory = function(mynode, tableSpecification,
sessionFactory, dbSession, mapping, ctor, onTableHandler) {
this.sessionFactory = sessionFactory;
this.dbSession = dbSession;
this.onTableHandler = onTableHandler;
this.mapping = mapping;
this.mynode = mynode;
this.ctor = ctor;
this.tableSpecification = tableSpecification;
stats.TableHandlerFactory++;
this.createTableHandler = function() {
var tableHandlerFactory = this;
var tableHandler;
var tableMetadata;
var tableMapping;
var onTableMetadata = function(err, tableMetadata) {
var tableHandler;
var tableKey = tableHandlerFactory.tableSpecification.qualifiedTableName;
if(udebug.is_detail()) {
udebug.log('TableHandlerFactory.onTableMetadata for ',
tableHandlerFactory.tableSpecification.qualifiedTableName + ' with err: ' + err);
}
if (err) {
tableHandlerFactory.onTableHandler(err, null);
} else {
// check to see if the metadata has already been processed
if (typeof tableHandlerFactory.sessionFactory.tableMetadatas[tableKey] === 'undefined') {
// put the table metadata into the table metadata map
tableHandlerFactory.sessionFactory.tableMetadatas[tableKey] = tableMetadata;
}
// we have the table metadata; now create the table handler if needed
// put the table handler into the session factory
if (typeof(tableHandlerFactory.sessionFactory.tableHandlers[tableKey]) === 'undefined') {
if(udebug.is_detail()) {
udebug.log('UserContext caching the table handler in the sessionFactory for ',
tableHandlerFactory.tableName);
}
tableHandler = new DBTableHandler(tableMetadata, tableHandlerFactory.mapping,
tableHandlerFactory.ctor);
tableHandlerFactory.sessionFactory.tableHandlers[tableKey] = tableHandler;
} else {
tableHandler = tableHandlerFactory.sessionFactory.tableHandlers[tableKey];
if(udebug.is_detail()) {
udebug.log('UserContext got tableHandler but someone else put it in the cache first for ',
tableHandlerFactory.tableName);
}
}
if (tableHandlerFactory.ctor) {
if (typeof(tableHandlerFactory.ctor.prototype.mynode.tableHandler) === 'undefined') {
// if a domain object mapping, cache the table handler in the prototype
stats.TableHandler.success++;
tableHandler = new DBTableHandler(tableMetadata, tableHandlerFactory.mapping,
tableHandlerFactory.ctor);
if (tableHandler.isValid) {
tableHandlerFactory.ctor.prototype.mynode.tableHandler = tableHandler;
if(udebug.is_detail()) {
udebug.log('UserContext caching the table handler in the prototype for constructor.');
}
} else {
tableHandlerFactory.err = tableHandler.err;
if(udebug.is_detail()) { udebug.log('UserContext got invalid tableHandler', tableHandler.errorMessages); }
}
} else {
tableHandler = tableHandlerFactory.ctor.prototype.mynode.tableHandler;
stats.TableHandler.idempotent++;
if(udebug.is_detail()) {
udebug.log('UserContext got tableHandler but someone else put it in the prototype first.');
}
}
}
tableHandlerFactory.onTableHandler(tableHandlerFactory.err, tableHandler);
}
};
function tableHandlerFactoryOnCreateTable(err) {
if (err) {
onTableMetadata(err, null);
} else {
sessionFactory.dbConnectionPool.getTableMetadata(tableHandlerFactory.tableSpecification.dbName,
tableHandlerFactory.tableSpecification.unqualifiedTableName, session.dbSession, onTableMetadata);
}
}
// start of createTableHandler
// get the table metadata from the cache of table metadatas in session factory
tableMetadata =
tableHandlerFactory.sessionFactory.tableMetadatas[tableHandlerFactory.tableSpecification.qualifiedTableName];
if (tableMetadata) {
// we already have cached the table metadata
onTableMetadata(null, tableMetadata);
} else {
// create the schema if it does not already exist
tableMapping = sessionFactory.tableMappings[tableSpecification.qualifiedTableName];
if (tableMapping) {
udebug.log('tableHandlerFactory.createTableHandler creating schema using tableMapping: ', tableMapping);
sessionFactory.createTable(tableMapping, tableHandlerFactoryOnCreateTable);
return;
}
// get the table metadata from the db connection pool
// getTableMetadata(dbSession, databaseName, tableName, callback(error, DBTable));
udebug.log('TableHandlerFactory.createTableHandler for ',
tableHandlerFactory.tableSpecification.dbName,
tableHandlerFactory.tableSpecification.unqualifiedTableName);
this.sessionFactory.dbConnectionPool.getTableMetadata(
tableHandlerFactory.tableSpecification.dbName,
tableHandlerFactory.tableSpecification.unqualifiedTableName, session.dbSession, onTableMetadata);
}
};
};
// start of getTableHandler
var err, mynode, tableHandler, tableMapping, tableHandlerFactory, tableIndicatorType, tableSpecification, databaseDotTable;
tableIndicatorType = typeof(domainObjectTableNameOrConstructor);
if (tableIndicatorType === 'string') {
if(udebug.is_detail()) {
udebug.log('UserContext.getTableHandler for table ', domainObjectTableNameOrConstructor);
}
tableSpecification = getTableSpecification(session.sessionFactory.properties.database,
domainObjectTableNameOrConstructor);
// parameter is a table name; look up in table name to table handler hash
tableHandler = session.sessionFactory.tableHandlers[tableSpecification.qualifiedTableName];
if (typeof(tableHandler) === 'undefined') {
udebug.log('UserContext.getTableHandler did not find cached tableHandler for table ',
tableSpecification.qualifiedTableName);
// get a table mapping from session factory
tableMapping = session.sessionFactory.tableMappings[tableSpecification.qualifiedTableName];
// create a new table handler for a table name with no mapping
// create a closure to create the table handler
tableHandlerFactory = new TableHandlerFactory(
null, tableSpecification, session.sessionFactory, session.dbSession,
tableMapping, null, onTableHandler);
tableHandlerFactory.createTableHandler(null);
} else {
if(udebug.is_detail()) {
udebug.log('UserContext.getTableHandler found cached tableHandler for table ',
tableSpecification.qualifiedTableName);
}
// send back the tableHandler
onTableHandler(null, tableHandler);
}
} else if (tableIndicatorType === 'function') {
if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler for constructor.'); }
mynode = domainObjectTableNameOrConstructor.prototype.mynode;
// parameter is a constructor; it must have been annotated already
if (typeof(mynode) === 'undefined') {
err = new Error('User exception: constructor for ' + domainObjectTableNameOrConstructor.prototype.constructor.name +
' must have been annotated (call TableMapping.applyToClass).');
onTableHandler(err, null);
} else {
tableHandler = mynode.tableHandler;
if (typeof(tableHandler) === 'undefined') {
udebug.log('UserContext.getTableHandler did not find cached tableHandler for constructor.',
domainObjectTableNameOrConstructor);
// create the tableHandler
// getTableMetadata(dbSession, databaseName, tableName, callback(error, DBTable));
databaseDotTable = constructDatabaseDotTable(mynode.mapping.database, mynode.mapping.table);
tableSpecification = getTableSpecification(session.sessionFactory.properties.database, databaseDotTable);
tableHandlerFactory = new TableHandlerFactory(
mynode, tableSpecification, session.sessionFactory, session.dbSession,
mynode.mapping, domainObjectTableNameOrConstructor, onTableHandler);
tableHandlerFactory.createTableHandler();
} else {
stats.TableHandler.cache_hit++;
if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler found cached tableHandler for constructor.'); }
// prototype has been annotated; return the table handler
onTableHandler(null, tableHandler);
}
}
} else if (tableIndicatorType === 'object') {
if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler for domain object.'); }
// parameter is a domain object; it must have been mapped already
mynode = domainObjectTableNameOrConstructor.constructor.prototype.mynode;
if (typeof(mynode) === 'undefined') {
err = new Error('User exception: constructor for ' + domainObjectTableNameOrConstructor.constructor.name +
' must have been annotated (call TableMapping.applyToClass).');
onTableHandler(err, null);
} else {
tableHandler = mynode.tableHandler;
if (typeof(tableHandler) === 'undefined') {
if(udebug.is_detail()) {
udebug.log('UserContext.getTableHandler did not find cached tableHandler for object\n',
util.inspect(domainObjectTableNameOrConstructor),
'constructor\n', domainObjectTableNameOrConstructor.constructor);
}
databaseDotTable = constructDatabaseDotTable(mynode.mapping.database, mynode.mapping.table);
tableSpecification = getTableSpecification(session.sessionFactory.properties.database, databaseDotTable);
// create the tableHandler
// getTableMetadata(dbSession, databaseName, tableName, callback(error, DBTable));
tableHandlerFactory = new TableHandlerFactory(
mynode, tableSpecification, session.sessionFactory, session.dbSession,
mynode.mapping, domainObjectTableNameOrConstructor.constructor, onTableHandler);
tableHandlerFactory.createTableHandler();
} else {
if(udebug.is_detail()) { udebug.log('UserContext.getTableHandler found cached tableHandler for constructor.'); }
// prototype has been annotated; return the table handler
onTableHandler(null, tableHandler);
}
}
} else {
err = new Error('User error: parameter must be a domain object, string, or constructor function.');
onTableHandler(err, null);
}
};
/** 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 connectionKey;
var connection;
var factory;
var newSession;
var sp;
var i;
var m;
var resolveTableMappingsOnSession = function(err, session) {
var mappings = [];
var mappingBeingResolved = 0;
var resolveTableMappingsOnTableHandler = function(err, tableHandler) {
if(udebug.is_detail()) {
udebug.log('UserContext.resolveTableMappinsgOnTableHandler', mappingBeingResolved + 1,
'of', mappings.length, mappings[mappingBeingResolved]);
}
if (err) {
userContext.appendErrorMessage(err);
}
if (++mappingBeingResolved === mappings.length || mappingBeingResolved > 10) {
// close the session the hard way (not using UserContext)
session.dbSession.close(function(err) {
if (err) {
callback(err, null);
} else {
// now remove the session from the session factory's open connections
session.sessionFactory.closeSession(session.index);
// mark this session as unusable
session.closed = true;
// if any errors during table mapping, report them
if (userContext.errorMessages) {
err = new Error(userContext.errorMessages);
callback(err, null);
} else {
// no errors
callback(null, factory);
}
}
});
} else {
// get the table handler for the next one, and so on until all are done
getTableHandler(mappings[mappingBeingResolved], session, resolveTableMappingsOnTableHandler);
}
};
// resolveTableMappingsOnSession begins here
var tableMappingsType = typeof(tableMappings);
var tableMapping;
var tableMappingType;
switch (tableMappingsType) {
case 'string':
mappings.push(tableMappings);
break;
case 'function':
mappings.push(tableMappings);
break;
case 'object':
if (tableMappings.length) {
for (m = 0; m < tableMappings.length; ++m) {
tableMapping = tableMappings[m];
tableMappingType = typeof(tableMapping);
if (tableMappingType === 'function' || tableMappingType === 'string') {
mappings.push(tableMapping);
} else {
userContext.appendErrorMessage('unknown table mapping' + util.inspect(tableMapping));
}
}
} else {
userContext.appendErrorMessage('unknown table mappings' + util.inspect(tableMappings));
}
break;
default:
userContext.appendErrorMessage('unknown table mappings' + util.inspect(tableMappings));
break;
}
if (mappings.length === 0) {
if(udebug.is_detail()) { udebug.log('resolveTableMappingsOnSession no mappings!'); }
callback(null, factory);
}
// 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(mappings[0], session, resolveTableMappingsOnTableHandler);
};
var resolveTableMappingsAndCallback = function() {
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 {
var newSession = new apiSession.Session(sessionSlot, factory, dbSession);
factory.sessions[sessionSlot] = newSession;
resolveTableMappingsOnSession(err, newSession);
}
});
}
};
var createFactory = function(dbConnectionPool) {
var newFactory;
udebug.log('connect createFactory creating factory for', connectionKey, 'database', database);
newFactory = new sessionFactory.SessionFactory(connectionKey, dbConnectionPool,
properties, tableMappings, mynode.deleteFactory);
return newFactory;
};
var dbConnectionPoolCreated_callback = function(error, dbConnectionPool) {
if (connection.isConnecting) {
// the first requester for this connection
connection.isConnecting = false;
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...'); }
udebug.log('dbConnectionPoolCreated', error, dbConnectionPool);
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;
connectionKey = mynode.getConnectionKey(properties);
connection = mynode.getConnection(connectionKey);
if(typeof(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 = mynode.newConnection(connectionKey);
sp = spi.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 (typeof(factory) === 'undefined') {
// 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]);
var connectOnSessionFactory = function(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.
* 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 relationship is 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.
* @param inout parameter: outerLoopProjection the projection at the outer loop
* which is modified by this function to add the sectors field
* @param inout parameter: innerLoopProjection the projection at the inner loop
* which is modified by this function to add the sectors field
* @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(outerLoopProjection, innerLoopProjection, sectors, index, offset) {
udebug.log('createSector ' + outerLoopProjection.name + ' for ' + outerLoopProjection.domainObject.name +
' inner: ' + innerLoopProjection.name + ' for ' + innerLoopProjection.domainObject.name +
' index: ' + index + ' offset: ' + offset);
var projection = innerLoopProjection;
var innerNestedProjection, outerNestedProjection;
var sector = {};
var tableHandler, relationships;
var keyFieldCount, nonKeyFieldCount;
var fieldNames, field;
var indexHandler;
var relationshipName;
var relatedFieldMapping, relatedSector, relatedTableHandler, relatedTargetFieldName, relatedTargetField;
var thisFieldMapping;
var joinTable, joinTableHandler;
var foreignKeys, foreignKey, fkIndex, foreignKeyName;
var i, fkFound;
// initialize sector
sector.keyFields = [];
sector.nonKeyFields = [];
sector.keyFieldNames = [];
sector.projection = projection;
sector.offset = offset;
tableHandler = projection.domainObject.prototype.mynode.tableHandler;
udebug.log_detail('createSector for table handler', tableHandler.dbTable.name);
sector.tableHandler = tableHandler;
// relatedFieldMapping is the field mapping for the sector to the "left" of this sector
// it contains the field in the "left" sector and mapping information including join columns
relatedFieldMapping = projection.relatedFieldMapping;
sector.relatedFieldMapping = relatedFieldMapping;
udebug.log_detail('createSector thisDBTable name', tableHandler.dbTable.name, index, sectors);
if (relatedFieldMapping && index !== 0) {
// only perform related field mapping for nested projections
relatedSector = sectors[index - 1];
relatedTableHandler = relatedSector.tableHandler;
sector.relatedTableHandler = relatedTableHandler;
// get this optional field mapping that corresponds to the related field mapping
// it may be needed to find the foreign key or join table
relatedTargetFieldName = relatedFieldMapping.targetField;
relatedTargetField = sector.tableHandler.fieldNameToFieldMap[relatedTargetFieldName];
if (relatedFieldMapping.toMany && relatedFieldMapping.manyTo) {
// this is a many-to-many relationship using a join table
joinTable = relatedFieldMapping.joinTable;
// joinTableHandler is the DBTableHandler for the join table resolved during validateProjection
if (joinTable) {
// join table is defined on the related side
joinTableHandler = relatedFieldMapping.joinTableHandler;
} else {
// join table must be defined on this side
thisFieldMapping = tableHandler.fieldNameToFieldMap[relatedFieldMapping.targetField];
joinTable = thisFieldMapping.joinTable;
if (!joinTable) {
// error; neither side defined the join table
projection.error += '\nMappingError: ' + relatedTableHandler.newObjectConstructor.name +
' field ' + relatedFieldMapping.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) {
for (foreignKeyName in joinTableHandler.foreignKeyMap) {
if (joinTableHandler.foreignKeyMap.hasOwnProperty(foreignKeyName)) {
foreignKey = joinTableHandler.foreignKeyMap[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
relatedFieldMapping.otherForeignKey = foreignKey;
}
if (foreignKey.targetDatabase === relatedTableHandler.dbTable.database &&
foreignKey.targetTable === relatedTableHandler.dbTable.name) {
relatedFieldMapping.thisForeignKey = foreignKey;
}
}
}
if (!(relatedFieldMapping.thisForeignKey && relatedFieldMapping.otherForeignKey)) {
// error must have foreign keys to both this table and related table
projection.error += '\nMappingError: ' + relatedTableHandler.newObjectConstructor.name +
' field ' + relatedFieldMapping.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 = relatedFieldMapping.foreignKey;
if (foreignKeyName) {
// foreign key is defined on the other side
foreignKey = relatedTableHandler.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
relatedTargetField = sector.tableHandler.fieldNameToFieldMap[relatedTargetFieldName];
foreignKeyName = relatedTargetField.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: ' + relatedTableHandler.newObjectConstructor.name +
' field ' + relatedFieldMapping.fieldName + ' neither side defined the foreign key.';
}
}
}
}
// create key fields from primary key index handler
indexHandler = tableHandler.dbIndexHandlers[0];
keyFieldCount = indexHandler.fieldNumberToFieldMap.length;
sector.keyFieldCount = keyFieldCount;
for (i = 0; i < keyFieldCount; ++i) {
field = indexHandler.fieldNumberToFieldMap[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.fieldNameToFieldMap[fieldName];
sector.nonKeyFields.push(field);
}
});
sector.nonKeyFieldCount = sector.nonKeyFields.length;
udebug.log_detail('createSector created new sector for index', index, 'sector', sector);
sectors.push(sector);
innerNestedProjection = projection.firstNestedProjection;
if (innerNestedProjection) {
// recurse at the same outer loop, creating the next sector for the inner loop projection
createSector(outerLoopProjection, innerNestedProjection,
sectors, index + 1, offset + keyFieldCount + sector.nonKeyFieldCount);
} else {
// we are done at this outer projection level;
if (outerLoopProjection.name) {
udebug.log('createSector for ' + outerLoopProjection.name +
' created ' + outerLoopProjection.sectors.length + ' sectors for ' + outerLoopProjection.domainObject.name);
}
// now go to the outer projection next level down and do it all over again
outerNestedProjection = outerLoopProjection.firstNestedProjection;
if (outerNestedProjection) {
outerNestedProjection.sectors = [];
createSector(outerNestedProjection, 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.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.
* 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.
*
* In the second phase, validate that the relationship is mapped with valid
* foreign keys and join tables in the database.
* After all projections have been validated, 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, relationshipProjection;
var fieldMapping;
var index, offset;
var errors;
var foreignKeyNames, foreignKeyName;
var toBeChecked, toBeValidated, allValidated;
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(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);
domainObject = projection.domainObject;
domainObjectName = domainObject.prototype.constructor.name;
domainObjectMynode = domainObject.prototype.mynode;
if (domainObjectMynode && domainObjectMynode.mapping.error) {
// remember errors in mapping
errors += domainObjectMynode.mapping.error;
}
if (!err) {
projection.dbTableHandler = dbTableHandler;
// validate using table handler
if (typeof(domainObject) === 'function' &&
typeof(domainObject.prototype.mynode) === 'object' &&
typeof(domainObject.prototype.mynode.mapping) === 'object') {
// good domainObject; have we seen this one before?
mappingId = domainObject.prototype.mynode.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.fieldNameToFieldMap[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.foreignKeyMap.hasOwnProperty(foreignKeyName)) {
errors += '\nBad relationship field mapping; foreign key ' + foreignKeyName +
' does not exist in table; possible foreign keys are: ' + Object.keys(dbTableHandler.foreignKeyMap);
}
}
// 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.fieldNameToFieldMap[key];
if (fieldMapping) {
if (fieldMapping.relationship) {
relationshipProjection = relationships[key];
relationshipProjection.relatedFieldMapping = fieldMapping;
// add each relationship to list of projections
projections.push(relationshipProjection);
// record the first (currently the only) projection that is nested in this projection
if (!projection.firstNestedProjection) {
projection.firstNestedProjection = relationshipProjection;
}
} 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 + ': field ' + 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(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) {
// do the next projection
if (projections[index].domainObject.prototype.mynode.dbTableHandler) {
udebug.log('validateProjection with cached tableHandler for', projections[index].domainObject.name);
validateProjectionOnTableHandler(null, projections[index].domainObject.prototype.mynode.dbTableHandler);
} else {
udebug.log('validateProjection with no cached tableHandler for', projections[index].domainObject.name);
getTableHandler(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 = [];
index = 0;
offset = 0;
// create the first sector; additional sectors will be created recursively
createSector(projection, projection, projection.sectors, index, offset);
// 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
index = 0; // index into projections for the projection being validated
offset = 0; // the number of fields in previous projections
errors = ''; // 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.mynode;
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(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 result, values;
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.onValidProjection err: ', err);
userContext.applyCallback(err, null);
} else {
dbTableHandler = projection.dbTableHandler;
userContext.dbTableHandler = dbTableHandler;
keys = userContext.user_arguments[1];
indexHandler = dbTableHandler.getIndexHandler(keys, true);
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;
var tableHandler;
if (typeof(this.user_arguments[0]) === 'function') {
userContext.domainObject = true;
}
function findOnResult(err, dbOperation) {
udebug.log('find.findOnResult');
var result, values;
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.getIndexHandler(keys, true);
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, 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.user_arguments[0], userContext.session, findOnTableHandler);
}
}
return userContext.promise;
};
/** Create a query object.
*
*/
exports.UserContext.prototype.createQuery = function() {
var userContext = this;
var tableHandler;
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);
}
}
// createQuery starts here
// session.createQuery(constructorOrTableName, callback)
// if the first parameter is a query object then copy the interesting bits and create a new object
if (this.user_arguments[0].mynode_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.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.executeQueryPKOnResult');
var result, values, resultList;
var error = checkOperation(err, dbOperation);
if (error) {
userContext.applyCallback(error, null);
} else {
if (userContext.queryDomainType.mynode_query_domain_type.domainObject) {
values = dbOperation.result.value;
result = userContext.queryDomainType.mynode_query_domain_type.dbTableHandler.newResultObject(values);
} else {
result = dbOperation.result.value;
}
if (result !== null) {
// TODO: filter in memory if the adapter didn't filter all conditions
resultList = [result];
} else {
resultList = [];
}
userContext.applyCallback(null, resultList);
}
}
// transform query result
function executeQueryScanOnResult(err, dbOperation) {
if(udebug.is_detail()) { udebug.log('executeQuery.executeQueryScanOnResult'); }
var result, values, resultList;
var error = checkOperation(err, dbOperation);
if (error) {
userContext.applyCallback(error, 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 both index scan and table scan
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 (typeof(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 (typeof(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, then order must be specified.');
}
}
}
if (typeof(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();
userContext.operation = dbSession.buildScanOperation(
queryDomainType, userContext.user_arguments[0], transactionHandler,
executeQueryScanOnResult);
// TODO: this currently does not support batching
transactionHandler.execute([userContext.operation], function() {
if(udebug.is_detail()) { udebug.log('executeQueryPK transactionHandler.execute callback.'); }
});
}
// 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
var executeKeyQuery = function() {
// create the find operation and execute it
dbSession = userContext.session.dbSession;
transactionHandler = dbSession.getTransactionHandler();
var dbIndexHandler = queryDomainType.mynode_query_domain_type.queryHandler.candidateIndex.dbIndexHandler;
var keys = queryDomainType.mynode_query_domain_type.queryHandler.getKeys(userContext.user_arguments[0]);
userContext.operation = dbSession.buildReadOperation(dbIndexHandler, keys, transactionHandler,
executeQueryKeyOnResult);
// TODO: this currently does not support batching
transactionHandler.execute([userContext.operation], function() {
if(udebug.is_detail()) { udebug.log('executeQueryPK transactionHandler.execute callback.'); }
});
// 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)
udebug.log('QueryDomainType.execute', queryDomainType.mynode_query_domain_type.predicate,
'with parameters', userContext.user_arguments[0]);
// execute the query and call back user
queryType = queryDomainType.mynode_query_domain_type.queryType;
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;
var object;
function persistOnResult(err, dbOperation) {
udebug.log('persist.persistOnResult');
// 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.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 tableHandler, object, 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.getIndexHandler(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.user_arguments[0], userContext.session, saveOnTableHandler);
return userContext.promise;
};
/** Update the object.
*
*/
exports.UserContext.prototype.update = function() {
var userContext = this;
var tableHandler, object, 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.user_arguments[0], userContext.session, updateOnTableHandler);
return userContext.promise;
};
/** Load the object.
*
*/
exports.UserContext.prototype.load = function() {
var userContext = this;
var tableHandler;
function loadOnResult(err, dbOperation) {
udebug.log('load.loadOnResult');
var result, values;
var error = checkOperation(err, dbOperation);
if (error) {
if (userContext.session.tx.isActive()) {
userContext.session.tx.setRollbackOnly();
}
userContext.applyCallback(err);
return;
}
values = dbOperation.result.value;
// apply the values to the parameter domain object
userContext.dbTableHandler.setFields(userContext.user_arguments[0], values);
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];
// ask getIndexHandler for only unique key indexes
index = dbTableHandler.getIndexHandler(keys, true);
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,
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].mynode) !== 'object') {
userContext.applyCallback(new Error('Illegal argument: load requires a mapped domain object.'));
return;
}
var ctor = userContext.user_arguments[0].mynode.constructor;
getTableHandler(ctor, userContext.session, loadOnTableHandler);
return userContext.promise;
};
/** Remove the object.
*
*/
exports.UserContext.prototype.remove = function() {
var userContext = this;
var tableHandler, object;
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 {
var result = dbOperation.result.value;
userContext.applyCallback(null);
}
}
function removeOnTableHandler(err, dbTableHandler) {
var transactionHandler, object, 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.getIndexHandler(userContext.keys, true);
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.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.resolvedMapping;
userContext.applyCallback(null, mapping);
}
// getMapping starts here
getTableHandler(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 (typeof(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 mynode.openSession (without a session factory)
* and SessionFactory.openSession (with a session factory).
*/
exports.UserContext.prototype.openSession = function() {
var userContext = this;
var openSessionOnSession = function(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;
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() {
var userContext, openFactories, nToClose;
userContext = this;
openFactories = mynode.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 (typeof(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);
};
exports.Promise = Promise;