ezobjects
Advanced tools
Comparing version 1.1.5 to 2.0.0
const ezobjects = require('./index'); | ||
/** Create a customized object on the global (node) or window (browser) namespace */ | ||
ezobjects({ | ||
name: 'DatabaseRecord', | ||
ezobjects.createObject({ | ||
className: 'DatabaseRecord', | ||
properties: [ | ||
{ name: 'id', type: 'int' } | ||
{ name: 'id', type: 'number', setTransform: x => parseInt(x) } | ||
] | ||
@@ -17,4 +17,4 @@ }); | ||
/** Create another customized object that extends the first one */ | ||
ezobjects({ | ||
name: 'Person', | ||
ezobjects.createObject({ | ||
className: 'Person', | ||
extends: DatabaseRecord, | ||
@@ -24,3 +24,3 @@ properties: [ | ||
{ name: 'lastName', type: 'string' }, | ||
{ name: 'checkingBalance', type: 'float' }, | ||
{ name: 'checkingBalance', type: 'number', setTransform: x => parseFloat(x) }, | ||
{ name: 'permissions', type: 'Array' }, | ||
@@ -27,0 +27,0 @@ { name: 'favoriteDay', type: 'Date' } |
580
index.js
@@ -0,1 +1,3 @@ | ||
const mysqlConnection = require('./mysql-connection'); | ||
/** | ||
@@ -5,5 +7,198 @@ * @module ezobjects | ||
* @license MIT | ||
* @description Easy automatic class creation using simple configuration objects. Capable | ||
* of automatically creating a matching MySQL table and generating insert(), load(), and | ||
* update() methods in addition to the constructor, initializer, and getters/setters for | ||
* all configured properties. | ||
* | ||
* @signature createTable(db, obj) | ||
* @module ezobjects | ||
* @param db MySQLConnection | ||
* @param obj Object | ||
* @descrtiption Create a MySQL table with the specifications outlined in `obj`, if it doesn't already exist. | ||
*/ | ||
module.exports.createTable = async (db, obj) => { | ||
/** Create some helpful arrays for identifying MySQL types that have certain features */ | ||
const mysqlTypesAllowed = [`BIT`, `TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `INTEGER`, `BIGINT`, `REAL`, `DOUBLE`, `FLOAT`, | ||
`DECIMAL`, `NUMERIC`, `DATE`, `TIME`, `TIMESTAMP`, `DATETIME`, `YEAR`, `CHAR`, `VARCHAR`, `BINARY`, | ||
`VARBINARY`, `TINYBLOB`, `BLOB`, `MEDIUMBLOB`, `LONGBLOB`, `TINYTEXT`, `TEXT`, `MEDIUMTEXT`, | ||
`LONGTEXT`, `ENUM`, `SET`, `JSON`]; | ||
const mysqlTypesWithLength = [`BIT`, `TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `INTEGER`, `BIGINT`, `REAL`, `DOUBLE`, `FLOAT`, | ||
`DECIMAL`, `NUMERIC`, `CHAR`, `VARCHAR`, `BINARY`, `VARBINARY`, `BLOB`, `TEXT`]; | ||
const mysqlTypesWithDecimals = [`REAL`, `DOUBLE`, `FLOAT`, `DECIMAL`, `NUMERIC`]; | ||
const mysqlTypesWithLengthRequiringDecimals = [`REAL`, `DOUBLE`, `FLOAT`]; | ||
const mysqlTypesWithUnsignedAndZerofill = [`TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `INTEGER`, `BIGINT`, `REAL`, `DOUBLE`, `FLOAT`, | ||
`DECIMAL`, `NUMERIC`]; | ||
const mysqlTypesWithCharacterSetAndCollate = [`CHAR`, `VARCHAR`, `TINYTEXT`, `TEXT`, `MEDIUMTEXT`, `LONGTEXT`, `ENUM`, `SET`]; | ||
/** Helper method that can be recursively called to add all properties to the create query */ | ||
const addPropertiesToCreateQuery = (obj) => { | ||
/** If this object extends another, recursively add properties from the extended object */ | ||
if ( obj.extendsConfig ) | ||
addPropertiesToCreateQuery(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Convert the type to upper case for reliable string comparison */ | ||
property.mysqlType = property.mysqlType.toUpperCase(); | ||
/** Add property name and type to query */ | ||
createQuery += `${property.name} ${property.mysqlType}`; | ||
/** Types where length is required, throw error if missing */ | ||
if ( property.mysqlType == 'VARCHAR' && isNaN(property.length) ) | ||
throw new Error(`Property of type VARCHAR used without required length.`); | ||
else if ( property.mysqlType == 'VARBINARY' && isNaN(property.length) ) | ||
throw new Error(`Property of type VARBINARY used without required length.`); | ||
else if ( mysqlTypesWithLengthRequiringDecimals.includes(property.type) && !isNaN(property.length) && isNaN(property.decimals) ) | ||
throw new Error(`Property of type REAL, DOUBLE, or FLOAT used with length, but without decimals.`); | ||
/** Properties with length and/or decimals */ | ||
if ( !isNaN(property.length) && mysqlTypesWithLength.includes(property.mysqlType) && ( !mysqlTypesWithDecimals.includes(property.mysqlType) || isNaN(property.decimals) ) ) | ||
createQuery += `(${property.length})`; | ||
else if ( !isNaN(property.length) && !isNaN(property.decimals) && mysqlTypesWithLength.includes(property.mysqlType) && mysqlTypesWithDecimals.includes(property.mysqlType) ) | ||
createQuery += `(${property.length}, ${property.decimals})`; | ||
/** Properties with UNSIGNED */ | ||
if ( property.unsigned && mysqlTypesWithUnsignedAndZerofill.includes(property.mysqlType) ) | ||
createQuery += ` UNSIGNED`; | ||
/** Properties with ZEROFILL */ | ||
if ( property.zerofill && mysqlTypesWithUnsignedAndZerofill.includes(property.mysqlType) ) | ||
createQuery += ` ZEROFILL`; | ||
/** Properties with CHARACTER SET */ | ||
if ( property.charsetName && mysqlTypesWithCharacterSetAndCollate.includes(property.mysqlType) ) | ||
createQuery += ` CHARACTER SET ${property.charsetName}`; | ||
/** Properties with COLLATE */ | ||
if ( property.collationName && mysqlTypesWithCharacterSetAndCollate.includes(property.mysqlType) ) | ||
createQuery += ` COLLATE ${property.collationName}`; | ||
/** Properties with NULL */ | ||
if ( property.null ) | ||
createQuery += ` NULL`; | ||
else | ||
createQuery += ` NOT NULL`; | ||
/** Properties with DEFAULT */ | ||
if ( property.default ) | ||
createQuery += ` DEFAULT ${property.default}`; | ||
/** Properties with AUTO_INCREMENT */ | ||
if ( property.autoIncrement ) | ||
createQuery += ` AUTO_INCREMENT`; | ||
/** Properties with UNIQUE KEY */ | ||
if ( property.unique ) | ||
createQuery += ` UNIQUE`; | ||
/** Properties with PRIMARY KEY */ | ||
if ( property.primary ) | ||
createQuery += ` PRIMARY`; | ||
if ( property.unique || property.primary ) | ||
createQuery += ` KEY`; | ||
/** Properties with COMMENT */ | ||
if ( property.comment && typeof property.comment == 'string' ) | ||
createQuery += ` COMMENT '${property.comment.replace(`'`, ``)}'`; | ||
createQuery += `, `; | ||
}); | ||
}; | ||
/** Helper method that can be recursively called to add all indexes to the create query */ | ||
const addIndexesToCreateQuery = (obj) => { | ||
/** If this object extends another, recursively add indexes from the extended object */ | ||
if ( obj.extendsConfig ) | ||
addIndexesToCreateQuery(obj.extendsConfig); | ||
/** If there are any indexes defined */ | ||
if ( obj.indexes ) { | ||
/** Loop through each index */ | ||
obj.indexes.forEach((index) => { | ||
/** Convert the type to upper case for reliable string comparison */ | ||
index.type = index.type.toUpperCase(); | ||
/** If type is not defined, default to BTREE */ | ||
if ( typeof index.type !== 'string' ) | ||
index.type = 'BTREE'; | ||
/** Validate index settings */ | ||
if ( index.type != 'BTREE' && index.type != 'HASH' ) | ||
throw new Error(`Invalid index type '${index.type}'.`); | ||
else if ( index.visible && index.invisible ) | ||
throw new Error(`Index cannot have both VISIBLE and INVISIBLE options set.`); | ||
/** Add index name and type to query */ | ||
createQuery += `INDEX ${index.name} USING ${index.type} (`; | ||
/** Loop through each indexed column and append to query */ | ||
index.columns.forEach((column) => { | ||
createQuery += `${column}, `; | ||
}); | ||
/** Trim off extra ', ' from columns */ | ||
createQuery = createQuery.substr(0, createQuery.length - 2); | ||
/** Close column list */ | ||
createQuery += `)`; | ||
/** Indexes with KEY_BLOCK_SIZE */ | ||
if ( typeof index.keyBlockSize === 'number' ) | ||
createQuery += ` KEY_BLOCK_SIZE ${index.keyBlockSize}`; | ||
/** Indexes with WITH PARSER */ | ||
if ( typeof index.parserName === 'string' ) | ||
createQuery += ` WITH PARSER ${index.parserName}`; | ||
/** Indexes with COMMENT */ | ||
if ( typeof index.comment === 'string' ) | ||
createQuery += ` COMMENT '${index.comment.replace(`'`, ``)}'`; | ||
/** Indexes with VISIBLE */ | ||
if ( typeof index.visible === 'boolean' && index.visible ) | ||
createQuery += ` VISIBLE`; | ||
/** Indexes with INVISIBLE */ | ||
if ( typeof index.visible === 'boolean' && index.invisible ) | ||
createQuery += ` INVISIBLE`; | ||
createQuery += `, `; | ||
}); | ||
} | ||
}; | ||
/** Begin create table query */ | ||
let createQuery = `CREATE TABLE IF NOT EXISTS ${obj.tableName} (`; | ||
/** Call recursive methods to add properties and indexes to query */ | ||
addPropertiesToCreateQuery(obj); | ||
addIndexesToCreateQuery(obj); | ||
/** Trim extra ', ' from property and/or index list */ | ||
createQuery = createQuery.substr(0, createQuery.length - 2); | ||
/** Close property and/or index list */ | ||
createQuery += `)`; | ||
/** Await query execution and return result */ | ||
return await db.query(createQuery); | ||
}; | ||
/** | ||
* @signature createObject(obj) | ||
* @module ezobjects | ||
* @param obj Object Configuration object | ||
* @description Easy, automatic object creation from simple templates with strict typing | ||
*/ | ||
module.exports = (obj) => { | ||
module.exports.createObject = (obj) => { | ||
/** Create default transform function that doesn't change the input */ | ||
const defaultTransform = x => x; | ||
let parent; | ||
@@ -16,6 +211,6 @@ | ||
parent = global; | ||
/** Create new class on global scope */ | ||
parent[obj.name] = class extends (obj.extends || Object) { | ||
/** Constructor for new object. */ | ||
parent[obj.className] = class extends (obj.extends || Object) { | ||
/** Create constructor */ | ||
constructor(data = {}) { | ||
@@ -27,38 +222,45 @@ super(data); | ||
/** Initializer */ | ||
/** Create initializer */ | ||
init(data = {}) { | ||
/** If there is an 'init' function on super, call it */ | ||
if ( typeof super.init === 'function' ) | ||
super.init(data); | ||
/** If data is a string, assume it's JSON encoded and parse */ | ||
if ( typeof data == 'string' ) | ||
data = JSON.parse(data); | ||
/** Loop through each key/val pair in data */ | ||
Object.keys(data).forEach((key) => { | ||
/** If key begins with '_' */ | ||
if ( key.match(/^_/) ) { | ||
/** Create a new key with the '_' character stripped from the beginning */ | ||
Object.defineProperty(data, key.replace(/^_/, ''), Object.getOwnPropertyDescriptor(data, key)); | ||
/** Delete the old key that has '_' */ | ||
delete data[key]; | ||
} | ||
}); | ||
/** Loop through each property in the obj */ | ||
obj.properties.forEach((col) => { | ||
/** Initialize 'int' and 'float' types to zero */ | ||
if ( col.type == 'int' || col.type == 'float' ) | ||
this[col.name](data[col.name] || col.default || 0); | ||
obj.properties.forEach((property) => { | ||
/** Initialize 'number' types to zero */ | ||
if ( property.type == 'number' ) | ||
this[property.name](data[property.name] || property.default || 0); | ||
/** Initialize 'boolean' types to false */ | ||
else if ( col.type == 'boolean' ) | ||
this[col.name](data[col.name] || col.default || false); | ||
else if ( property.type == 'boolean' ) | ||
this[property.name](data[property.name] || property.default || false); | ||
/** Initialize 'string' types to empty */ | ||
else if ( col.type == 'string' ) | ||
this[col.name](data[col.name] || col.default || ''); | ||
else if ( property.type == 'string' ) | ||
this[property.name](data[property.name] || property.default || ''); | ||
/** Initialize 'Array' types to empty */ | ||
else if ( col.type == 'Array' ) | ||
this[col.name](data[col.name] || col.default || []); | ||
else if ( property.type == 'Array' ) | ||
this[property.name](data[property.name] || property.default || []); | ||
/** Initialize all other types to null */ | ||
else | ||
this[col.name](data[col.name] || col.default || null); | ||
this[property.name](data[property.name] || property.default || null); | ||
}); | ||
@@ -69,18 +271,34 @@ } | ||
/** Loop through each property in the obj */ | ||
obj.properties.forEach((col) => { | ||
/** For 'int' type properties */ | ||
if ( col.type == 'int' ) { | ||
obj.properties.forEach((property) => { | ||
/** If there is no getter transform, set to default */ | ||
if ( typeof property.getTransform !== 'function' ) | ||
property.getTransform = defaultTransform; | ||
/** If there is no setter transform, set to default */ | ||
if ( typeof property.setTransform !== 'function' ) | ||
property.setTransform = defaultTransform; | ||
/** If there is no save transform, set to default */ | ||
if ( typeof property.saveTransform !== 'function' ) | ||
property.saveTransform = defaultTransform; | ||
/** If there is no load transform, set to default */ | ||
if ( typeof property.loadTransform !== 'function' ) | ||
property.loadTransform = defaultTransform; | ||
/** For 'number' type properties */ | ||
if ( property.type == 'number' ) { | ||
/** Create class method on prototype */ | ||
parent[obj.name].prototype[col.name] = function (arg) { | ||
parent[obj.className].prototype[property.name] = function (arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this[`_${col.name}`]; | ||
return property.getTransform(this[`_${property.name}`]); | ||
/** Setter */ | ||
else if ( typeof arg == 'number' ) | ||
this[`_${col.name}`] = parseInt(arg); | ||
this[`_${property.name}`] = property.setTransform(arg); | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.${col.name}(${typeof arg}): Invalid signature.`); | ||
throw new TypeError(`${this.constructor.name}.${property.name}(${typeof arg}): Invalid signature.`); | ||
@@ -90,40 +308,19 @@ /** Return this object for set call chaining */ | ||
}; | ||
} | ||
} | ||
/** For 'float' type properties */ | ||
else if ( col.type == 'float' ) { | ||
/** Create class method on prototype */ | ||
parent[obj.name].prototype[col.name] = function (arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this[`_${col.name}`]; | ||
/** Setter */ | ||
else if ( typeof arg == 'number' ) | ||
this[`_${col.name}`] = parseFloat(arg); | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.${col.name}(${typeof arg}): Invalid signature.`); | ||
/** Return this object for set call chaining */ | ||
return this; | ||
}; | ||
} | ||
/** For 'boolean' type properties */ | ||
else if ( col.type == 'boolean' ) { | ||
else if ( property.type == 'boolean' ) { | ||
/** Create class method on prototype */ | ||
parent[obj.name].prototype[col.name] = function (arg) { | ||
parent[obj.className].prototype[property.name] = function (arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this[`_${col.name}`]; | ||
return property.getTransform(this[`_${property.name}`]); | ||
/** Setter */ | ||
else if ( typeof arg == 'boolean' ) | ||
this[`_${col.name}`] = arg; | ||
this[`_${property.name}`] = property.setTransform(arg); | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.${col.name}(${typeof arg}): Invalid signature.`); | ||
throw new TypeError(`${this.constructor.name}.${property.name}(${typeof arg}): Invalid signature.`); | ||
@@ -136,16 +333,16 @@ /** Return this object for set call chaining */ | ||
/** For 'string' type properties */ | ||
else if ( col.type == 'string' ) { | ||
else if ( property.type == 'string' ) { | ||
/** Create class method on prototype */ | ||
parent[obj.name].prototype[col.name] = function (arg) { | ||
parent[obj.className].prototype[property.name] = function (arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this[`_${col.name}`]; | ||
return property.getTransform(this[`_${property.name}`]); | ||
/** Setter */ | ||
else if ( typeof arg == 'string' ) | ||
this[`_${col.name}`] = arg; | ||
this[`_${property.name}`] = property.setTransform(arg); | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.${col.name}(${typeof arg}): Invalid signature.`); | ||
throw new TypeError(`${this.constructor.name}.${property.name}(${typeof arg}): Invalid signature.`); | ||
@@ -158,16 +355,16 @@ /** Return this object for set call chaining */ | ||
/** For 'Array' type properties */ | ||
else if ( col.type == 'Array' ) { | ||
else if ( property.type == 'Array' ) { | ||
/** Create class method on prototype */ | ||
parent[obj.name].prototype[col.name] = function (arg) { | ||
parent[obj.className].prototype[property.name] = function (arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this[`_${col.name}`]; | ||
return property.getTransform(this[`_${property.name}`]); | ||
/** Setter */ | ||
else if ( typeof arg == 'object' && arg.constructor.name == col.type ) | ||
this[`_${col.name}`] = arg; | ||
else if ( typeof arg == 'object' && arg.constructor.name == property.type ) | ||
this[`_${property.name}`] = property.setTransform(arg); | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.${col.name}(${typeof arg}): Invalid signature.`); | ||
throw new TypeError(`${this.constructor.name}.${property.name}(${typeof arg}): Invalid signature.`); | ||
@@ -182,14 +379,14 @@ /** Return this object for set call chaining */ | ||
/** Create class method on prototype */ | ||
parent[obj.name].prototype[col.name] = function (arg) { | ||
parent[obj.className].prototype[property.name] = function (arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this[`_${col.name}`]; | ||
return property.getTransform(this[`_${property.name}`]); | ||
/** Setter */ | ||
else if ( arg === null || ( typeof arg == 'object' && arg.constructor.name == col.type ) ) | ||
this[`_${col.name}`] = arg; | ||
else if ( arg === null || ( typeof arg == 'object' && arg.constructor.name == property.type ) ) | ||
this[`_${property.name}`] = property.setTransform(arg); | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.${col.name}(${typeof arg}): Invalid signature.`); | ||
throw new TypeError(`${this.constructor.name}.${property.name}(${typeof arg}): Invalid signature.`); | ||
@@ -200,2 +397,246 @@ /** Return this object for set call chaining */ | ||
} | ||
if ( typeof obj.tableName == 'string' ) { | ||
/** Create MySQL insert method on prototype */ | ||
parent[obj.className].prototype.insert = async function (db) { | ||
/** If the argument is a valid database, insert record into database and capture ID */ | ||
if ( typeof db == 'object' && db.constructor.name == 'MySQLConnection' ) { | ||
/** Create array for storing values to insert */ | ||
const params = []; | ||
/** Create helper method for recursively adding properties to params array */ | ||
const propertyValues = (obj) => { | ||
/** If this object extends another, recursively add properties from the extended object */ | ||
if ( obj.extendsConfig ) | ||
propertyValues(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Ignore ID since we'll get that from the insert */ | ||
if ( property.name == 'id' ) | ||
return; | ||
/** Add property to params array after performing the save transform */ | ||
params.push(property.saveTransform(this[property.name]())); | ||
}); | ||
}; | ||
/** Recursively add properties to params array */ | ||
propertyValues(obj); | ||
/** Begin INSERT query */ | ||
let query = `INSERT INTO ${obj.tableName} (`; | ||
/** Create helper method for recursively adding property names to query */ | ||
const propertyNames = (obj) => { | ||
/** If this object extends another, recursively add property names from the extended object */ | ||
if ( obj.extendsConfig ) | ||
propertyNames(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Ignore ID since we'll get that from the insert */ | ||
if ( property.name == 'id' ) | ||
return; | ||
/** Append property name to query */ | ||
query += `${property.name}, `; | ||
}); | ||
}; | ||
/** Add property names to query */ | ||
propertyNames(obj); | ||
/** Trim extra ', ' from property list */ | ||
query = query.substr(0, query.length - 2); | ||
/** Continue query */ | ||
query += `) VALUES (`; | ||
/** Create helper method for recursively adding property value placeholders to query */ | ||
const propertyPlaceholders = (obj) => { | ||
/** If this object extends another, recursively add property placeholders from the extended object */ | ||
if ( obj.extendsConfig ) | ||
propertyPlaceholders(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Ignore ID since we'll get that from the insert */ | ||
if ( property.name == 'id' ) | ||
return; | ||
/** Append placeholder to query */ | ||
query += `?, `; | ||
}); | ||
}; | ||
/** Add property placeholders to query */ | ||
propertyPlaceholders(obj); | ||
/** Trim extra ', ' from placeholder list */ | ||
query = query.substr(0, query.length - 2); | ||
/** Finish query */ | ||
query += `)`; | ||
/** Execute query to add record to database */ | ||
const result = await db.query(query, params); | ||
/** Store the resulting insert ID */ | ||
this.id(result.insertId); | ||
} | ||
/** Otherwise throw TypeError */ | ||
else { | ||
throw new TypeError(`${this.constructor.name}.insert(${typeof db}): Invalid signature.`); | ||
} | ||
/** Allow for call chaining */ | ||
return this; | ||
}; | ||
/** Create MySQL load method on prototype */ | ||
parent[obj.className].prototype.load = async function (arg1, arg2) { | ||
/** If the first argument is a valid database and the second is a number, load record from database by ID */ | ||
if ( typeof arg1 == 'object' && arg1.constructor.name == 'MySQLConnection' && typeof arg2 == 'number' ) { | ||
/** Begin SELECT query */ | ||
let query = `SELECT `; | ||
/** Create helper method for recursively adding property names to query */ | ||
const propertyNames = (obj) => { | ||
/** If this object extends another, recursively add property names from the extended object */ | ||
if ( obj.extendsConfig ) | ||
propertyNames(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Append property name to query */ | ||
query += `${property.name}, `; | ||
}); | ||
}; | ||
/** Add property names to query */ | ||
propertyNames(obj); | ||
/** Trim extra ', ' from property list */ | ||
query = query.substr(0, query.length - 2); | ||
/** Finish query */ | ||
query += ` FROM ${obj.tableName} `; | ||
query += `WHERE id = ?`; | ||
/** Execute query to load record properties from the database */ | ||
const result = await arg1.query(query, [arg2]); | ||
/** If a record with that ID doesn't exist, throw error */ | ||
if ( !result[0] ) | ||
throw new ReferenceError(`${this.constructor.name}.load(): Record ${arg2} in ${obj.tableName} does not exist.`); | ||
/** Create helper method for recursively loading property values into object */ | ||
const loadProperties = (obj) => { | ||
/** If this object extends another, recursively add extended property values into objecct */ | ||
if ( obj.extendsConfig ) | ||
loadProperties(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Append property in object */ | ||
this[property.name](property.loadTransform(result[0][property.name])); | ||
}); | ||
}; | ||
/** Store loaded record properties into object */ | ||
loadProperties(obj); | ||
} | ||
/** If the first argument is a MySQL RowDataPacket, load from row data */ | ||
else if ( typeof arg1 == 'object' && ( arg1.constructor.name == 'RowDataPacket' || arg1.constructor.name == 'Array' ) ) { | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Append property in object */ | ||
this[property.name](arg1[property.name]); | ||
}); | ||
} | ||
/** Otherwise throw TypeError */ | ||
else { | ||
throw new TypeError(`${this.constructor.name}.load(${typeof arg1}, ${typeof arg2}): Invalid signature.`); | ||
} | ||
/** Allow for call chaining */ | ||
return this; | ||
}; | ||
/** Create MySQL update method on prototype */ | ||
parent[obj.className].prototype.update = async function (db) { | ||
/** If the argument is a valid database, update database record */ | ||
if ( typeof db == 'object' && db.constructor.name == 'MySQLConnection' ) { | ||
/** Create array for storing values to update */ | ||
const params = []; | ||
/** Create helper method for recursively adding properties to params array */ | ||
const propertyValues = (obj) => { | ||
/** If this object extends another, recursively add properties from the extended object */ | ||
if ( obj.extendsConfig ) | ||
propertyValues(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Ignore ID since we will use that to locate the record, and will never update it */ | ||
if ( property.name == 'id' ) | ||
return; | ||
/** Add property to params array after performing the save transform */ | ||
params.push(property.saveTransform(this[property.name]())); | ||
}); | ||
}; | ||
/** Recursively add properties to params array */ | ||
propertyValues(obj); | ||
/** Add ID to params array at the end so we can locate the record to update */ | ||
params.push(this.id()); | ||
/** Begin UPDATE query */ | ||
let query = `UPDATE ${obj.tableName} SET `; | ||
/** Create helper method for recursively adding property updates to query */ | ||
const propertyUpdates = (obj) => { | ||
/** If this object extends another, recursively add properties from the extended object */ | ||
if ( obj.extendsConfig ) | ||
propertyUpdates(obj.extendsConfig); | ||
/** Loop through each property */ | ||
obj.properties.forEach((property) => { | ||
/** Ignore ID since we will use that to locate the record, and will never update it */ | ||
if ( property.name == 'id' ) | ||
return; | ||
/** Append property update to query */ | ||
query += `${property.name} = ?, `; | ||
}); | ||
}; | ||
/** Add property updates to query */ | ||
propertyUpdates(obj); | ||
/** Trim extra ', ' from property list */ | ||
query = query.substr(0, query.length - 2); | ||
/** Finish query */ | ||
query += ` WHERE id = ?`; | ||
/** Execute query to update record in database */ | ||
await db.query(query, params); | ||
} | ||
/** Otherwise throw TypeError */ | ||
else { | ||
throw new TypeError(`${this.constructor.name}.update(${typeof db}): Invalid signature.`); | ||
} | ||
/** Allow for call chaining */ | ||
return this; | ||
}; | ||
} | ||
}); | ||
@@ -207,3 +648,6 @@ | ||
*/ | ||
Object.defineProperty(parent[obj.name], 'name', { value: obj.name }); | ||
Object.defineProperty(parent[obj.className], 'name', { value: obj.className }); | ||
} | ||
/** Re-export MySQLConnection */ | ||
module.exports.MySQLConnection = mysqlConnection.MySQLConnection; |
{ | ||
"name": "ezobjects", | ||
"version": "1.1.5", | ||
"version": "2.0.0", | ||
"description": "Easy dynamic object generation with strict typing and set chaining", | ||
"main": "index.js", | ||
"scripts": { | ||
"start": "node example.js", | ||
"test": "eslint example.js index.js" | ||
"start": "node example-mysql.js", | ||
"test": "eslint example.js example-mysql.js index.js mysql-connection.js" | ||
}, | ||
@@ -24,3 +24,7 @@ "repository": { | ||
"author": "Rich Lowe", | ||
"license": "MIT" | ||
"license": "MIT", | ||
"dependencies": { | ||
"moment": "^2.22.1", | ||
"mysql": "^2.15.0" | ||
} | ||
} |
407
README.md
@@ -1,106 +0,175 @@ | ||
# EZ Objects v1.1.5 | ||
# EZ Objects v2.0.0 | ||
EZ Objects is a Node.js module (that can also be usefully browserify'd) that aims to save you lots of time | ||
writing the initializers, property getters, and property setters for your data model objects. The library | ||
takes the form of a single function that can be passed a generic object containing a few configuration keys, | ||
defined below, that will allow it to automatically generate a new ES6 class object with the following features: | ||
writing class objects. All you have to do is create simple configurations for each of your objects and then call | ||
the library function(s). Let's start by showing a basic example: | ||
Auto-initializes all properties (including parent object properties, if extended) | ||
* Default default values for different data types are listed further below | ||
* You can specify the default value for a property in the config when you create the EZ Object | ||
* You can pass a value that the property should have when instansiating the EZ Object | ||
* If the object has been JSON encoded and parsed, the properties will have the internal property | ||
underscores `_`, fortunately the module will strip the `_` from the object if you wish to initialize | ||
a new object using a JSON encoded/parsed version of the same object | ||
## Basic Example | ||
Automatically creates methods that perform getter/setter functionality with strict data typing | ||
* Methods use myMethod() for getter and myMethod(val) for setter | ||
* Methods throw TypeError if type does not match that specified | ||
* Methods return 'this' when setting so set calls can be chained | ||
```javascript | ||
const ezobjects = require('ezobjects'); | ||
See the examples below to witness them used in a variety of situations. | ||
/** Create a customized object on the global (node) or window (browser) namespace */ | ||
ezobjects.createObject({ | ||
className: 'DatabaseRecord', | ||
properties: [ | ||
{ name: 'id', type: 'number', setTransform: x => parseInt(x) } | ||
] | ||
}); | ||
## Status | ||
const record = new DatabaseRecord(); | ||
``` | ||
Fully operational! Please open an issue for any bug reports or feature requests. | ||
In this short snippet, we have effectively created a new class named `DatabaseRecord`. The class | ||
has a constructor, an initializer, and a getter/setter method for `id`. The constructor calls the | ||
initializer (named `init`), which sets the value of `id` to the default for type **number**, being | ||
zero in this case. You could also explicitly pass a default by adding a `default` key to the property, | ||
should you so desire. We've also added a transform function that will take any value passed to the `id` | ||
setter and apply parseInt() to it. | ||
## Principles of Operation | ||
## MySQL Example w/ Extended Object | ||
This module, when required, is a function that takes a single object argument. At present, that object can have the | ||
following keys: | ||
```javascript | ||
const ezobjects = require('ezobjects'); | ||
const fs = require('fs'); | ||
const moment = require('moment'); | ||
* name - A string containing the name of the desired class object (required) | ||
* extends - An object that you wish the class to extend from (optional, note this is the class itself, not the name) | ||
* properties - An array of properties for which the class will have getters/setters/initialization implemented (optional) | ||
Each property in the properties array is an object that can have the following keys: | ||
/** Load external MySQL configuration */ | ||
const configMySQL = JSON.parse(fs.readFileSync('mysql-config.json')); | ||
* name - The name of the property (required) | ||
* type - The type of the property (required, can be string, int, float, boolean, Array, or any other object name) | ||
* default - The default initialized value (optional) | ||
/** Connect to the MySQL database using our MySQL module async/await wrapper */ | ||
const db = new ezobjects.MySQLConnection(configMySQL); | ||
Default defaults are: | ||
/** | ||
* Configure a new EZ Object called DatabaseRecord with one 'id' property that | ||
* contains extended MySQL configuration settings. | ||
*/ | ||
const configDatabaseRecord = { | ||
className: 'DatabaseRecord', | ||
properties: [ | ||
{ | ||
name: 'id', | ||
type: 'number', | ||
mysqlType: 'int', | ||
autoIncrement: true, | ||
primary: true, | ||
setTransform: x => parseInt(x) | ||
} | ||
] | ||
}; | ||
* string - '' | ||
* int - 0 | ||
* float - 0 | ||
* boolean - false | ||
* Array - [] | ||
* Any others - null | ||
/** Create the DatabaseRecord object */ | ||
ezobjects.createObject(configDatabaseRecord); | ||
Note that the created objects are added to the global space, being `global` (node) or `window` (browser), though you'll | ||
have to browserify or equivalent to use in browser. Like normal classes, they can have other properties/methods added | ||
externally using the prototype, though note that if you want external prototype-added properties to be initialized, you'll | ||
have to rewrite the init() function manually. Alternatively, you can just extend the class and init the parent with | ||
`super`, see examples below. | ||
/** | ||
* Configure a new EZ Object called Person that extends from the DatabaseRecord | ||
* object and adds several additional properties and a MySQL index. | ||
*/ | ||
const configPerson = { | ||
tableName: 'people', | ||
className: 'Person', | ||
extends: DatabaseRecord, | ||
extendsConfig: configDatabaseRecord, | ||
properties: [ | ||
{ | ||
name: 'firstName', | ||
type: 'string', | ||
mysqlType: 'varchar', | ||
length: 20 | ||
}, | ||
{ | ||
name: 'lastName', | ||
type: 'string', | ||
mysqlType: 'varchar', | ||
length: 20 | ||
}, | ||
{ | ||
name: 'checkingBalance', | ||
type: 'number', | ||
mysqlType: 'double', | ||
setTransform: x => parseFloat(x) | ||
}, | ||
{ | ||
name: 'permissions', | ||
type: 'Array', | ||
mysqlType: 'text', | ||
saveTransform: x => x.join(','), | ||
loadTransform: x => x.split(',') | ||
}, | ||
{ | ||
name: 'favoriteDay', | ||
type: 'Date', | ||
mysqlType: 'datetime', | ||
saveTransform: x => moment(x).format('Y-MM-DD HH:mm:ss'), | ||
loadTransform: x => new Date(x) | ||
} | ||
], | ||
indexes: [ | ||
{ name: 'lastName', type: 'BTREE', columns: [ 'lastName' ] } | ||
] | ||
}; | ||
## Examples | ||
/** Create the Person object */ | ||
ezobjects.createObject(configPerson); | ||
#### Creating a class | ||
```javascript | ||
const ezobjects = require('ezobjects'); | ||
/** Create a customized object on the global (node) or window (browser) namespace */ | ||
ezobjects({ | ||
name: 'DatabaseRecord', | ||
properties: [ | ||
{ name: 'id', type: 'int' } | ||
] | ||
/** Create new person, initializing with object passed to constructor */ | ||
const person = new Person({ | ||
firstName: 'Rich', | ||
lastName: 'Lowe', | ||
checkingBalance: 4.32, | ||
permissions: [1, 3, 5], | ||
favoriteDay: new Date('01-01-2018') | ||
}); | ||
/** Example of the object newly instansiated */ | ||
const a = new DatabaseRecord(); | ||
console.log(a); | ||
/** Self-executing async wrapper so we can await results */ | ||
(async () => { | ||
/** Create table if it doesn't already exist */ | ||
await ezobjects.createTable(db, configPerson); | ||
/** Insert person into the database */ | ||
await person.insert(db); | ||
/** Log person (should have automatically incremented ID now) */ | ||
console.log(person); | ||
/** Close database connection */ | ||
db.close(); | ||
})(); | ||
``` | ||
#### Output | ||
### Expected Output | ||
``` | ||
DatabaseRecord { _id: 0 } | ||
Person { | ||
_id: 8, | ||
_firstName: 'Rich', | ||
_lastName: 'Lowe', | ||
_checkingBalance: 4.32, | ||
_permissions: [ 1, 3, 5 ], | ||
_favoriteDay: 2018-01-01T06:00:00.000Z } | ||
``` | ||
#### Creating a class that's extended from another class | ||
In this snippet, we've created two classes, DatabaseRecord and Person. Person extends DatabaseRecord and is also associated with | ||
a MySQL table called `people`. Each property is given a JavaScript `type` and a MySQL `mysqlType`. Additional MySQL property | ||
configuration options can be provided, which are outlined in more detail below. A BTREE index is also added on the lastName column | ||
for faster searching. While not required, the moment library is used to help translate date formats between MySQL and JavaScript. | ||
Since the Person class configuration provided a `tableName` property, it will automatically have additional methods created that are | ||
not present in a basic EZ Object. The additional methods are insert(db), load(db, id), and update(db). These methods can be used to | ||
insert the object properties as a new MySQL record, load a MySQL record into the object properties, or update an existing MySQL record | ||
with the object properties. Transforms can be used to adjust the values properties when they are get or set in the object, or when they | ||
are saved or loaded from the database. | ||
## Various Uses of EZ Objects | ||
### Constructor Default | ||
```javascript | ||
/** Create another customized object that extends the first one */ | ||
ezobjects({ | ||
name: 'Person', | ||
extends: DatabaseRecord, | ||
properties: [ | ||
{ name: 'firstName', type: 'string' }, | ||
{ name: 'lastName', type: 'string' }, | ||
{ name: 'checkingBalance', type: 'float' }, | ||
{ name: 'permissions', type: 'Array' }, | ||
{ name: 'favoriteDay', type: 'Date' } | ||
] | ||
}); | ||
const a = new Person(); | ||
/** Example of the extended object newly instansiated */ | ||
const b = new Person(); | ||
console.log(b); | ||
console.log(a); | ||
``` | ||
#### Output | ||
### Expected Output | ||
@@ -117,19 +186,18 @@ ``` | ||
#### Using an intializer object passed to constructor | ||
### Using Initializer Object | ||
```javascript | ||
/** Example of the extended object instansiated and initialized using object passed to constructor */ | ||
const c = new Person({ | ||
const b = new Person({ | ||
id: 1, | ||
firstName: 'Rich', | ||
lastName: 'Lowe', | ||
checkingBalance: 4.87, | ||
permissions: [1, 2, 3], | ||
checkingBalance: 4.32, | ||
permissions: [1, 3, 5], | ||
favoriteDay: new Date('01-01-2018') | ||
}); | ||
console.log(c); | ||
console.log(b); | ||
``` | ||
#### Output | ||
### Expected Output | ||
@@ -141,24 +209,23 @@ ``` | ||
_lastName: 'Lowe', | ||
_checkingBalance: 4.87, | ||
_permissions: [ 1, 2, 3 ], | ||
_checkingBalance: 4.32, | ||
_permissions: [ 1, 4, 5 ], | ||
_favoriteDay: 2018-01-01T06:00:00.000Z } | ||
``` | ||
#### Using the auto-created setters | ||
### Using Auto-generated Setters | ||
```javascript | ||
/** Example of the extended object instansiated, then loaded with data using setter methods */ | ||
const d = new Person(); | ||
const c = new Person(); | ||
d.id(2); | ||
d.firstName('Bert'); | ||
d.lastName('Reynolds'); | ||
d.checkingBalance(91425518.32); | ||
d.permissions([1, 4]); | ||
d.favoriteDay(new Date('06-01-2017')); | ||
c.id(2); | ||
c.firstName('Bert'); | ||
c.lastName('Reynolds'); | ||
c.checkingBalance(91425518.32); | ||
c.permissions([1, 4]); | ||
c.favoriteDay(new Date('06-01-2017')); | ||
console.log(d); | ||
console.log(c); | ||
``` | ||
#### Output | ||
### Expected Output | ||
@@ -175,15 +242,14 @@ ``` | ||
#### Using the auto-created getters | ||
### Using Auto-generated Getters | ||
```javascript | ||
/** Example of the extended object's properties being accessed using getter methods */ | ||
console.log(`ID: ${d.id()}`); | ||
console.log(`First Name: ${d.firstName()}`); | ||
console.log(`Last Name: ${d.lastName()}`); | ||
console.log(`Checking Balance: $${d.checkingBalance()}`); | ||
console.log(`Permissions: ${d.permissions().join(`, `)}`); | ||
console.log(`Favorite Day: ${d.favoriteDay().toString()}`); | ||
console.log(`ID: ${c.id()}`); | ||
console.log(`First Name: ${c.firstName()}`); | ||
console.log(`Last Name: ${c.lastName()}`); | ||
console.log(`Checking Balance: $${c.checkingBalance()}`); | ||
console.log(`Permissions: ${c.permissions().join(`, `)}`); | ||
console.log(`Favorite Day: ${c.favoriteDay().toString()}`); | ||
``` | ||
#### Output | ||
### Expected Output | ||
@@ -199,102 +265,53 @@ ``` | ||
#### Adding properties by using the class prototype | ||
## Module Specification | ||
```javascript | ||
/** Adding property to the generated object's prototype */ | ||
DatabaseRecord.prototype.table = function (arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this._table; | ||
/** Setter */ | ||
else if ( typeof arg == 'string' ) | ||
this._table = arg; | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.table(${typeof arg}): Invalid signature.`); | ||
/** Return this object for set call chaining */ | ||
return this; | ||
}; | ||
### The module has three exports: | ||
/** Yuck, now I have to manually override the init() call if I want it initialized */ | ||
DatabaseRecord.prototype.init = function (data = {}) { | ||
this.id(data.id || 0); | ||
this.table(data.table || ''); | ||
}; | ||
**ezobjects.createTable(db, obj)** | ||
* Creates a MySQL table corresponding to the configuration outlined in `obj`, if it doesn't already exist | ||
const e = new DatabaseRecord(); | ||
**ezobjects.createObject(obj)** | ||
* Creates an ES6 class corresponding to the configuration outlined in `obj`, with constructor/init/getters/setters, and insert/load/update if `tableName` is configured | ||
console.log(e); | ||
``` | ||
**ezobjects.MySQLConnection(config)** | ||
* A MySQL database connection wrapper that uses the standard mysql package and wraps it with async/await and transaction helpers | ||
#### Output | ||
### An object configuration can have the following: | ||
``` | ||
DatabaseRecord { _id: 0, _table: '' } | ||
``` | ||
* tableName - string - (optional) Provide if object should be linked with MySQL database table | ||
* className - string - (required) Name of the class | ||
* extends - object - (optional) The object that the new object should be extended from [required to extend object] | ||
* extendsConfig - object - (optional) The EZ Object configuration for the object that is being extended from [required to extend object] | ||
* properties - Array - (required) An array of properties that the object (and MySQL table, if applicable) should contain | ||
* indexes - Array - (optional) An array of indexes that should be created in the MySQL table, if applicable | ||
#### Adding capability other than properties by using the class prototype | ||
### A property configuration can have the following: | ||
```javascript | ||
/** Adding arbitrary capability other than property to the generated object's prototype */ | ||
DatabaseRecord.prototype.hello = function () { | ||
return "Hello, World!"; | ||
}; | ||
* name - string - (required) Name of the property, must conform to both JavaScript and MySQL rules | ||
* type - string - (required) JavaScript data type for the property | ||
* mysqlType - string - (optional) MySQL data type for the property [required for MySQL table association] | ||
* length - number - (optional) MySQL data length for the property [required for MySQL table association on some data types like VARCHAR] | ||
* decimals - number - (optional) Number of decimals that should be provided for certain data types when SELECT'ed from the MySQL table | ||
* primary - boolean - (optional) Indicates the property is a PRIMARY KEY in the MySQL table [required for MySQL table association on at least one property in the table] | ||
* unique - boolean - (optional) Indicates the property is a UNIQUE KEY in the MySQL table | ||
* null - boolean - (optional) Indicates the property can be NULL [default is properties must be NOT NULL] | ||
* default - mixed - (optional) Sets the default value for the property in the MySQL table, assuming its of the correct type | ||
* unsigned - boolean - (optional) Indicates the property should be unsigned in the MySQL table | ||
* zerofill - boolean - (optional) Indicates the property should be zero-filled in the MySQL table | ||
* comment - string - (optional) Indicates the property should note the provided comment in the MySQL table | ||
* charsetName - string - (optional) Indicates the property should use the provided charset in the MySQL table | ||
* collationName - string - (optional) Indicates the property should use the provided collation in the MySQL table | ||
* autoIncrement - boolean - (optional) Indicates the property should be auto-incremented in the MySQL table | ||
* getTransform - function - (optional) Function that transforms and returns the property value prior to getting | ||
* setTransform - function - (optional) Function that transforms and returns the property value prior to setting | ||
* saveTransform - function - (optional) Function that transforms and returns the property value prior to saving in the database | ||
* loadTransform - function - (optional) Function that transforms and returns the property value after loading from the database | ||
const f = new DatabaseRecord(); | ||
### An index configuration can have the following (for MySQL table association only): | ||
console.log(f.hello()); | ||
``` | ||
#### Output | ||
``` | ||
Hello, World! | ||
``` | ||
#### Adding properties and/or capability by extending the class | ||
```javascript | ||
/** These objects can be extended instead to accomplish the same things if preferred */ | ||
class DatabaseRecord2 extends DatabaseRecord { | ||
constructor(data = {}) { | ||
super(data); | ||
} | ||
init(data = {}) { | ||
super.init(data); | ||
this.test('Test'); | ||
} | ||
test(arg) { | ||
/** Getter */ | ||
if ( arg === undefined ) | ||
return this._test; | ||
/** Setter */ | ||
else if ( typeof arg == 'string' ) | ||
this._test = arg.toString(); | ||
/** Handle type errors */ | ||
else | ||
throw new TypeError(`${this.constructor.name}.test(${typeof arg}): Invalid signature.`); | ||
/** Return this object for set call chaining */ | ||
return this; | ||
} | ||
} | ||
const g = new DatabaseRecord2(); | ||
console.log(g); | ||
console.log(g.hello()); | ||
``` | ||
#### Output | ||
``` | ||
DatabaseRecord2 { _id: 0, _table: '', _test: 'Test' } | ||
Hello, World! | ||
``` | ||
* name - string - (required) Name of the index, can be arbitrary, but must be unique and not PRIMARY | ||
* type - string - (optional) Index type, can be BTREE or HASH, defaults to BTREE | ||
* keyBlockSize - number - (optional) Indicates the index should use the provided key block size | ||
* withParser - string - (optional) Indicates the index should use the provided parser | ||
* visible - boolean - (optional) Indicates the index should be visible | ||
* invisible - boolean - (optional) Indicates the index should be invisible |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
53009
7
946
314
2
1
1
+ Addedmoment@^2.22.1
+ Addedmysql@^2.15.0
+ Addedbignumber.js@9.0.0(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@1.0.0(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedmysql@2.18.1(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedreadable-stream@2.3.7(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedsqlstring@2.3.1(transitive)
+ Addedstring_decoder@1.1.1(transitive)
+ Addedutil-deprecate@1.0.2(transitive)