UNPKG

sql-toolkit

Version:

A SQL Toolkit based on pure business objects passed to and from stateful data access objects

331 lines (304 loc) 10.5 kB
const camelCase = require('camelcase'); module.exports = ({ getBusinessObjects }) => class Base { constructor(props) { Object.assign(this, props); } static primaryKey() { const primaryKey = this.sqlColumnsData.filter(x => x.primaryKey); return primaryKey.length > 0 ? primaryKey : ['id']; } static get columns() { return this.sqlColumnsData.map( x => x.property || camelCase(x.column || x) ); } static get sqlColumns() { return this.sqlColumnsData.map(x => x.column || x); } static get references() { return this.sqlColumnsData .filter(x => x.references) .reduce( (accum, item) => Object.assign({}, accum, { [item.property || camelCase(item.column || item)]: item.references }), {} ); } static get displayName() { return camelCase(this.tableName); } static getPrefixedColumnNames() { return this.sqlColumns.map(col => `${this.tableName}#${col}`); } static getSQLSelectClause() { return this.getPrefixedColumnNames() .map( (prefixed, index) => `"${this.tableName}".${this.sqlColumns[index]} as "${prefixed}"` ) .join(', '); } /* * Make objects (based on special table#column names) from flat database * return value. */ static objectifyDatabaseResult(result) { return Object.keys(result).reduce((obj, text) => { const tableName = text.indexOf('#') > -1 ? text.split('#')[0] : this.tableName; const column = text.indexOf('#') > -1 ? text.split('#')[1] : text; obj[tableName] = obj[tableName] || {}; obj[tableName][column] = result[text]; return obj; }, {}); } static mapToBos(objectified) { return Object.keys(objectified).map(tableName => { const Bo = getBusinessObjects().find(bo => bo.tableName === tableName); if (!Bo) { throw Error(`No business object with table name "${tableName}"`); } const propified = Object.keys(objectified[tableName]).reduce( (obj, column) => { let propertyName = Bo.columns[Bo.sqlColumns.indexOf(column)]; if (!propertyName) { if (column.startsWith('meta_')) { propertyName = camelCase(column); } else { throw Error( `No property name for "${column}" in business object "${ Bo.displayName }". Non-spec'd columns must begin with "meta_".` ); } } obj[propertyName] = objectified[tableName][column]; return obj; }, {} ); return new Bo(propified); }); } /* * Clump array of flat objects into groups based on id of root * In: * [ * [Article {id: 32}, ArticleTag {id: 54}] * [Article {id: 32}, ArticleTag {id: 55}] * [Article {id: 33}, ArticleTag {id: 56}] * ] * Out: * [ * [ * [Article {id: 32}, ArticleTag {id: 54}] * [Article {id: 32}, ArticleTag {id: 55}] * ] * [ * [Article {id: 33}, ArticleTag {id: 56}] * ] * ] */ static clumpIntoGroups(processed) { const clumps = processed.reduce((accum, item) => { const id = this.primaryKey() .map(key => item.find(x => x.constructor === this)[key]) .join('@'); if (accum.has(id)) { accum.set(id, [...accum.get(id), item]); } else { accum.set(id, [item]); } return accum; }, new Map()); return [...clumps.values()]; } /* * In: * [ * [Article {id: 32}, ArticleTag {id: 54}] * [Article {id: 32}, ArticleTag {id: 55}] * ] * Out: * Article {id: 32, ArticleTags articleTags: [ArticleTag {id: 54}, ArticleTag {id: 55}] */ static nestClump(clump) { clump = clump.map(x => Object.values(x)); // clump wasn't actually what I have documented const root = clump[0][0]; clump = clump.map(row => row.filter((item, index) => index !== 0)); const built = { [root.constructor.displayName]: root }; let nodes = [root]; // Wowzer is this both CPU and Memory inefficient clump.forEach(array => { array.forEach(bo => { const nodePointingToIt = nodes.find(x => { const index = Object.values(x.constructor.references).indexOf( bo.constructor ); if (index === -1) { return false; } const property = Object.keys(x.constructor.references)[index]; return x[property] === bo.id; }); const nodeItPointsTo = nodes.find(x => { const index = Object.values(bo.constructor.references).indexOf( x.constructor ); if (index === -1) { return false; } const property = Object.keys(bo.constructor.references)[index]; return bo[property] === x.id; }); if (!(nodePointingToIt || nodeItPointsTo)) { if (!bo.getId()) { // If the join is fruitless; todo: add a test for this path return; } throw Error( `Could not find how this BO fits: ${JSON.stringify(bo)}` ); } if (nodePointingToIt) { nodePointingToIt[bo.constructor.displayName] = bo; } else { let collection = nodeItPointsTo[bo.BoCollection.displayName]; if (collection) { collection.models.push(bo); } else { nodeItPointsTo[bo.BoCollection.displayName] = new bo.BoCollection( { models: [bo] } ); } } nodes = [bo, ...nodes]; }); }); return built; } static createFromDatabase(_result) { const result = Array.isArray(_result) ? _result : [_result]; const objectified = result.map(this.objectifyDatabaseResult.bind(this)); const boified = objectified.map(this.mapToBos.bind(this)); const clumps = this.clumpIntoGroups(boified); const nested = clumps.map(this.nestClump.bind(this)); const models = nested.map(n => Object.values(n)[0]); return new new this().BoCollection({ models }); } static createOneFromDatabase(_result) { const collection = this.createFromDatabase(_result); if (collection.models.length > 1) { throw Error('Got more than one.'); } else if (collection.models.length === 0) { throw Error('Did not get one.'); } return collection.models[0]; } static createOneOrNoneFromDatabase(_result) { const collection = this.createFromDatabase(_result); if (collection.models.length > 1) { throw Error('Got more than one.'); } return collection.models[0]; } static createManyFromDatabase(_result) { const collection = this.createFromDatabase(_result); if (collection.models.length === 0) { throw Error('Did not get at least one.'); } return collection; } getSqlInsertParts() { const columns = this.constructor.sqlColumns .filter( (column, index) => this[this.constructor.columns[index]] != null ) .join(', '); const values = this.constructor.columns .map(column => this[column]) .filter(value => value != null); const valuesVar = values.map((value, index) => `$${index + 1}`); return { columns, values, valuesVar }; } getSqlUpdateParts() { const clauseArray = this.constructor.sqlColumns .filter( (sqlColumn, index) => this[this.constructor.columns[index]] != null ) .map((sqlColumn, index) => `${sqlColumn} = $${index + 1}`); const clause = clauseArray.join(', '); const idVar = `$${clauseArray.length + 1}`; const _values = this.constructor.columns .map(column => this[column]) .filter(value => value != null); const values = [..._values, this.id]; return { clause, idVar, values }; } getMatchingParts() { const whereClause = this.constructor.columns .map((col, index) => this[col] != null ? `"${this.constructor.tableName}".${ this.constructor.sqlColumns[index] }` : null ) .filter(x => x != null) .map((x, i) => `${x} = $${i + 1}`) .join(' AND '); const values = this.constructor.columns .map(col => (this[col] != null ? this[col] : null)) .filter(x => x != null); return { whereClause, values }; } // This one returns an object, which allows it to be more versatile. // Todo: make this one even better and use it instead of the one above. getMatchingPartsObject() { const whereClause = this.constructor.columns .map((col, index) => this[col] != null ? `"${this.constructor.tableName}".${ this.constructor.sqlColumns[index] }` : null ) .filter(x => x != null) .map((x, i) => `${x} = $(${i + 1})`) .join(' AND '); const values = this.constructor.columns .map(col => (this[col] != null ? this[col] : null)) .filter(x => x != null) .reduce( (accum, val, index) => Object.assign({}, accum, { [index + 1]: val }), {} ); return { whereClause, values }; } getNewWith(sqlColumns, values) { const Constructor = this.constructor; const boKeys = sqlColumns.map( key => Constructor.columns[Constructor.sqlColumns.indexOf(key)] ); const boData = boKeys.reduce((data, key, index) => { data[key] = values[index]; return data; }, {}); return new Constructor(boData); } getValueBySqlColumn(sqlColumn) { return this[ this.constructor.columns[this.constructor.sqlColumns.indexOf(sqlColumn)] ]; } // Returns unique identifier of bo (the values of the primary keys) getId() { return this.constructor .primaryKey() .map(key => this[key]) .join(''); } };