database-jones/Adapter/api/TableMapping.js (481 lines of code) (raw):

/* Copyright (c) 2015, 2106, 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"), assert = require("assert"), Meta = require("./Meta"); /* file scope mapping id used to uniquely identify a mapped domain object */ var mappingId = 0; //////// FieldMapping ///////////////// function FieldMapping(fieldName) { this.fieldName = fieldName; this.persistent = true; } //////// Relationship ///////////////// function Relationship() { this.relationship = true; this.persistent = true; this.toMany = false; this.manyTo = false; this.target = null; // a mapped constructor this.targetField = ""; this.columnName = ""; this.foreignKey = ""; this.converter = null; this.error = ""; } function OneToOneMapping() { Relationship.call(this); } function OneToManyMapping() { Relationship.call(this); this.toMany = true; } function ManyToOneMapping() { Relationship.call(this); this.manyTo = true; } function ManyToManyMapping() { Relationship.call(this); this.toMany = true; this.manyTo = true; this.joinTable = ""; } //////// Functions to verify the validity of an object literal function noCheck() { return true; } function isString(value) { return (typeof value === 'string'); } function isNonEmptyString(value) { return (typeof value === 'string' && value.length > 0); } function isEmptyString(value) { return (value === ""); } function isBool(value) { return (value === true || value === false); } function isBoolOrNull(value) { return (value === true || value === false || value === null); } function isConverter(converter) { return ((converter === null) || (typeof converter === 'object' && typeof converter.toDB === 'function' && typeof converter.fromDB === 'function')); } function isConverterMap(obj) { var property; for(property in obj) { if(obj.hasOwnProperty(property)) { if(! isConverter(obj[property])) { return false; } } } return true; } function isFunction(value) { return (typeof value === 'function'); } function isMeta(value) { return (value && value.isMeta && value.isMeta()); } function isLiteralMeta(value) { return (value && typeof value === 'object' && typeof value.isNullable === 'boolean'); } function isMetaOrLiteral(value) { return (isMeta(value) || isLiteralMeta(value)); } function isMinimalField(value) { return (typeof value === 'object' && value !== null && typeof value.fieldName === 'string'); } function isArrayOf(elementVerifier) { assert.equal(typeof elementVerifier, "function"); return function(value) { var i; if(! Array.isArray(value)) { return false; } for(i = 0 ; i < value.length ; i++) { if(! elementVerifier(value[i])) { return false; } } return true; }; } function isElementOrArrayOf(elementVerifier) { var arrayVerifier; assert.equal(typeof elementVerifier, "function"); arrayVerifier = isArrayOf(elementVerifier); return function(value) { return Array.isArray(value) ? arrayVerifier(value) : elementVerifier(value); }; } function LiteralObjectVerifier() { this.requiredProperties = []; // List of property names this.allowedProperties = []; // List of property names this.verifiers = {}; // Map property name => verifier function // Any literal object can have a property "user" which we ignore this.set("user", noCheck); } LiteralObjectVerifier.prototype.set = function(name, verifier) { this.allowedProperties.push(name); this.verifiers[name] = verifier; return this; // chainable }; LiteralObjectVerifier.prototype.setRequired = function(property) { this.requiredProperties.push(property); return this; // chainable }; // These functions return error message, or empty string if valid LiteralObjectVerifier.prototype.checkProperty = function(property, value) { var msg = ""; if(typeof this.verifiers[property] === 'function') { if(! this.verifiers[property](value)) { msg = "property " + property + " invalid: " + JSON.stringify(value); } } else { msg = "unknown property " + property + "; " ; } return msg; }; LiteralObjectVerifier.prototype.getErrors = function(literal) { var property, msg, i, req; msg = ""; for(property in literal) { if(literal.hasOwnProperty(property)) { msg += this.checkProperty(property, literal[property]); } } for(i = 0; i < this.requiredProperties.length ; i++) { req = this.requiredProperties[i]; if(! literal.hasOwnProperty(req)) { msg += "Required property '" + req + "' is missing; "; } } return msg; }; LiteralObjectVerifier.prototype.buildObjectFromLiteral = function(object, literal) { var i, key, errors; errors = this.getErrors(literal); if(! errors.length) { for(i = 0 ; i < this.allowedProperties.length ; i++) { key = this.allowedProperties[i]; if(literal[key] !== undefined) { object[key] = literal[key]; } } } return errors; }; function getValidator(literalObjectVerifier) { return function(value) { var errors = literalObjectVerifier.getErrors(value); return (errors.length === 0); }; } //////// Allowed properties in literal values function BasicFieldVerifier() { var that = new LiteralObjectVerifier(); // properties that can be present in any field-related literal that.set("fieldName", isNonEmptyString); that.set("columnName", isString); that.set("persistent", isBool); that.set("converter", isConverter); that.set("relationship", isBool); that.setRequired("fieldName"); return that; } /* A FieldMapping literal can have any of the basic properties, plus "meta" */ var fieldMappingProperties = new BasicFieldVerifier(). set("toManyColumns", isArrayOf(isString)). set("meta", isMetaOrLiteral); function RelationshipVerifier(type, ctor) { var that = new BasicFieldVerifier(); that.type = type; that.ctor = ctor; // properties that can be present in any relationship literal: that.set("target", isFunction); that.setRequired("target"); that.set("targetField", isNonEmptyString); return that; } var manyToOneMappingProperties = new RelationshipVerifier("ManyToOne", ManyToOneMapping). set("foreignKey", isNonEmptyString); var oneToManyMappingProperties = new RelationshipVerifier("OneToMany", OneToManyMapping); var manyToManyMappingProperties = new RelationshipVerifier("ManyToMany", ManyToManyMapping). set("joinTable", isNonEmptyString); var oneToOneMappingProperties = new RelationshipVerifier("OneToOne", OneToOneMapping). set("foreignKey", isNonEmptyString); //////// TableMapping ///////////////// /* Table Mapping literal properties */ var tableMappingProperties = new LiteralObjectVerifier(). set("table", isNonEmptyString). set("database", isString). set("mapAllColumns", isBool). set("fields", isArrayOf(isMinimalField)). set("excludedFieldNames", isArrayOf(isNonEmptyString)). set("meta", isElementOrArrayOf(isMetaOrLiteral)). set("sparseContainer", getValidator(fieldMappingProperties)). set("error", isEmptyString). set("columnConverterMap", isConverterMap). setRequired("table"); function TableMapping(tableNameOrLiteral) { this.table = ""; this.database = ""; this.mapAllColumns = true; this.fields = []; this.excludedFieldNames = []; this.meta = []; this.columnConverterMap = {}; this.error = ""; switch(typeof tableNameOrLiteral) { case 'object': this.constructFromObject(tableNameOrLiteral); break; case 'string': this.constructFromTableName(tableNameOrLiteral); break; default: this.error = "TableMapping(): string tableName or " + "literal tableMapping is a required parameter."; return; } if (arguments.length > 1) { // Each additional argument is a Meta this.assignMeta(arguments); } } TableMapping.prototype.isValid = function() { return (this.error.length === 0); }; TableMapping.prototype.constructFromObject = function(literal) { if(literal.field && ! literal.fields) { literal.fields = [ literal.field ]; } else if(literal.fields && ! Array.isArray(literal.fields)) { literal.fields = [ literal.fields ]; } this.error = tableMappingProperties.buildObjectFromLiteral(this, literal); if(this.isValid()) { // Build arrays that are independent from the original this.fields = this.fields.slice(); this.excludedFieldNames = this.excludedFieldNames.slice(); } }; TableMapping.prototype.constructFromTableName = function(tableName) { var parts = tableName.split("."); if (parts[2] || tableName.indexOf(' ') !== -1) { this.error = 'MappingError: tableName must contain one or two parts: [database.]table'; } else if(parts[0] && parts[1]) { this.database = parts[0]; this.table = parts[1]; } else { this.table = parts[0]; } }; TableMapping.prototype.assignMeta = function(args) { var i, arg; for (i = 1; i < args.length; i++) { arg = args[i]; if(isMeta(arg)) { this.meta.push(arg); } else if(isLiteralMeta(arg)) { this.meta.push(Meta.fromLiteralMeta(arg)); } else { this.error += 'MappingError: valid arguments are meta; invalid argument ' + i + ': (' + typeof arg + ') ' + arg; } } }; TableMapping.prototype.getFieldMapping = function(fieldName) { var fm, j; for(j = 0 ; j < this.fields.length ; j++) { fm = this.fields[j]; if(fm.fieldName === fieldName) { return fm; } } }; /* mapField(fieldName, [columnName], [converter], [meta], [persistent]) mapField(literalFieldMapping) IMMEDIATE Create FieldMapping for fieldName */ TableMapping.prototype.mapField = function(nameOrLiteral) { var i, arg, fieldMapping, fieldName, fieldMappingLiteral, errors; switch(typeof nameOrLiteral) { case 'string': fieldMappingLiteral = { "fieldName" : nameOrLiteral }; for(i = 1; i < arguments.length ; i++) { arg = arguments[i]; switch(typeof arg) { case 'string': fieldMappingLiteral.columnName = arg; break; case 'boolean': fieldMappingLiteral.persistent = arg; break; case 'object': if (isMeta(arg)) { fieldMappingLiteral.meta = arg; } else if(isLiteralMeta(arg)) { fieldMappingLiteral.meta = Meta.fromLiteralMeta(arg); } else if(isConverter(arg)) { fieldMappingLiteral.converter = arg; } else if(isArrayOf(isString)(arg)) { fieldMappingLiteral.toManyColumns = arg; } else { this.error += "mapField(): Invalid argument " + arg; } break; default: this.error += "mapField(): Invalid argument " + arg; } } break; case 'object': fieldMappingLiteral = nameOrLiteral; break; default: this.error += "mapField() expects a literal FieldMapping or valid arguments list"; return this; } errors = fieldMappingProperties.getErrors(fieldMappingLiteral); if(errors.length) { this.error += errors; udebug.log("mapField() Errors: ", errors); } else { fieldName = fieldMappingLiteral.fieldName; if(this.getFieldMapping(fieldName)) { udebug.log("mapField(): Duplicate fieldName error"); this.error += '\nmapField(): "' + fieldName + '" is duplicated; it cannot replace an existing field.'; } else { fieldMapping = new FieldMapping(fieldName); fieldMappingProperties.buildObjectFromLiteral(fieldMapping, fieldMappingLiteral); this.fields.push(fieldMapping); if (!fieldMapping.persistent) { this.excludedFieldNames.push(fieldMapping.fieldName); } udebug.log("mapField success: field", fieldMapping); } } return this; }; TableMapping.prototype.createRelationshipField = function(verifier, literal) { var relationship = new verifier.ctor(); var errorMessage = verifier.getErrors(literal); if (!literal.targetField && !literal.foreignKey && !literal.joinTable) { errorMessage += "\nMappingError: targetField, foreignKey, or joinTable is a required field for relationship mapping"; } if(this.getFieldMapping(literal.fieldName)) { errorMessage += '\nMappingError: relationship field "' + literal.fieldName + '" is duplicated.'; } if (errorMessage) { this.error += errorMessage; } else { verifier.buildObjectFromLiteral(relationship, literal); return relationship; } }; TableMapping.prototype.mapRelationship = function(relationshipVerifier, literalMapping) { var mapping; if (typeof literalMapping !== 'object') { this.error += "\nMappingError: map" + relationshipVerifier.type + " supports only literal field mapping"; return this; } if(this.getFieldMapping(literalMapping.fieldName)) { this.error += '"' + literalMapping.fieldName + '" is duplicated; ' + "it cannot replace an existing field."; return this; } mapping = this.createRelationshipField(relationshipVerifier, literalMapping); if(mapping) { this.fields.push(mapping); } return this; }; /* mapOneToOne(literalFieldMapping) * IMMEDIATE */ TableMapping.prototype.mapOneToOne = function(literalMapping) { return this.mapRelationship(oneToOneMappingProperties, literalMapping); }; /* mapManyToOne(literalFieldMapping) * IMMEDIATE */ TableMapping.prototype.mapManyToOne = function(literalMapping) { return this.mapRelationship(manyToOneMappingProperties, literalMapping); }; /* mapOneToMany(literalFieldMapping) * IMMEDIATE */ TableMapping.prototype.mapOneToMany = function(literalMapping) { return this.mapRelationship(oneToManyMappingProperties, literalMapping); }; /* mapManyToMany(literalFieldMapping) * IMMEDIATE */ TableMapping.prototype.mapManyToMany = function(literalMapping) { return this.mapRelationship(manyToManyMappingProperties, literalMapping); }; /** fieldIsNotSparse(name) * Exclude a field from sparse field handling. */ TableMapping.prototype.fieldIsNotSparse = function(fieldName) { if (this.excludedFieldNames.indexOf(fieldName) === -1) { this.excludedFieldNames.push(fieldName); } return this; }; /** excludeFields(fieldNames) * Exclude a list or array of named field(s) from being persisted. */ TableMapping.prototype.excludeFields = function() { var i, j, fieldName, fieldNames; for (i = 0; i < arguments.length; ++i) { fieldNames = arguments[i]; if (typeof fieldNames === 'string') { this.fieldIsNotSparse(fieldNames); } else if (Array.isArray(fieldNames)) { for (j = 0; j < fieldNames.length; ++j) { fieldName = fieldNames[j]; if (typeof fieldName === 'string') { this.fieldIsNotSparse(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 + '\"'; } } return this; }; /* mapSparseFields(columnName, converter) * columnName: required * converter: optional converter function */ TableMapping.prototype.mapSparseFields = function(columnName) { var i, arg, fieldMapping; if(typeof columnName === 'string') { fieldMapping = new FieldMapping(columnName); fieldMapping.columnName = columnName; for(i = 1; i < arguments.length ; i++) { arg = arguments[i]; switch(typeof arg) { case 'object': // argument is a meta or converter if(isMeta(arg)) { fieldMapping.meta = arg; } else if(isLiteralMeta(arg)) { fieldMapping.meta = Meta.fromLiteralMeta(arg); } else if (isConverter(arg)) { // validate converter fieldMapping.converter = arg; } else { this.error += "\nmapSparseFields Argument is an object " + "that is not a meta or a converter object: \"" + util.inspect(arg) + "\""; } break; default: this.error += "\nmapSparseFields: Argument must be a meta or converter object: \"" + util.inspect(arg) + "\""; } } this.sparseContainer = fieldMapping; } else { this.error +="\nmapSparseFields() requires a valid arguments list with column name as the first argument"; } return this; }; /* registerColumnConverter(name, converter): register a Converter for a named column IMMEDIATE */ TableMapping.prototype.registerColumnConverter = function(name, converter) { var error; if(isConverter(converter)) { this.columnConverterMap[name] = converter; } else { error = "registerColumnConverter() for column " + name + ": not a converter"; this.error += error; } return error; }; /* applyToClass(constructor) IMMEDIATE */ TableMapping.prototype.applyToClass = function(ctor) { if (typeof ctor === 'function') { ctor.prototype.jones = {}; ctor.prototype.jones.mapping = this; ctor.prototype.jones.constructor = ctor; ctor.prototype.jones.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 = isConverter;