UNPKG

trilogy

Version:

TypeScript SQLite layer with support for both native C++ & pure JavaScript drivers.

279 lines (278 loc) 10.1 kB
import { ColumnTypes, IgnorableProps, KnexNoArgs } from './constants'; import { isWhereMultiple, isWhereTuple, findKey, runQuery } from './helpers'; import { invariant, isFunction, isObject, isString, mapObj, toArray } from './util'; import * as types from './types'; const TimestampTriggerTemplate = ` create trigger if not exists :name: after update on :modelName: begin update :modelName: set :column: = \`current_timestamp\` where :key: = \`old\`.:key:; end `; export function toKnexSchema(model, options) { return (table) => { // every property of `model.schema` is a column for (const [name, descriptor] of Object.entries(model.schema)) { // these timestamp fields are handled as part of the model options // processing below, ignore them here so we don't duplicate the fields if (options.timestamps && (name === 'created_at' || name === 'updated_at')) { continue; } // each column's value is either its type or a descriptor const type = getDataType(descriptor); const partial = table[toKnexMethod(type)](name); if (isFunction(descriptor) || !isObject(descriptor)) { continue; } const props = types.ColumnDescriptor.check(descriptor); if ('nullable' in props) { if ('notNullable' in props) { invariant(false, `can't set both 'nullable' & 'notNullable' - they work inversely`); } props.notNullable = !props.nullable; } for (const [property, value] of Object.entries(props)) { if (property in IgnorableProps) continue; if (property in KnexNoArgs) { value && partial[property](); } else { partial[property](value); } } } for (const [key, value] of Object.entries(options)) { if (key === 'timestamps' && options.timestamps) { table.timestamps(false, true); // tslint:disable-next-line:no-floating-promises createTimestampTrigger(model); } else if (key === 'index' && value) { createIndices(table, value); } else { table[key](value); } } }; } function createIndices(table, value) { if (isString(value)) { table.index([value]); } else if (Array.isArray(value)) { if (value.every(isString)) { table.index(value); } value.forEach(columns => table.index(columns)); } else if (isObject(value)) { for (const [indexName, columns] of Object.entries(value)) { table.index(toArray(columns), indexName); } } } export var TriggerEvent; (function (TriggerEvent) { TriggerEvent["Insert"] = "insert"; TriggerEvent["Update"] = "update"; TriggerEvent["Delete"] = "delete"; })(TriggerEvent || (TriggerEvent = {})); export async function createTrigger(model, event) { const keys = Object.keys(model.schema); const tableName = `${model.name}_returning_temp`; const triggerName = `on_${event}_${model.name}`; const keyBindings = keys.map(() => '??').join(', '); const fieldPrefix = event === TriggerEvent.Delete ? 'old.' : 'new.'; const fieldReferences = keys.map(k => fieldPrefix + k); const queryOptions = { model, internal: true }; const tempTable = `create table if not exists ?? (${keyBindings})`; const tempTrigger = ` create trigger if not exists ?? after ${event} on ?? begin insert into ?? select ${keyBindings}; end `; await Promise.all([ model.ctx.knex.raw(tempTable, [tableName, ...keys]), model.ctx.knex.raw(tempTrigger, [triggerName, model.name, tableName, ...fieldReferences]) ].map(query => runQuery(model.ctx, query, queryOptions))); let query = model.ctx.knex(tableName); if (event === TriggerEvent.Insert) { // tslint:disable-next-line:semicolon ; query = query.first(); } const cleanup = () => { return Promise.all([ model.ctx.knex.raw(`drop table if exists ??`, tableName), model.ctx.knex.raw(`drop trigger if exists ??`, triggerName) ].map(query => runQuery(model.ctx, query, queryOptions))); }; return [query, cleanup]; } export async function createTimestampTrigger(model, column = 'updated_at') { const { key, hasIncrements } = findKey(model.schema); if (!key && !hasIncrements) { // there's no way to uniquely identify the updated record return; } const query = model.ctx.knex.raw(TimestampTriggerTemplate, { name: `on_update_${model.name}_timestamp`, modelName: model.name, column, key }); return runQuery(model.ctx, query, { model, internal: true }); } export function castValue(value) { const type = typeof value; if (type === 'number' || type === 'string') { return value; } if (type === 'boolean') return Number(value); if (Array.isArray(value) || isObject(value)) { return JSON.stringify(value); } return value; } export function normalizeSchema(schema, options) { const keys = Object.keys(schema); invariant(keys.length > 0, 'model schemas cannot be empty'); const result = {}; for (const key of keys) { const descriptor = schema[key]; const type = typeof descriptor; result[key] = type === 'function' || type === 'string' ? { type: descriptor } : descriptor; } if (options.timestamps) { // tslint:disable-next-line:semicolon ; result.created_at = { type: Date }; result.updated_at = { type: Date }; } return result; } function getDataType(property) { let type = property; if (isFunction(property)) { type = property.name; } else if (isObject(property)) { type = isFunction(property.type) ? property.type.name : property.type; } if (isString(type)) { const lower = type.toLowerCase(); if (!(lower in ColumnTypes)) { return 'string'; } return lower; } invariant(false, `column type must be of type string`); } export function toKnexMethod(type) { switch (type) { case 'string': case 'array': case 'object': case 'json': return 'text'; case 'number': case 'boolean': return 'integer'; case 'date': return 'dateTime'; case 'increments': return 'increments'; default: invariant(false, `invalid column type definition: ${type}`); } } export function toInputType(type, value) { switch (type) { case 'string': return String(value); case 'array': case 'object': case 'json': return JSON.stringify(value); case 'number': case 'boolean': case 'increments': return Number(value); case 'date': return value.toISOString(); default: invariant(false, `invalid type on insert to database: ${type}`); } } export function toReturnType(type, value) { switch (type) { case 'string': return String(value); case 'array': case 'object': case 'json': return JSON.parse(value); case 'number': case 'increments': return Number(value); case 'boolean': return Boolean(value); case 'date': return new Date(value); default: invariant(false, `invalid type returned from database: ${type}`); } } export class Cast { constructor(model) { this.model = model; } toDefinition(object, options) { if (isWhereTuple(object)) { const clone = object.slice(); const valueIndex = clone.length - 1; clone[valueIndex] = this.toColumnDefinition(clone[0], clone[valueIndex], options); return clone; } if (isWhereMultiple(object)) { return object.map(clause => this.toDefinition(clause, options)); } if (isObject(object)) { return mapObj(object, (value, column) => { return this.toColumnDefinition(column, value, options); }); } invariant(false, `invalid input type: '${typeof object}'`); } fromDefinition(object, options) { return mapObj(object, (value, column) => { return this.fromColumnDefinition(column, value, options); }); } toColumnDefinition(column, value, options = { raw: false }) { const definition = this.model.schema[column]; invariant(!(definition.notNullable && value == null), `${this.model.name}.${column} is not nullable but received nil`); const type = getDataType(definition); const cast = value !== null ? toInputType(type, value) : value; if (!options.raw && isFunction(definition.set)) { return castValue(definition.set(cast)); } return cast; } fromColumnDefinition(column, value, options = { raw: false }) { const definition = this.model.schema[column]; const type = getDataType(definition); const cast = value !== null ? toReturnType(type, value) : value; if (!options.raw && isFunction(definition.get)) { return definition.get(cast); } return cast; } }