trilogy
Version:
TypeScript SQLite layer with support for both native C++ & pure JavaScript drivers.
291 lines (290 loc) • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Cast = exports.toReturnType = exports.toInputType = exports.toKnexMethod = exports.normalizeSchema = exports.castValue = exports.createTimestampTrigger = exports.createTrigger = exports.TriggerEvent = exports.toKnexSchema = void 0;
const constants_1 = require("./constants");
const helpers_1 = require("./helpers");
const util_1 = require("./util");
const types = require("./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
`;
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 (util_1.isFunction(descriptor) || !util_1.isObject(descriptor)) {
continue;
}
const props = types.ColumnDescriptor.check(descriptor);
if ('nullable' in props) {
if ('notNullable' in props) {
util_1.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 constants_1.IgnorableProps)
continue;
if (property in constants_1.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);
}
}
};
}
exports.toKnexSchema = toKnexSchema;
function createIndices(table, value) {
if (util_1.isString(value)) {
table.index([value]);
}
else if (Array.isArray(value)) {
if (value.every(util_1.isString)) {
table.index(value);
}
value.forEach(columns => table.index(columns));
}
else if (util_1.isObject(value)) {
for (const [indexName, columns] of Object.entries(value)) {
table.index(util_1.toArray(columns), indexName);
}
}
}
var TriggerEvent;
(function (TriggerEvent) {
TriggerEvent["Insert"] = "insert";
TriggerEvent["Update"] = "update";
TriggerEvent["Delete"] = "delete";
})(TriggerEvent = exports.TriggerEvent || (exports.TriggerEvent = {}));
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 => helpers_1.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 => helpers_1.runQuery(model.ctx, query, queryOptions)));
};
return [query, cleanup];
}
exports.createTrigger = createTrigger;
async function createTimestampTrigger(model, column = 'updated_at') {
const { key, hasIncrements } = helpers_1.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 helpers_1.runQuery(model.ctx, query, { model, internal: true });
}
exports.createTimestampTrigger = createTimestampTrigger;
function castValue(value) {
const type = typeof value;
if (type === 'number' || type === 'string') {
return value;
}
if (type === 'boolean')
return Number(value);
if (Array.isArray(value) || util_1.isObject(value)) {
return JSON.stringify(value);
}
return value;
}
exports.castValue = castValue;
function normalizeSchema(schema, options) {
const keys = Object.keys(schema);
util_1.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;
}
exports.normalizeSchema = normalizeSchema;
function getDataType(property) {
let type = property;
if (util_1.isFunction(property)) {
type = property.name;
}
else if (util_1.isObject(property)) {
type = util_1.isFunction(property.type)
? property.type.name
: property.type;
}
if (util_1.isString(type)) {
const lower = type.toLowerCase();
if (!(lower in constants_1.ColumnTypes)) {
return 'string';
}
return lower;
}
util_1.invariant(false, `column type must be of type string`);
}
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:
util_1.invariant(false, `invalid column type definition: ${type}`);
}
}
exports.toKnexMethod = toKnexMethod;
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:
util_1.invariant(false, `invalid type on insert to database: ${type}`);
}
}
exports.toInputType = toInputType;
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:
util_1.invariant(false, `invalid type returned from database: ${type}`);
}
}
exports.toReturnType = toReturnType;
class Cast {
constructor(model) {
this.model = model;
}
toDefinition(object, options) {
if (helpers_1.isWhereTuple(object)) {
const clone = object.slice();
const valueIndex = clone.length - 1;
clone[valueIndex] =
this.toColumnDefinition(clone[0], clone[valueIndex], options);
return clone;
}
if (helpers_1.isWhereMultiple(object)) {
return object.map(clause => this.toDefinition(clause, options));
}
if (util_1.isObject(object)) {
return util_1.mapObj(object, (value, column) => {
return this.toColumnDefinition(column, value, options);
});
}
util_1.invariant(false, `invalid input type: '${typeof object}'`);
}
fromDefinition(object, options) {
return util_1.mapObj(object, (value, column) => {
return this.fromColumnDefinition(column, value, options);
});
}
toColumnDefinition(column, value, options = { raw: false }) {
const definition = this.model.schema[column];
util_1.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 && util_1.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 && util_1.isFunction(definition.get)) {
return definition.get(cast);
}
return cast;
}
}
exports.Cast = Cast;