609 lines
18 KiB
JavaScript
609 lines
18 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 udebug = unified_debug.getLogger("TableMapping.js"),
|
|
path = require("path"),
|
|
util = require("util"),
|
|
doc = require(path.join(mynode.fs.api_doc_dir, "TableMapping"));
|
|
|
|
/* file scope mapping id used to uniquely identify a mapped domain object */
|
|
var mappingId = 0;
|
|
|
|
/* Code to verify the validity of a TableMapping */
|
|
|
|
function isString(value) {
|
|
return (typeof value === 'string' && value !== null);
|
|
}
|
|
|
|
function isNonEmptyString(value) {
|
|
return (isString(value) && value.length > 0);
|
|
}
|
|
|
|
function isBool(value) {
|
|
return (value === true || value === false);
|
|
}
|
|
|
|
function isValidConverterObject(converter) {
|
|
return ((converter === null) ||
|
|
(typeof converter === 'object'
|
|
&& typeof converter.toDB === 'function'
|
|
&& typeof converter.fromDB === 'function'));
|
|
}
|
|
|
|
function isValidConstructor(constructor) {
|
|
return (constructor != null && typeof constructor === 'function');
|
|
}
|
|
|
|
function isMeta(value) {
|
|
var i;
|
|
if (Array.isArray(value)) {
|
|
for (i=0; i > value.length; ++i) {
|
|
if (!value[i].isMeta || ! value[i].isMeta()) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
return (value.isMeta());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function Relationship() {
|
|
}
|
|
Relationship.prototype.relationship = true;
|
|
Relationship.prototype.persistent = true;
|
|
|
|
function OneToOneMapping() {
|
|
}
|
|
OneToOneMapping.prototype = new Relationship();
|
|
|
|
function OneToManyMapping() {
|
|
}
|
|
OneToManyMapping.prototype = new Relationship();
|
|
OneToManyMapping.prototype.toMany = true;
|
|
|
|
function ManyToOneMapping() {
|
|
}
|
|
ManyToOneMapping.prototype = new Relationship();
|
|
ManyToOneMapping.prototype.manyTo = true;
|
|
|
|
function ManyToManyMapping() {
|
|
}
|
|
ManyToManyMapping.prototype = new Relationship();
|
|
ManyToManyMapping.prototype.toMany = true;
|
|
ManyToManyMapping.prototype.manyTo = true;
|
|
|
|
var fieldMappingProperties = {
|
|
"fieldName" : isNonEmptyString,
|
|
"columnName" : isString,
|
|
"persistent" : isBool,
|
|
"converter" : isValidConverterObject,
|
|
"relationship" : isBool,
|
|
"user" : function() { return true; },
|
|
"meta" : isMeta
|
|
};
|
|
|
|
var manyToOneMappingProperties = {
|
|
"type" : "ManyToOne",
|
|
"foreignKey" : isNonEmptyString,
|
|
"target" : isValidConstructor,
|
|
"targetField" : isNonEmptyString,
|
|
"fieldName" : isNonEmptyString,
|
|
"columnName" : isString,
|
|
"converter" : isValidConverterObject,
|
|
"user" : function() { return true; },
|
|
"ctor" : ManyToOneMapping
|
|
};
|
|
|
|
var oneToManyMappingProperties = {
|
|
"type" : "OneToMany",
|
|
"target" : isValidConstructor,
|
|
"targetField" : isNonEmptyString,
|
|
"fieldName" : isNonEmptyString,
|
|
"columnName" : isString,
|
|
"converter" : isValidConverterObject,
|
|
"user" : function() { return true; },
|
|
"ctor" : OneToManyMapping
|
|
};
|
|
|
|
var manyToManyMappingProperties = {
|
|
"type" : "ManyToMany",
|
|
"target" : isValidConstructor,
|
|
"targetField" : isNonEmptyString,
|
|
"fieldName" : isNonEmptyString,
|
|
"columnName" : isString,
|
|
"converter" : isValidConverterObject,
|
|
"joinTable" : isNonEmptyString,
|
|
"user" : function() { return true; },
|
|
"ctor" : ManyToManyMapping
|
|
};
|
|
|
|
var oneToOneMappingProperties = {
|
|
"type" : "OneToOne",
|
|
"foreignKey" : isNonEmptyString,
|
|
"target" : isValidConstructor,
|
|
"targetField" : isNonEmptyString,
|
|
"fieldName" : isNonEmptyString,
|
|
"columnName" : isString,
|
|
"converter" : isValidConverterObject,
|
|
"user" : function() { return true; },
|
|
"ctor" : OneToOneMapping
|
|
};
|
|
|
|
// These functions return error message, or empty string if valid
|
|
function verifyProperty(property, value, verifiers) {
|
|
udebug.log_detail('verifyProperty', property, value);
|
|
var isValid = '', chk;
|
|
if(verifiers[property]) {
|
|
chk = verifiers[property](value);
|
|
if(chk !== true && chk.length) {
|
|
isValid = 'property ' + property + ' invalid: ' + chk;
|
|
}
|
|
else if(chk === false) {
|
|
isValid = 'property ' + property + ' invalid: ' + JSON.stringify(value);
|
|
}
|
|
}
|
|
else if(typeof value !== 'function') {
|
|
isValid = 'unknown property ' + property +'; ' ;
|
|
}
|
|
return isValid;
|
|
}
|
|
|
|
function isValidMapping(m, verifiers) {
|
|
var property, isValid = '';
|
|
for(property in m) {
|
|
if(m.hasOwnProperty(property)) {
|
|
isValid += verifyProperty(property, m[property], verifiers);
|
|
}
|
|
}
|
|
return isValid;
|
|
}
|
|
|
|
function isValidFieldMapping(fm, number) {
|
|
var reason = isValidMapping(fm, fieldMappingProperties);
|
|
number = number || '';
|
|
if(reason.length) {
|
|
return "field " + number + " is not a valid FieldMapping: " + reason;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function isValidFieldMappingArray(fieldMappings) {
|
|
var i, isValid = '';
|
|
if(fieldMappings !== null) {
|
|
for(i = 0; i < fieldMappings.length ; i++) {
|
|
isValid += isValidFieldMapping(fieldMappings[i], i+1);
|
|
}
|
|
}
|
|
return isValid;
|
|
}
|
|
|
|
function isStringOrStringArray(arg) {
|
|
var i;
|
|
if (typeof arg === 'string') return true;
|
|
if (!Array.isArray(arg)) return 'must be a string or string array';
|
|
for (i = 0; i < arg.length; ++i) {
|
|
if (typeof arg[i] !== 'string') return 'must be a string or string array';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
var tableMappingProperties = {
|
|
"error" : isString,
|
|
"table" : isNonEmptyString,
|
|
"database" : isString,
|
|
"mapAllColumns" : isBool,
|
|
"field" : isValidFieldMapping,
|
|
"fields" : isValidFieldMappingArray,
|
|
"user" : function() { return true; },
|
|
"excludedFieldNames": isStringOrStringArray,
|
|
"mappedFieldNames" : isStringOrStringArray,
|
|
"meta" : isMeta
|
|
};
|
|
|
|
function isValidTableMapping(tm) {
|
|
var err = isValidMapping(tm, tableMappingProperties);
|
|
if (!err) {
|
|
// make sure there is a valid table
|
|
if (!tm.hasOwnProperty('table')) {
|
|
return '\nRequired property \'table\' is missing.';
|
|
}
|
|
} else {
|
|
return err;
|
|
}
|
|
}
|
|
|
|
function buildMappingFromObject(mapping, literal, verifier) {
|
|
var p, keys, key;
|
|
keys = Object.keys(verifier);
|
|
for(p in keys) {
|
|
key = keys[p];
|
|
if(typeof literal[key] !== 'undefined') {
|
|
mapping[key] = literal[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* A canonical TableMapping has a "fields" array,
|
|
though a literal one may have a "field" or "fields" object or array
|
|
*/
|
|
function makeCanonical(tableMapping) {
|
|
if(tableMapping.field) { // rename field => fields
|
|
tableMapping.fields = tableMapping.field;
|
|
delete tableMapping.field;
|
|
}
|
|
|
|
if(! tableMapping.fields) {
|
|
tableMapping.fields = []; // create empty fields array if needed
|
|
}
|
|
else if(! Array.isArray(tableMapping.fields)) {
|
|
tableMapping.fields = [ tableMapping.fields ];
|
|
}
|
|
}
|
|
|
|
|
|
/* TableMapping constructor
|
|
Takes tableName or tableMappingLiteral
|
|
*/
|
|
function TableMapping(tableNameOrLiteral) {
|
|
var err;
|
|
var i, arg;
|
|
switch(typeof tableNameOrLiteral) {
|
|
case 'object':
|
|
buildMappingFromObject(this, tableNameOrLiteral, tableMappingProperties);
|
|
makeCanonical(this);
|
|
break;
|
|
|
|
case 'string':
|
|
var parts = tableNameOrLiteral.split(".");
|
|
if (parts[2] || tableNameOrLiteral.indexOf(' ') !== -1) {
|
|
this.error = 'MappingError: tableName must contain one or two parts: [database.]table';
|
|
this.table = parts[0];
|
|
} else if(parts[0] && parts[1]) {
|
|
this.database = parts[0];
|
|
this.table = parts[1];
|
|
}
|
|
else {
|
|
this.table = parts[0];
|
|
}
|
|
this.fields = [];
|
|
this.mappedFieldNames = [];
|
|
if (arguments.length >1) {
|
|
this.meta = [];
|
|
// look for optional meta following the table name
|
|
for (i = 1; i < arguments.length; i++) {
|
|
arg = arguments[i];
|
|
if (arg && arg.isMeta && arg.isMeta()) {
|
|
this.meta.push(arg);
|
|
} else {
|
|
this.error += 'MappingError: valid arguments are meta; invalid argument ' + i + ': (' + typeof arg + ') ' + arg;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
this.error = "MappingError: string tableName or literal tableMapping is a required parameter.";
|
|
}
|
|
err = isValidTableMapping(this);
|
|
if (err) {
|
|
this.error += err;
|
|
}
|
|
}
|
|
/* Get prototype from documentation
|
|
*/
|
|
TableMapping.prototype = doc.TableMapping;
|
|
|
|
|
|
/* FieldMapping constructor
|
|
* This is exported & used by DBTableHandler, but not by the public.
|
|
*/
|
|
function FieldMapping(fieldName) {
|
|
this.fieldName = fieldName;
|
|
this.columnName = fieldName;
|
|
this.relationship = false;
|
|
}
|
|
FieldMapping.prototype = doc.FieldMapping;
|
|
|
|
|
|
/* mapField(fieldName, [columnName], [converter], [persistent])
|
|
mapField(literalFieldMapping)
|
|
IMMEDIATE
|
|
|
|
Create or replace FieldMapping for fieldName
|
|
*/
|
|
TableMapping.prototype.mapField = function() {
|
|
var i, args, arg, fieldName, fieldMapping;
|
|
args = arguments;
|
|
|
|
function getFieldMapping(tableMapping, fieldName) {
|
|
var fm, i;
|
|
for(i = 0 ; i < tableMapping.fields.length ; i++) {
|
|
fm = tableMapping.fields[i];
|
|
if(fm.fieldName === fieldName) {
|
|
return fm;
|
|
}
|
|
}
|
|
fm = new FieldMapping(fieldName);
|
|
tableMapping.fields.push(fm);
|
|
return fm;
|
|
}
|
|
|
|
/* mapField() starts here */
|
|
arg = args[0];
|
|
if(typeof arg === 'string') {
|
|
fieldName = arg;
|
|
fieldMapping = getFieldMapping(this, fieldName);
|
|
for(i = 1; i < args.length ; i++) {
|
|
arg = args[i];
|
|
switch(typeof arg) {
|
|
case 'string':
|
|
fieldMapping.columnName = arg;
|
|
break;
|
|
case 'boolean':
|
|
fieldMapping.persistent = arg;
|
|
break;
|
|
case 'object':
|
|
// argument is a meta or converter
|
|
if (arg && arg.isMeta && arg.isMeta()) {
|
|
fieldMapping.meta = arg;
|
|
} else {
|
|
fieldMapping.converter = arg;
|
|
}
|
|
break;
|
|
default:
|
|
this.error += "mapField(): Invalid argument " + arg;
|
|
}
|
|
}
|
|
}
|
|
else if(typeof args[0] === 'object') {
|
|
fieldName = args[0].fieldName;
|
|
fieldMapping = getFieldMapping(this, fieldName);
|
|
buildMappingFromObject(fieldMapping, args[0], fieldMappingProperties);
|
|
}
|
|
else {
|
|
this.error +="\nmapField() expects a literal FieldMapping or valid arguments list";
|
|
}
|
|
|
|
/* Validate the candidate mapping */
|
|
this.error += isValidFieldMapping(fieldMapping);
|
|
this.mappedFieldNames.push(fieldName);
|
|
return this;
|
|
};
|
|
|
|
function createRelationshipFieldFromLiteral(relationshipProperties, tableMapping, literal) {
|
|
var relationship = new relationshipProperties.ctor();
|
|
relationship.error = '';
|
|
var fieldValidator, value, valid;
|
|
var errorMessage = "";
|
|
// iterate the literal and set properties
|
|
var literalField;
|
|
for (literalField in literal) {
|
|
if (literal.hasOwnProperty(literalField)) {
|
|
// validate each field in the literal
|
|
udebug.log_detail('createRelationshipFieldFromLiteral validating', relationshipProperties.type, literalField,
|
|
literal[literalField]);
|
|
fieldValidator = relationshipProperties[literalField];
|
|
if (!fieldValidator) {
|
|
errorMessage += "\nMappingError: invalid literal field: " + literalField + "\n";
|
|
} else {
|
|
value = literal[literalField];
|
|
valid = fieldValidator(value);
|
|
udebug.log_detail('createRelationshipFieldFromLiteral fieldValidator for', literalField, "is", valid);
|
|
if (valid) {
|
|
relationship[literalField] = value;
|
|
} else {
|
|
errorMessage += "\nMappingError: invalid value for literal field: " + literalField + "\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!relationship.fieldName) {
|
|
errorMessage += "\nMappingError: fieldName is a required field for relationship mapping";
|
|
}
|
|
if (!relationship.targetField && !relationship.foreignKey && !relationship.joinTable) {
|
|
errorMessage += "\nMappingError: targetField, foreignKey, or joinTable is a required field for relationship mapping";
|
|
}
|
|
if (!relationship.target) {
|
|
errorMessage += '\nMappingError: target is a required field for relationship mapping';
|
|
}
|
|
if (errorMessage) {
|
|
tableMapping.error += errorMessage;
|
|
}
|
|
return relationship;
|
|
}
|
|
|
|
/* mapOneToOne(literalFieldMapping)
|
|
* IMMEDIATE
|
|
*/
|
|
TableMapping.prototype.mapOneToOne = function(literalMapping) {
|
|
var mapping;
|
|
if (typeof literalMapping === 'object') {
|
|
mapping = createRelationshipFieldFromLiteral(oneToOneMappingProperties, this, literalMapping);
|
|
this.fields.push(mapping);
|
|
} else {
|
|
this.error += '\nMappingError: mapOneToOne supports only literal field mapping';
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/* mapManyToOne(literalFieldMapping)
|
|
* IMMEDIATE
|
|
*/
|
|
TableMapping.prototype.mapManyToOne = function(literalMapping) {
|
|
var mapping;
|
|
if (typeof literalMapping === 'object') {
|
|
mapping = createRelationshipFieldFromLiteral(manyToOneMappingProperties, this, literalMapping);
|
|
this.fields.push(mapping);
|
|
} else {
|
|
this.error += '\nMappingError: mapManyToOne supports only literal field mapping';
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/* mapOneToMany(literalFieldMapping)
|
|
* IMMEDIATE
|
|
*/
|
|
TableMapping.prototype.mapOneToMany = function(literalMapping) {
|
|
var mapping;
|
|
if (typeof literalMapping === 'object') {
|
|
mapping = createRelationshipFieldFromLiteral(oneToManyMappingProperties, this, literalMapping);
|
|
this.fields.push(mapping);
|
|
} else {
|
|
this.error += '\nMappingError: mapManyToOne supports only literal field mapping';
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/* mapManyToMany(literalFieldMapping)
|
|
* IMMEDIATE
|
|
*/
|
|
TableMapping.prototype.mapManyToMany = function(literalMapping) {
|
|
var mapping;
|
|
if (typeof literalMapping === 'object') {
|
|
mapping = createRelationshipFieldFromLiteral(manyToManyMappingProperties, this, literalMapping);
|
|
this.fields.push(mapping);
|
|
} else {
|
|
this.error += '\nMappingError: mapManyToOne supports only literal field mapping';
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/** excludeFields(fieldNames)
|
|
* Exclude the named field(s) from being persisted as part of sparse field handling.
|
|
*/
|
|
TableMapping.prototype.excludeFields = function() {
|
|
var i, j, fieldName;
|
|
if (!this.excludedFieldNames) this.excludedFieldNames = [];
|
|
for (i = 0; i < arguments.length; ++i) {
|
|
var fieldNames = arguments[i];
|
|
if (typeof fieldNames === 'string') {
|
|
this.excludedFieldNames.push(fieldNames);
|
|
} else if (Array.isArray(fieldNames)) {
|
|
for (j = 0; j < fieldNames.length; ++j) {
|
|
fieldName = fieldNames[j];
|
|
if (typeof fieldName === 'string') {
|
|
this.excludedFieldNames.push(fieldName);
|
|
} else {
|
|
this.error += '\nMappingError: excludeFields argument must be a field name or an array or list of field names: \"' +
|
|
fieldName + '\"';
|
|
}
|
|
}
|
|
} else {
|
|
this.error += '\nMappingError: excludeFields argument must be a field name or an array or list of field names: \"' +
|
|
fieldNames + '\"';
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/* mapSparseFields(columnName, fieldNames, converter)
|
|
* columnName: required
|
|
* fieldNames: optional string or array of strings
|
|
* converter: optional converter function default Converters/JSONSparseFieldsConverter
|
|
*/
|
|
TableMapping.prototype.mapSparseFields = function() {
|
|
var i, j, args, arg, columnName, fieldMapping, sparseFieldNames = [];
|
|
args = arguments;
|
|
|
|
if(typeof args[0] === 'string') {
|
|
columnName = args[0];
|
|
fieldMapping = new FieldMapping(columnName);
|
|
fieldMapping.tableMapping = this;
|
|
for(i = 1; i < args.length ; i++) {
|
|
arg = args[i];
|
|
switch(typeof arg) {
|
|
case 'string':
|
|
sparseFieldNames.push(arg);
|
|
break;
|
|
case 'object':
|
|
if (Array.isArray(arg)) {
|
|
// verify array of field names
|
|
for (j = 0; j < arg.length; ++j) {
|
|
if (typeof arg[j] !== 'string') {
|
|
this.error += "\nmapSparseFields Illegal argument; element " + j +
|
|
" is not a string: \"" + util.inspect(arg[j]) + "\"";
|
|
} else {
|
|
sparseFieldNames.push(arg[j]);
|
|
}
|
|
}
|
|
} else {
|
|
// argument is a meta or converter
|
|
if (arg && arg.isMeta && arg.isMeta()) {
|
|
fieldMapping.meta = arg;
|
|
} else {
|
|
// validate converter
|
|
if (isValidConverterObject(arg)) {
|
|
fieldMapping.converter = arg;
|
|
} else {
|
|
this.error += "\nmapSparseFields Argument is an object " +
|
|
"that is not a meta, an array of field names, or a converter object: \"" + util.inspect(arg) + "\"";
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
this.error += "\nmapSparseFields: Argument must be a field name, a meta, an array of field names, or a converter object: \"" +
|
|
util.inspect(arg) + "\"";
|
|
}
|
|
}
|
|
if (!fieldMapping.converter) {
|
|
// default sparse fields converter
|
|
fieldMapping.converter = mynode.converters.JSONSparseConverter;
|
|
}
|
|
if (sparseFieldNames.length !== 0) {
|
|
fieldMapping.sparseFieldNames = sparseFieldNames;
|
|
}
|
|
fieldMapping.sparseFieldMapping = true;
|
|
this.fields.push(fieldMapping);
|
|
}
|
|
else {
|
|
this.error +="\nmapSparseFields() requires a valid arguments list with column name as the first argument";
|
|
}
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
/* applyToClass(constructor)
|
|
IMMEDIATE
|
|
*/
|
|
TableMapping.prototype.applyToClass = function(ctor) {
|
|
if (typeof ctor === 'function') {
|
|
ctor.prototype.mynode = {};
|
|
ctor.prototype.mynode.mapping = this;
|
|
ctor.prototype.mynode.constructor = ctor;
|
|
ctor.prototype.mynode.mappingId = ++mappingId;
|
|
} else {
|
|
this.error += '\nMappingError: applyToClass() parameter must be constructor';
|
|
}
|
|
return ctor;
|
|
};
|
|
|
|
|
|
/* Public exports of this module: */
|
|
exports.TableMapping = TableMapping;
|
|
exports.FieldMapping = FieldMapping;
|
|
exports.isValidConverterObject = isValidConverterObject;
|