UNPKG

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
"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']; 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;