pure-orm
Version:
A SQL Toolkit based on pure business objects passed to and from stateful data access objects
411 lines (410 loc) • 17.2 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'];
const getPkId = (model) => {
let id = '';
for (let i = 0; i < primaryKeys.length; i++) {
const part = model[primaryKeys[i]];
if (part !== void 0 && part !== null) {
id += String(part);
}
}
return id;
};
const references = {};
const referencesEntries = [];
for (const col of columns) {
if (col.references) {
references[col.property] = col.references;
referencesEntries.push({
property: col.property,
ModelClass: col.references
});
}
}
const columnToPropertyMap = new Map();
const propertyToColumnMap = new Map();
for (let i = 0; i < columnNames.length; i++) {
columnToPropertyMap.set(columnNames[i], propertyNames[i]);
propertyToColumnMap.set(propertyNames[i], columnNames[i]);
}
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,
columnToPropertyMap,
propertyToColumnMap,
referencesEntries
};
});
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);
};
const entityReferencePlans = new Map();
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
const plans = new Array(entity.referencesEntries.length);
for (let j = 0; j < entity.referencesEntries.length; j++) {
const ref = entity.referencesEntries[j];
plans[j] = {
property: ref.property,
targetEntity: getEntityByModelClass(ref.ModelClass)
};
}
entityReferencePlans.set(entity, plans);
}
const getPkIdFromRow = (row, primaryKeyRowKeys) => {
let id = '';
for (let i = 0; i < primaryKeyRowKeys.length; i++) {
const part = row[primaryKeyRowKeys[i]];
if (part !== void 0 && part !== null) {
id += String(part);
}
}
return id;
};
const buildEntityRowPlans = (sampleRow) => {
const plansByTable = new Map();
const tableOrder = [];
for (const text in sampleRow) {
if (!Object.prototype.hasOwnProperty.call(sampleRow, text)) {
continue;
}
const hashIndex = text.indexOf('#');
if (hashIndex === -1) {
throw new Error('Column names must be namespaced to table');
}
const tableName = text.substring(0, hashIndex);
const column = text.substring(hashIndex + 1);
let plan = plansByTable.get(tableName);
if (!plan) {
const entity = getEntityByTableName(tableName);
const primaryKeyRowKeys = entity.primaryKeys.map((pk) => `${tableName}#${pk}`);
plan = {
entity,
columnPlans: [],
primaryKeyRowKeys
};
plansByTable.set(tableName, plan);
tableOrder.push(tableName);
}
let propertyName = plan.entity.columnToPropertyMap.get(column);
if (!propertyName) {
if (column.startsWith('meta_')) {
propertyName = (0, camelcase_1.default)(column);
}
else {
throw Error(`No property name for "${column}" in business object "${plan.entity.displayName}". Non-spec'd columns must begin with "meta_".`);
}
}
plan.columnPlans.push({
rowKey: text,
propertyName
});
}
const orderedPlans = new Array(tableOrder.length);
for (let i = 0; i < tableOrder.length; i++) {
orderedPlans[i] = plansByTable.get(tableOrder[i]);
}
return orderedPlans;
};
const materializeModelsFromRow = (row, entityRowPlans, rootScopedModelsByEntity, rowModels, rowModelPkIds, rowCreatedWithPkIndexes) => {
rowCreatedWithPkIndexes.length = 0;
for (let i = 0; i < entityRowPlans.length; i++) {
const plan = entityRowPlans[i];
const pkId = getPkIdFromRow(row, plan.primaryKeyRowKeys);
rowModelPkIds[i] = pkId;
if (pkId) {
let modelsForEntity = rootScopedModelsByEntity.get(plan.entity);
if (!modelsForEntity) {
modelsForEntity = new Map();
rootScopedModelsByEntity.set(plan.entity, modelsForEntity);
}
else {
const existing = modelsForEntity.get(pkId);
if (existing) {
rowModels[i] = existing;
continue;
}
}
}
else if (i !== 0) {
// No primary key means this is typically an outer-joined null row.
// Skip model construction for non-root entities since it cannot link.
rowModels[i] = void 0;
continue;
}
const props = {};
for (let j = 0; j < plan.columnPlans.length; j++) {
const columnPlan = plan.columnPlans[j];
props[columnPlan.propertyName] = row[columnPlan.rowKey];
}
const model = new plan.entity.Model(props);
if (pkId) {
// modelsForEntity is guaranteed to be initialized above for pk rows.
rootScopedModelsByEntity.get(plan.entity).set(pkId, model);
rowCreatedWithPkIndexes.push(i);
}
rowModels[i] = model;
}
return rowModels[0];
};
const getRootScopeKey = (row, rootEntity, rootPrimaryKeys) => {
let rootScopeKey = '';
for (let i = 0; i < rootPrimaryKeys.length; i++) {
if (i > 0) {
rootScopeKey += '@';
}
const value = row[`${rootEntity.tableName}#${rootPrimaryKeys[i]}`];
rootScopeKey += value === void 0 || value === null ? '' : String(value);
}
return rootScopeKey;
};
const ensureRootScopeState = (rootScopeKey, rootScopeStateByKey) => {
let state = rootScopeStateByKey.get(rootScopeKey);
if (!state) {
state = {
modelsByEntity: new Map()
};
rootScopeStateByKey.set(rootScopeKey, state);
}
return state;
};
const ensureCollectionMembership = (rootScopeState) => {
if (!rootScopeState.collectionMembership) {
rootScopeState.collectionMembership = new WeakMap();
}
return rootScopeState.collectionMembership;
};
const linkSourceToTarget = ({ sourceEntity, sourceModel, sourceModelPkId, targetEntity, targetModel, collectionMembership }) => {
sourceModel[targetEntity.displayName] =
targetModel;
const collectionKey = sourceEntity.collectionDisplayName;
let collection = targetModel[collectionKey];
if (!collection) {
const Collection = sourceEntity.Collection;
const createdCollection = new Collection({ models: [] });
// Keep ORM back-reference collections out of default JSON serialization.
Object.defineProperty(targetModel, collectionKey, {
value: createdCollection,
writable: true,
configurable: true,
enumerable: false
});
collection = createdCollection;
}
let byCollection = collectionMembership.get(targetModel);
if (!byCollection) {
byCollection = new Map();
collectionMembership.set(targetModel, byCollection);
}
let memberIds = byCollection.get(sourceEntity);
if (!memberIds) {
memberIds = new Set();
byCollection.set(sourceEntity, memberIds);
}
if (!memberIds.has(sourceModelPkId)) {
collection.models.push(sourceModel);
memberIds.add(sourceModelPkId);
}
};
/*
* createFromDatabase architecture:
* 1) Compile row plans once (column -> property mapping per entity/table).
* 2) Materialize models per row with scoped de-duplication by root scope key.
* 3) Index models by root scope + entity + entity primary key.
* 4) Link refs incrementally as new models appear.
* 5) Return root models in first-seen root scope order.
*/
const createFromDatabase = (rows) => {
var _a;
const result = Array.isArray(rows) ? rows : [rows];
const len = result.length;
const entityRowPlans = buildEntityRowPlans(result[0]);
const selectedEntities = new Set();
for (let i = 0; i < entityRowPlans.length; i++) {
selectedEntities.add(entityRowPlans[i].entity);
}
const applicableRefPlans = new Map();
for (let i = 0; i < entityRowPlans.length; i++) {
const entity = entityRowPlans[i].entity;
const refs = entityReferencePlans.get(entity) || [];
const filteredRefs = refs.filter((ref) => selectedEntities.has(ref.targetEntity));
applicableRefPlans.set(entity, filteredRefs);
}
const rootEntity = entityRowPlans[0].entity;
const rootPrimaryKeys = rootEntity.primaryKeys;
const rootScopeOrder = [];
const rootModelsByScopeKey = new Map();
const rootScopeStateByKey = new Map();
let currentRootScopeKey = void 0;
let currentRootScopeState = void 0;
const rowModels = new Array(entityRowPlans.length);
const rowModelPkIds = new Array(entityRowPlans.length);
const rowCreatedWithPkIndexes = [];
// Phase 1: materialize and index model instances by root scope + entity.
for (let i = 0; i < len; i++) {
const row = result[i];
const rootScopeKey = getRootScopeKey(row, rootEntity, rootPrimaryKeys);
let rootScopeState = currentRootScopeState;
if (!rootScopeState || rootScopeKey !== currentRootScopeKey) {
rootScopeState = ensureRootScopeState(rootScopeKey, rootScopeStateByKey);
currentRootScopeKey = rootScopeKey;
currentRootScopeState = rootScopeState;
}
const rootModel = materializeModelsFromRow(row, entityRowPlans, rootScopeState.modelsByEntity, rowModels, rowModelPkIds, rowCreatedWithPkIndexes);
if (!rootModelsByScopeKey.has(rootScopeKey)) {
rootScopeOrder.push(rootScopeKey);
rootModelsByScopeKey.set(rootScopeKey, rootModel);
}
for (let c = 0; c < rowCreatedWithPkIndexes.length; c++) {
const j = rowCreatedWithPkIndexes[c];
const sourceModel = rowModels[j];
const sourceModelPkId = rowModelPkIds[j];
const sourceEntity = entityRowPlans[j].entity;
const refs = applicableRefPlans.get(sourceEntity);
if (!refs || refs.length === 0) {
continue;
}
for (let r = 0; r < refs.length; r++) {
const ref = refs[r];
const refId = sourceModel[ref.property];
if (refId == null) {
continue;
}
const targetPkId = String(refId);
const target = (_a = rootScopeState.modelsByEntity
.get(ref.targetEntity)) === null || _a === void 0 ? void 0 : _a.get(targetPkId);
if (target) {
linkSourceToTarget({
sourceEntity,
sourceModel,
sourceModelPkId,
targetEntity: ref.targetEntity,
targetModel: target,
collectionMembership: ensureCollectionMembership(rootScopeState)
});
}
}
}
}
const models = new Array(rootScopeOrder.length);
for (let i = 0; i < rootScopeOrder.length; i++) {
models[i] = rootModelsByScopeKey.get(rootScopeOrder[i]);
}
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;