UNPKG

pure-orm

Version:

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

334 lines (333 loc) 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createCore = void 0; const camelcase_1 = __importDefault(require("camelcase")); const createCore = ({ entities: externalEntities }) => { const entities = externalEntities.map((d) => { const tableName = d.tableName; const displayName = d.displayName || (0, camelcase_1.default)(d.tableName); const collectionDisplayName = d.collectionDisplayName || `${displayName}s`; const columns = (typeof d.columns === 'function' ? d.columns() : d.columns).map((d) => { if (typeof d === 'string') { return { column: d, property: (0, camelcase_1.default)(d), primaryKey: false }; } return Object.assign({ column: d.column, property: d.property || (0, camelcase_1.default)(d.column), primaryKey: d.primaryKey || false }, (d.references ? { references: d.references } : {})); }); const propertyNames = columns.map((x) => x.property); const columnNames = columns.map((x) => x.column); const prefixedColumnNames = columnNames.map((col) => `${tableName}#${col}`); const Model = d.Model; const Collection = d.Collection; const pkColumnsData = columns.filter((x) => x.primaryKey); const _primaryKeys = pkColumnsData.map((x) => x.column); const primaryKeys = _primaryKeys.length > 0 ? _primaryKeys : ['id']; // Returns unique identifier of model (the values of the primary keys) const getPkId = (model) => { return primaryKeys .map((key) => model[key]) .join(''); }; const references = columns .filter((x) => x.references) .reduce((accum, item) => Object.assign({}, accum, { [item.property]: item.references }), {}); const selectColumnsClause = prefixedColumnNames .map((prefixed, index) => `"${tableName}".${columnNames[index]} as "${prefixed}"`) .join(', '); return { tableName, displayName, collectionDisplayName, columns, propertyNames, Model, Collection, columnNames, prefixedColumnNames, primaryKeys, references, selectColumnsClause, getPkId }; }); const tableNameToEntityMap = entities.reduce((map, entity) => { map.set(entity.tableName, entity); return map; }, new Map()); const getEntityByTableName = (tableName) => { const entity = tableNameToEntityMap.get(tableName); if (!entity) { throw new Error(`Could not find entity for table ${tableName}`); } return entity; }; const modelToEntityMap = entities.reduce((map, entity) => { map.set(entity.Model, entity); return map; }, new Map()); const getEntityByModelClass = (Model) => { const entity = modelToEntityMap.get(Model); if (!entity) { throw new Error(`Could not find entity for class ${Model}`); } return entity; }; const getEntityByModel = (model) => { return getEntityByModelClass(model.constructor); }; /* * 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}] */ const nestClump = (clump) => { clump = clump.map((x) => Object.values(x)); const root = clump[0][0]; clump = clump.map((row) => row.filter((item, index) => index !== 0)); const built = { [getEntityByModel(root).displayName]: root }; let nodes = [root]; // Wowzer is this both CPU and Memory inefficient clump.forEach((array) => { array.forEach((_model) => { const nodeAlreadySeen = nodes.find((x) => x.constructor.name === _model.constructor.name && getEntityByModel(x).getPkId(x) === getEntityByModel(_model).getPkId(_model)); const model = nodeAlreadySeen || _model; const isNodeAlreadySeen = !!nodeAlreadySeen; const nodePointingToIt = nodes.find((node) => { const indexes = Object.values(getEntityByModel(node).references) .map((x, i) => x === model.constructor ? i : null) .filter((x, i) => x != null); if (!indexes.length) { return false; } for (const index of indexes) { const property = Object.keys(getEntityByModel(node).references)[index]; if (node[property] === model.id) { return true; } } return false; }); // For first obj type which is has an instance in nodes array, // get its index in nodes array const indexOfOldestParent = array.reduce((answer, obj) => { if (answer != null) { return answer; } const index = nodes.findIndex((n) => n.constructor === obj.constructor); if (index !== -1) { return index; } return null; }, null) || 0; const parentHeirarchy = [ root, ...nodes.slice(0, indexOfOldestParent + 1).reverse() ]; const nodeItPointsTo = parentHeirarchy.find((parent) => { const indexes = Object.values(getEntityByModel(model).references) .map((x, i) => x === parent.constructor ? i : null) .filter((x, i) => x != null); if (!indexes.length) { return false; } for (const index of indexes) { const property = Object.keys(getEntityByModel(model).references)[index]; if (model[property] === parent.id) { return true; } } return false; }); if (isNodeAlreadySeen) { if (nodeItPointsTo && !nodePointingToIt) { nodes = [model, ...nodes]; return; } // If the nodePointingToIt (eg, parcel_event) is part of an // existing collection on this node (eg, parcel) which is a // nodeAlreadySeen, early return so we don't create it (parcel) on // the nodePointingToIt (parcel_event), since it (parcel) has been // shown to be the parent (of parcel_events). if (nodePointingToIt) { const ec = model[getEntityByModel(nodePointingToIt) .collectionDisplayName]; if (ec && ec.models.find((m) => m === nodePointingToIt)) { nodes = [model, ...nodes]; return; } } } if (nodePointingToIt) { nodePointingToIt[getEntityByModel(model).displayName] = model; } else if (nodeItPointsTo) { let collection = nodeItPointsTo[getEntityByModel(model).collectionDisplayName]; if (collection) { collection.models.push(model); } else { const Collection = getEntityByModel(model).Collection; nodeItPointsTo[getEntityByModel(model).collectionDisplayName] = new Collection({ models: [model] }); } } else { if (!getEntityByModel(model).getPkId(model)) { // If the join is fruitless; todo: add a test for this path return; } throw Error(`Could not find how this BO fits: ${JSON.stringify(model)} ${getEntityByModel(model).tableName}`); } nodes = [model, ...nodes]; }); }); return built; }; /* * 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}] * ] * ] */ const clumpIntoGroups = (processed) => { const root = processed[0][0]; const rootBo = root.constructor; const clumps = processed.reduce((accum, item) => { const id = getEntityByModel(root) .primaryKeys.map((key) => { var _a; return (_a = item.find((x) => x.constructor === rootBo)) === null || _a === void 0 ? void 0 : _a[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()]; }; const mapToBos = (objectified) => { return Object.keys(objectified).map((tableName) => { const entity = getEntityByTableName(tableName); const propified = Object.keys(objectified[tableName]).reduce((obj, column) => { let propertyName = entity.propertyNames[entity.columnNames.indexOf(column)]; if (!propertyName) { if (column.startsWith('meta_')) { propertyName = (0, camelcase_1.default)(column); } else { throw Error(`No property name for "${column}" in business object "${entity.displayName}". Non-spec'd columns must begin with "meta_".`); } } obj[propertyName] = objectified[tableName][column]; return obj; }, {}); return new entity.Model(propified); }); }; /* * Make objects (based on special table#column names) from flat database * return value. */ const objectifyDatabaseResult = (result) => { return Object.keys(result).reduce((obj, text) => { const tableName = text.split('#')[0]; const column = text.split('#')[1]; if (!tableName || !column) { throw new Error('Column names must be namespaced to table'); } obj[tableName] = obj[tableName] || {}; obj[tableName][column] = result[text]; return obj; }, {}); }; const createFromDatabase = (rows) => { const result = Array.isArray(rows) ? rows : [rows]; const objectified = result.map(objectifyDatabaseResult); const boified = objectified.map(mapToBos); const clumps = clumpIntoGroups(boified); const nested = clumps.map(nestClump); const models = nested.map((n) => Object.values(n)[0]); const Collection = getEntityByModel(models[0]).Collection; return new Collection({ models }); }; const createAnyFromDatabase = (rows, rootKey) => { if (!rows || !rows.length) { const Collection = typeof rootKey === 'string' ? getEntityByTableName(rootKey).Collection : getEntityByModelClass(rootKey).Collection; return new Collection({ models: [] }); } return createFromDatabase(rows); }; const createOneFromDatabase = (rows) => { if (!rows || !rows.length) { throw Error('Did not get one.'); } const collection = createFromDatabase(rows); if (!collection || !collection.models || collection.models.length === 0) { throw Error('Did not get one.'); } else if (collection.models.length > 1) { throw Error('Got more than one.'); } return collection.models[0]; }; const createOneOrNoneFromDatabase = (rows) => { if (!rows || !rows.length) { return void 0; } return createOneFromDatabase(rows); }; const createManyFromDatabase = (rows) => { if (!rows || !rows.length) { throw Error('Did not get at least one.'); } return createFromDatabase(rows); }; return { getEntityByModel, getEntityByTableName, createFromDatabase, createAnyFromDatabase, createOneFromDatabase, createOneOrNoneFromDatabase, createManyFromDatabase, tables: entities.reduce((accum, data) => { accum[data.displayName] = { columns: data.selectColumnsClause }; return accum; }, {}) }; }; exports.createCore = createCore;