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
JavaScript
;
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;