UNPKG

pure-orm

Version:

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

347 lines (320 loc) 11.8 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 ------------------------------------*/ /* ------------------------------------------------------------------------*/ const getSqlInsertParts = ( model: IModel ): { columns: string; values: Array<string>; valuesVar: Array<string> } => { const columns = orm .getEntityByModel(model) .columnNames.filter( (column: string, index: number) => model[ orm.getEntityByModel(model).propertyNames[ index ] as keyof typeof model ] !== void 0 ) .map((col: string) => `"${col}"`) .join(', '); const values = orm .getEntityByModel(model) .propertyNames.map( (property: string) => model[property as keyof typeof model] ) .filter((value: any) => value !== void 0); const valuesVar = values.map( (value: any, index: number) => `$${index + 1}` ); return { columns, values, valuesVar }; }; const getSqlUpdateParts = ( model: IModel, on = 'id' ): { clause: string; idVar: string; values: Array<string> } => { const clauseArray = orm .getEntityByModel(model) .columnNames.filter( (sqlColumn: string, index: number) => model[ orm.getEntityByModel(model).propertyNames[ index ] as keyof typeof model ] !== void 0 ) .map( (sqlColumn: string, index: number) => `"${sqlColumn}" = $${index + 1}` ); const clause = clauseArray.join(', '); const idVar = `$${clauseArray.length + 1}`; const _values = orm .getEntityByModel(model) .propertyNames.map( (property: string) => model[property as keyof typeof model] ) .filter((value: any) => value !== void 0); const values = [..._values, model[on as keyof typeof model]]; return { clause, idVar, values }; }; const getMatchingParts = ( model: IModel ): { whereClause: string; values: Array<string> } => { const whereClause = orm .getEntityByModel(model) .propertyNames.map((property: string, index: number) => model[property as keyof typeof model] != null ? `"${orm.getEntityByModel(model).tableName}"."${ orm.getEntityByModel(model).columnNames[index] }"` : null ) .filter((x: string | null) => x != null) .map((x: string | null, i: number) => `${x} = $${i + 1}`) .join(' AND '); const values = orm .getEntityByModel(model) .propertyNames.map((property: string) => model[property as keyof typeof model] != null ? model[property as keyof typeof model] : null ) .filter((x: any) => x != null); 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 whereClause = orm .getEntityByModel(model) .propertyNames.map((property: string, index: number) => model[property as keyof typeof model] != null ? `"${orm.getEntityByModel(model).tableName}"."${ orm.getEntityByModel(model).columnNames[index] }"` : null ) .filter((x: string | null) => x != null) .map((x: string | null, i: number) => `${x} = $(${i + 1})`) .join(' AND '); const values = orm .getEntityByModel(model) .propertyNames.map((property: string) => model[property as keyof typeof model] != null ? model[property as keyof typeof model] : null ) .filter((x: any) => x != null) .reduce( (accum: any, val: any, index: number) => Object.assign({}, accum, { [index + 1]: val }), {} ); return { whereClause, values }; }; const getNewWith = (model: IModel, sqlColumns: any, values: any): IModel => { const Constructor = model.constructor as any; const modelKeys = sqlColumns.map( (key: string) => orm.getEntityByModel(model).propertyNames[ orm.getEntityByModel(model).columnNames.indexOf(key) ] ); const modelData = modelKeys.reduce( (data: any, key: string, index: number) => { data[key] = values[index]; return data; }, {} ); return new Constructor(modelData); }; const getValueBySqlColumn = (model: IModel, sqlColumn: string): string => { return model[ orm.getEntityByModel(model).propertyNames[ orm.getEntityByModel(model).columnNames.indexOf(sqlColumn) ] as keyof typeof model ]; }; const getSqlColumnForPropertyName = ( model: IModel, propertyName: string ): string => { return orm.getEntityByModel(model).columnNames[ orm.getEntityByModel(model).propertyNames.indexOf(propertyName) ]; }; /* ------------------------------------------------------------------------*/ /* Built-in basic CRUD functions ------------------------------------------*/ /* ------------------------------------------------------------------------*/ // Standard create const create = <T extends IModel>(model: T): Promise<T> => { const { columns, values, valuesVar } = getSqlInsertParts(model); const query = ` INSERT INTO "${orm.getEntityByModel(model).tableName}" ( ${columns} ) VALUES ( ${valuesVar} ) RETURNING ${orm.getEntityByModel(model).selectColumnsClause}; `; return orm.one<T>(query, values); }; // Standard update const update = <T extends IModel>( model: T, { on = 'id' } = {} ): Promise<T> => { const { clause, idVar, values } = getSqlUpdateParts(model, on); const query = ` UPDATE "${orm.getEntityByModel(model).tableName}" SET ${clause} WHERE "${ orm.getEntityByModel(model).tableName }".${getSqlColumnForPropertyName(model, on)} = ${idVar} RETURNING ${orm.getEntityByModel(model).selectColumnsClause}; `; return orm.one<T>(query, values); }; // Standard delete const _delete = <T extends IModel>(model: T): Promise<void> => { const id = (model as any).id; const query = ` DELETE FROM "${orm.getEntityByModel(model).tableName}" WHERE "${orm.getEntityByModel(model).tableName}".id = $(id) `; return orm.none(query, { id }); }; const deleteMatching = <T extends IModel>(model: T): Promise<void> => { const { whereClause, values } = getMatchingParts(model); const query = ` DELETE FROM "${orm.getEntityByModel(model).tableName}" WHERE ${whereClause}; `; return orm.none(query, values); }; const getMatching = <T extends IModel>(model: T): Promise<T> => { const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${orm.getEntityByModel(model).selectColumnsClause} FROM "${orm.getEntityByModel(model).tableName}" WHERE ${whereClause}; `; return orm.one<T>(query, values); }; const getOneOrNoneMatching = <T extends IModel>( model: T ): Promise<T | void> => { const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${orm.getEntityByModel(model).selectColumnsClause} FROM "${orm.getEntityByModel(model).tableName}" WHERE ${whereClause}; `; return orm.oneOrNone<T>(query, values); }; const getAnyMatching = <T extends ICollection<IModel>>( model: IModel ): Promise<T | void> => { const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${orm.getEntityByModel(model).selectColumnsClause} FROM "${orm.getEntityByModel(model).tableName}" WHERE ${whereClause}; `; return orm.any<T>(query, values); }; const getAllMatching = <T extends ICollection<IModel>>( model: IModel ): Promise<T> => { const { whereClause, values } = getMatchingParts(model); const query = ` SELECT ${orm.getEntityByModel(model).selectColumnsClause} FROM "${orm.getEntityByModel(model).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 }); };