UNPKG

pure-orm

Version:

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

368 lines (339 loc) 12.5 kB
import { createCore, IModel, ICollection, IEntities } from './core'; import { ICoreIntegratedDriver } from './driver-integrations/index'; import { createForPGP } from './driver-integrations/pgp'; export interface ICreateOptions { entities: IEntities<IModel>; db: any; logError?: (err: Error) => never; } export interface IPureORM extends ICoreIntegratedDriver { /* ------------------------------------------------------------------------*/ /* Built-in basic CRUD functions ------------------------------------------*/ /* ------------------------------------------------------------------------*/ /* These are just provided because they are so common and straight-forward. * While the goal of this library is foster writing SQL in your data access * layer (which returns pure business objects) some CRUD operations are so * common they are included in the ORM. Feel free to completely disregard * if you want to write these in your data access layer yourself. */ getMatching: <T extends IModel>(model: T) => Promise<T>; getOneOrNoneMatching: <T extends IModel>(model: T) => Promise<T | void>; getAnyMatching: <T extends ICollection<IModel>>( model: IModel ) => Promise<T | void>; getAllMatching: <T extends ICollection<IModel>>(model: IModel) => Promise<T>; create: <T extends IModel>(model: T) => Promise<T>; update: <T extends IModel>(model: T, options?: { on: string }) => Promise<T>; delete: <T extends IModel>(model: T) => Promise<void>; deleteMatching: <T extends IModel>(model: T) => Promise<void>; /* ------------------------------------------------------------------------*/ /* Helper Utility Functions -----------------------------------------------*/ /* ------------------------------------------------------------------------*/ getSqlInsertParts: (model: IModel) => { columns: string; values: Array<string>; valuesVar: Array<string>; }; getSqlUpdateParts: ( model: IModel, on?: string ) => { clause: string; idVar: string; values: Array<string> }; getMatchingParts: (model: IModel) => { whereClause: string; values: Array<string>; }; getMatchingPartsObject: (model: IModel) => { whereClause: string; values: Array<string>; }; getNewWith: (model: IModel, sqlColumns: any, values: any) => IModel; getValueBySqlColumn: (model: IModel, sqlColumn: string) => string; getSqlColumnForPropertyName: (model: IModel, propertyName: string) => string; } export const create = ({ entities: externalEntities, db, logError }: ICreateOptions): IPureORM => { const core = createCore({ entities: externalEntities }); let orm: ICoreIntegratedDriver; if (db.$config.pgp) { orm = createForPGP({ core, db, logError }); } else { throw new Error( `You're database driver is not yet supported. You can make a PR to add it, or use the \`createCore\` export which doesn't try to abstract over the database driver, and instead you pass the results of the database driver queries to it.` ); } /* ------------------------------------------------------------------------*/ /* Helper Utilities for CRUD functions ------------------------------------*/ /* ------------------------------------------------------------------------*/ interface IOrmHelperPlan { quotedColumns: Array<string>; updateClausePrefixes: Array<string>; wherePositionalPrefixes: Array<string>; whereNamedPrefixes: Array<string>; } const helperPlanByEntity = new Map<any, IOrmHelperPlan>(); const getHelperPlan = (entity: any): IOrmHelperPlan => { let plan = helperPlanByEntity.get(entity); if (!plan) { const quotedColumns = new Array(entity.columnNames.length); const updateClausePrefixes = new Array(entity.columnNames.length); const wherePositionalPrefixes = new Array(entity.columnNames.length); const whereNamedPrefixes = new Array(entity.columnNames.length); for (let i = 0; i < entity.columnNames.length; i++) { const column = entity.columnNames[i]; quotedColumns[i] = `"${column}"`; updateClausePrefixes[i] = `"${column}" = $`; wherePositionalPrefixes[i] = `"${entity.tableName}"."${column}" = $`; whereNamedPrefixes[i] = `"${entity.tableName}"."${column}" = $(`; } plan = { quotedColumns, updateClausePrefixes, wherePositionalPrefixes, whereNamedPrefixes }; helperPlanByEntity.set(entity, plan); } return plan; }; const getSqlInsertParts = ( model: IModel ): { columns: string; values: Array<string>; valuesVar: Array<string> } => { const entity = orm.getEntityByModel(model); const { columnNames, propertyNames } = entity; const helperPlan = getHelperPlan(entity); let columns = ''; const values: Array<any> = []; const valuesVar: Array<string> = []; let paramIndex = 1; for (let i = 0; i < columnNames.length; i++) { const val = model[propertyNames[i] as keyof typeof model]; if (val !== void 0) { if (columns) { columns += ', '; } columns += helperPlan.quotedColumns[i]; values.push(val); valuesVar.push(`$${paramIndex}`); paramIndex++; } } return { columns, values, valuesVar }; }; const getSqlUpdateParts = ( model: IModel, on = 'id' ): { clause: string; idVar: string; values: Array<string> } => { const entity = orm.getEntityByModel(model); const { columnNames, propertyNames } = entity; const helperPlan = getHelperPlan(entity); let clause = ''; const values: Array<any> = []; let paramIndex = 1; for (let i = 0; i < columnNames.length; i++) { const val = model[propertyNames[i] as keyof typeof model]; if (val !== void 0) { if (clause) { clause += ', '; } clause += helperPlan.updateClausePrefixes[i] + paramIndex; values.push(val); paramIndex++; } } const idVar = `$${paramIndex}`; values.push(model[on as keyof typeof model]); return { clause, idVar, values }; }; const getMatchingParts = ( model: IModel ): { whereClause: string; values: Array<string> } => { const entity = orm.getEntityByModel(model); const { propertyNames, columnNames } = entity; const helperPlan = getHelperPlan(entity); const values: Array<any> = []; let paramIndex = 1; let whereClause = ''; for (let i = 0; i < propertyNames.length; i++) { const val = model[propertyNames[i] as keyof typeof model]; if (val != null) { if (whereClause) { whereClause += ' AND '; } whereClause += helperPlan.wherePositionalPrefixes[i] + paramIndex; values.push(val); paramIndex++; } } return { whereClause, values }; }; // This one returns an object, which allows it to be more versatile. // To-do: make this one even better and use it instead of the one above. const getMatchingPartsObject = ( model: IModel ): { whereClause: string; values: Array<string> } => { const entity = orm.getEntityByModel(model); const { propertyNames, columnNames } = entity; const helperPlan = getHelperPlan(entity); const values: any = {}; let paramIndex = 1; let whereClause = ''; for (let i = 0; i < propertyNames.length; i++) { const val = model[propertyNames[i] as keyof typeof model]; if (val != null) { if (whereClause) { whereClause += ' AND '; } whereClause += helperPlan.whereNamedPrefixes[i] + paramIndex + ')'; values[paramIndex] = val; paramIndex++; } } return { whereClause, values }; }; const getNewWith = (model: IModel, sqlColumns: any, values: any): IModel => { const Constructor = model.constructor as any; const entity = orm.getEntityByModel(model); const modelData: any = {}; for (let i = 0; i < sqlColumns.length; i++) { const propertyName = entity.columnToPropertyMap.get(sqlColumns[i]); if (propertyName) { modelData[propertyName] = values[i]; } } return new Constructor(modelData); }; const getValueBySqlColumn = (model: IModel, sqlColumn: string): string => { const entity = orm.getEntityByModel(model); const propertyName = entity.columnToPropertyMap.get(sqlColumn); return propertyName ? model[propertyName as keyof typeof model] : (undefined as any); }; const getSqlColumnForPropertyName = ( model: IModel, propertyName: string ): string => { const entity = orm.getEntityByModel(model); const column = entity.propertyToColumnMap.get(propertyName); return column as string; }; /* ------------------------------------------------------------------------*/ /* Built-in basic CRUD functions ------------------------------------------*/ /* ------------------------------------------------------------------------*/ // Standard create const create = <T extends IModel>(model: T): Promise<T> => { const entity = orm.getEntityByModel(model); const { columns, values, valuesVar } = getSqlInsertParts(model); const query = ` INSERT INTO "${entity.tableName}" ( ${columns} ) VALUES ( ${valuesVar} ) RETURNING ${entity.selectColumnsClause}; `; return orm.one<T>(query, values); }; // Standard update const update = <T extends IModel>( model: T, { on = 'id' } = {} ): Promise<T> => { const entity = orm.getEntityByModel(model); const { clause, idVar, values } = getSqlUpdateParts(model, on); const query = ` UPDATE "${entity.tableName}" SET ${clause} WHERE "${entity.tableName}".${getSqlColumnForPropertyName( model, on )} = ${idVar} RETURNING ${entity.selectColumnsClause}; `; return orm.one<T>(query, values); }; // Standard delete const _delete = <T extends IModel>(model: T): Promise<void> => { const entity = orm.getEntityByModel(model); const id = (model as any).id; const query = ` DELETE FROM "${entity.tableName}" WHERE "${entity.tableName}".id = $(id) `; return orm.none(query, { id }); }; const deleteMatching = <T extends IModel>(model: T): Promise<void> => { const entity = orm.getEntityByModel(model); const { whereClause, values } = getMatchingParts(model); const query = ` DELETE FROM "${entity.tableName}" WHERE ${whereClause}; `; return orm.none(query, values); }; const getMatching = <T extends IModel>(model: T): Promise<T> => { const entity = orm.getEntityByModel(model); const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${entity.selectColumnsClause} FROM "${entity.tableName}" WHERE ${whereClause}; `; return orm.one<T>(query, values); }; const getOneOrNoneMatching = <T extends IModel>( model: T ): Promise<T | void> => { const entity = orm.getEntityByModel(model); const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${entity.selectColumnsClause} FROM "${entity.tableName}" WHERE ${whereClause}; `; return orm.oneOrNone<T>(query, values); }; const getAnyMatching = <T extends ICollection<IModel>>( model: IModel ): Promise<T | void> => { const entity = orm.getEntityByModel(model); const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${entity.selectColumnsClause} FROM "${entity.tableName}" WHERE ${whereClause}; `; return orm.any<T>(query, values); }; const getAllMatching = <T extends ICollection<IModel>>( model: IModel ): Promise<T> => { const entity = orm.getEntityByModel(model); const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${entity.selectColumnsClause} FROM "${entity.tableName}" WHERE ${whereClause}; `; return orm.many<T>(query, values); }; return Object.assign({}, orm, { // Built-in basic CRUD functions create, update, delete: _delete, deleteMatching, getMatching, getOneOrNoneMatching, getAnyMatching, getAllMatching, // Helper Utility functions getSqlInsertParts, getSqlUpdateParts, getMatchingParts, getMatchingPartsObject, getNewWith, getValueBySqlColumn, getSqlColumnForPropertyName }); };