UNPKG

trilogy

Version:

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

591 lines (590 loc) 23.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.connect = exports.Trilogy = void 0; const tslib_1 = require("tslib"); const path_1 = require("path"); const fs_1 = require("fs"); const knex = require("knex"); const model_1 = require("./model"); const helpers_1 = require("./helpers"); const schema_helpers_1 = require("./schema-helpers"); const sqljs_handler_1 = require("./sqljs-handler"); const util_1 = require("./util"); const types = require("./types"); const ensureExists = (atPath) => { try { fs_1.closeSync(fs_1.openSync(atPath, 'wx')); } catch (_a) { } }; const initOptions = (path, options) => (Object.assign(Object.assign({ client: 'sqlite3', dir: process.cwd() }, types.TrilogyOptions.check(options)), { connection: { filename: path } })); /** * Initialize a new datastore instance, creating a SQLite database file at * the provided path if it does not yet exist, or reading it if it does. * * It's recommended to use the {@link connect} function to create instances * rather than instantiating this class directly. * * @remarks * If path is exactly `':memory:'`, no file will be created and a memory-only * store will be used. This doesn't persist any of the data. */ class Trilogy { /** * @param path File path or `':memory:'` for in-memory storage * @param options Configuration for this trilogy instance */ constructor(path, options = {}) { util_1.invariant(path, 'trilogy constructor must be provided a file path'); const obj = this.options = initOptions(path, options); if (path === ':memory:') { obj.connection.filename = path; } else { obj.connection.filename = path_1.resolve(obj.dir, path); // ensure the directory exists util_1.makeDirPath(path_1.dirname(obj.connection.filename)); } this.isNative = obj.client === 'sqlite3'; if (path !== ':memory:') { ensureExists(obj.connection.filename); } const config = { client: 'sqlite3', useNullAsDefault: true }; if (this.isNative) { this.knex = knex(Object.assign(Object.assign({}, config), { connection: obj.connection })); } else { this.knex = knex(config); this.pool = sqljs_handler_1.pureConnect(this); } this._definitions = new Map(); } /** * Array of all model names defined on the instance. */ get models() { return [...this._definitions.keys()]; } /** * Define a new model with the provided schema, or return the existing * model if one is already defined with the given name. * * @param name Name of the model * @param schema Object defining the schema of the model * @param options Configuration for this model instance */ async model(name, schema, options = {}) { if (this._definitions.has(name)) { return this._definitions.get(name); } const model = new model_1.default(this, name, schema, options); this._definitions.set(name, model); const opts = schema_helpers_1.toKnexSchema(model, types.ModelOptions.check(options)); const check = this.knex.schema.hasTable(name); const query = this.knex.schema.createTable(name, opts); if (this.isNative) { if (!await check) { // tslint:disable-next-line:await-promise await query; } } else { if (!await helpers_1.runQuery(this, check, { needResponse: true })) { await helpers_1.runQuery(this, query); } } return model; } /** * Synchronously retrieve a model if it exists. If that model doesn't exist * an error will be thrown. * * @param name Name of the model * * @throws if `name` has not already been defined */ getModel(name) { return util_1.invariant(this._definitions.get(name), `no model defined by the name '${name}'`); } /** * First checks if the model's been defined with trilogy, then runs an * existence query on the database, returning `true` if the table exists * or `false` if it doesn't. * * @param name Name of the model */ async hasModel(name) { if (!this._definitions.has(name)) { return false; } const query = this.knex.schema.hasTable(name); return helpers_1.runQuery(this, query, { needResponse: true }); } /** * Removes the specified model from trilogy's definition and the database. * * @param name Name of the model to remove */ async dropModel(name) { if (!this._definitions.has(name)) { return false; } const query = this.knex.schema.dropTableIfExists(name); await helpers_1.runQuery(this, query, { needResponse: true }); this._definitions.delete(name); return true; } /** * Allows running any arbitrary query generated by trilogy's `knex` instance. * If the result is needed, pass `true` as the second argument, otherwise the * number of affected rows will be returned ( if applicable ). * * @param query Any query built with `knex` * @param [needResponse] Whether to return the result of the query */ async raw(query, needResponse) { return helpers_1.runQuery(this, query, { needResponse }); } /** * Drains the connection pools and releases connections to any open database * files. This should always be called at the end of your program to * gracefully shut down, and only once since the connection can't be reopened. */ async close() { if (this.isNative) { return this.knex.destroy(); } else { return this.pool.drain(); } } async create(modelName, object, options) { const model = this.getModel(modelName); return model.create(object, options); } async find(location, criteria, options) { const [table, column] = location.split('.', 2); const model = this.getModel(table); if (column) { return model.findIn(column, criteria, options); } else { return model.find(criteria, options); } } async findOne(location, criteria, options) { const [table, column] = location.split('.', 2); const model = this.getModel(table); if (column) { return model.findOneIn(column, criteria, options); } else { return model.findOne(criteria, options); } } async findOrCreate(modelName, criteria, creation, options) { const model = this.getModel(modelName); return model.findOrCreate(criteria, creation, options); } /** * Modify the properties of an existing object. While optional, if `data` * contains no properties no update queries will be run. * * @param modelName Name of the model * @param criteria Criteria used to restrict selection * @param data Updates to be made on matching objects * @param options */ async update(modelName, criteria, data, options) { const model = this.getModel(modelName); return model.update(criteria, data, options); } /** * Update an existing object or create it if it doesn't exist. If creation * is necessary a merged object created from `criteria` and `data` will be * used, with the properties from `data` taking precedence. * * @param modelName Name of the model * @param criteria Criteria used to restrict selection * @param data Updates to be made on matching objects * @param options */ async updateOrCreate(modelName, criteria, data, options) { const model = this.getModel(modelName); return model.updateOrCreate(criteria, data, options); } async get(location, criteria, defaultValue) { const [table, column] = location.split('.', 2); util_1.invariant(column, 'property name is required, ex: `get("users.rank")`'); const model = this.getModel(table); return model.get(column, criteria, defaultValue); } /** * Works similarly to the `set` methods in lodash, underscore, etc. Updates * the value at `column` to be `value` where the given criteria is met. * * @param location Model name and a column in dot-notation * @param criteria Criteria used to restrict selection * @param value Value returned if the result doesn't exist */ async set(location, criteria, value) { const [table, column] = location.split('.', 2); util_1.invariant(column, 'property name is required, ex: `set("users.rank")`'); const model = this.getModel(table); return model.set(column, criteria, value); } async getRaw(location, criteria, defaultValue) { const [table, column] = location.split('.', 2); util_1.invariant(column, 'property name is required, ex: `getRaw("users.rank")`'); const model = this.getModel(table); return model.getRaw(column, criteria, defaultValue); } /** * Works exactly like `set` but bypasses setters when updating the target value. * * @param location Model name and a column in dot-notation * @param criteria Criteria used to restrict selection * @param value Value returned if the result doesn't exist */ async setRaw(location, criteria, value) { const [table, column] = location.split('.', 2); util_1.invariant(column, 'property name is required, ex: `setRaw("users.rank")`'); const model = this.getModel(table); return model.setRaw(column, criteria, value); } /** * Increment the value of a given model's property by the specified amount, * which defaults to `1` if not provided. * * @param location Model name and a column in dot-notation * @param criteria Criteria used to restrict selection * @param amount */ async increment(location, criteria, amount) { const [table, column] = location.split('.', 2); const model = this.getModel(table); return model.increment(column, criteria, amount); } /** * Decrement the value of a given model's property by the specified amount, * which defaults to `1` if not provided. * * @param location Model name and a column in dot-notation * @param criteria Criteria used to restrict selection * @param amount */ async decrement(location, criteria, amount, allowNegative) { const [table, column] = location.split('.', 2); const model = this.getModel(table); return model.decrement(column, criteria, amount, allowNegative); } /** * Delete objects matching `criteria` from the given model. * * @remarks * If `criteria` is empty or absent, nothing will be done. This is a safeguard * against unintentionally deleting everything in the model. Use `clear` if * you really want to remove all rows. * * @param modelName Name of the model * @param criteria Criteria used to restrict selection */ async remove(modelName, criteria) { const model = this.getModel(modelName); return model.remove(criteria); } /** * Delete all objects from the given model. * * @param modelName Name of the model */ async clear(modelName) { const model = this.getModel(modelName); return model.clear(); } /** * Count the number of objects in the given model. * * @param location Model name and an optional column in dot-notation * @param criteria Criteria used to restrict selection * @param options */ async count(location, criteria, options) { if (location == null && criteria == null && options == null) { const query = this.knex('sqlite_master') .whereNot('name', 'sqlite_sequence') .where({ type: 'table' }) .count('* as count'); return helpers_1.runQuery(this, query, { needResponse: true }) .then(([{ count }]) => count); } const [table, column] = (location !== null && location !== void 0 ? location : '').split('.', 2); const model = this.getModel(table); return column ? model.countIn(column, criteria, options) : model.count(criteria, options); } /** * Find the minimum value contained in the model, comparing all values in * `column` that match the given criteria. * * @param location Model name and a column in dot-notation * @param criteria Criteria used to restrict selection * @param options */ async min(location, criteria, options) { const [table, column] = location.split('.', 2); util_1.invariant(column, 'property name is required, ex: `min("users.rank")`'); const model = this.getModel(table); return model.min(column, criteria, options); } /** * Find the maximum value contained in the model, comparing all values in * `column` that match the given criteria. * * @param location Model name and a column in dot-notation * @param criteria Criteria used to restrict selection * @param options */ async max(location, criteria, options) { const [table, column] = location.split('.', 2); util_1.invariant(column, 'property name is required, ex: `max("users.rank")`'); const model = this.getModel(table); return model.max(column, criteria, options); } /** * The `onQuery` hook is called each time a query is run on the database, * and receives the query in string form. * * @param [modelName] Optional, name of the model this subscriber will attach to * @param fn Function called when the hook is triggered * @param options * * @returns Unsubscribe function that removes the subscriber when called */ onQuery(...args) { // tslint:disable-next-line:no-empty let fn = () => { }; let location = ''; let options = { includeInternal: false }; if (args.length === 1) { fn = args[0]; } if (args.length >= 2) { if (typeof args[0] === 'string') { location = args[0]; } else if (typeof args[1] === 'function') { fn = args[0]; } if (typeof args[1] === 'function') { fn = args[1]; } else { options = Object.assign(Object.assign({}, options), args[1]); } } if (args.length === 3) { options = args[2] || options; } // console.log({ location, fn, options }) if (location !== '') { // all queries run on the model identified by `location` return this.getModel(location).onQuery(fn, options); } // all queries run across all defined models const unsubs = Array.from(new Array(this._definitions.size)); let i = -1; this._definitions.forEach(model => { unsubs[++i] = model.onQuery(fn, options); }); return () => unsubs.every(unsub => unsub()); } /** * Before an object is created, the beforeCreate hook is called with the * object. * * @remarks * This hook occurs before casting, so if a subscriber to this hook * modifies the incoming object those changes will be subject to casting. * It's also possible to prevent the object from being created entirely * by returning the EventCancellation symbol from a subscriber callback. * * @param [modelName] Optional, name of the model this subscriber will attach to * @param fn Function called when the hook is triggered * * @returns Unsubscribe function that removes the subscriber when called */ beforeCreate(...args) { if (args.length === 2) { // all creations run on the model identified by `scope` const [location, fn] = args; return this.getModel(location).beforeCreate(fn); } else { // all creations run across all defined models const [fn] = args; const unsubs = Array.from(new Array(this._definitions.size)); let i = -1; this._definitions.forEach(model => { unsubs[++i] = model.beforeCreate(fn); }); return () => unsubs.every(unsub => unsub()); } } /** * When an object is created, that object is returned to you and the * `afterCreate` hook is called with it. * * @param [modelName] Optional, name of the model this subscriber will attach to * @param fn Function called when the hook is triggered * * @returns Unsubscribe function that removes the subscriber when called */ afterCreate(...args) { if (args.length === 2) { // all creations run on the model identified by `scope` const [location, fn] = args; return this.getModel(location).afterCreate(fn); } else { // all creations run across all defined models const [fn] = args; const unsubs = Array.from(new Array(this._definitions.size)); let i = -1; this._definitions.forEach(model => { unsubs[++i] = model.afterCreate(fn); }); return () => unsubs.every(unsub => unsub()); } } /** * Prior to an object being updated the `beforeUpdate` hook is called with the * update delta, or the incoming changes to be made, as well as the criteria. * * @remarks * Casting occurs after this hook. A subscriber could choose to cancel the * update by returning the EventCancellation symbol or alter the selection * criteria. * * @param [modelName] Optional, name of the model this subscriber will attach to * @param fn Function called when the hook is triggered * * @returns Unsubscribe function that removes the subscriber when called */ beforeUpdate(...args) { if (args.length === 2) { // all updates run on the model identified by `scope` const [location, fn] = args; return this.getModel(location).beforeUpdate(fn); } else { // all updates run across all defined models const [fn] = args; const unsubs = Array.from(new Array(this._definitions.size)); let i = -1; this._definitions.forEach((model) => { unsubs[++i] = model.beforeUpdate(fn); }); return () => unsubs.every(unsub => unsub()); } } /** * Subscribers to the `afterUpdate` hook receive modified objects after they * are updated. * * @param [modelName] Optional, name of the model this subscriber will attach to * @param fn Function called when the hook is triggered * * @returns Unsubscribe function that removes the subscriber when called */ afterUpdate(...args) { if (args.length === 2) { // all updates run on the model identified by `scope` const [location, fn] = args; return this.getModel(location).afterUpdate(fn); } else { // all updates run across all defined models const [fn] = args; const unsubs = Array.from(new Array(this._definitions.size)); let i = -1; this._definitions.forEach(model => { unsubs[++i] = model.afterUpdate(fn); }); return () => unsubs.every(unsub => unsub()); } } /** * Before object removal, the criteria for selecting those objects is passed * to the `beforeRemove` hook. * * @remarks * Casting occurs after this hook. Subscribers can modify the selection * criteria or prevent the removal entirely by returning the `EventCancellation` * symbol. * * @param [modelName] Optional, name of the model this subscriber will attach to * @param fn Function called when the hook is triggered * * @returns Unsubscribe function that removes the subscriber when called */ beforeRemove(...args) { if (args.length === 2) { // all removals run on the model identified by `scope` const [location, fn] = args; return this.getModel(location).beforeRemove(fn); } else { // all removals run across all defined models const [fn] = args; const unsubs = Array.from(new Array(this._definitions.size)); let i = -1; this._definitions.forEach((model) => { unsubs[++i] = model.beforeRemove(fn); }); return () => unsubs.every(unsub => unsub()); } } /** * A list of any removed objects is passed to the `afterRemove` hook. * * @param [modelName] Optional, name of the model this subscriber will attach to * @param fn Function called when the hook is triggered * * @returns Unsubscribe function that removes the subscriber when called */ afterRemove(...args) { if (args.length === 2) { // all removals run on the model identified by `scope` const [location, fn] = args; return this.getModel(location).afterRemove(fn); } else { // all removals run across all defined models const [fn] = args; const unsubs = Array.from(new Array(this._definitions.size)); let i = -1; this._definitions.forEach(model => { unsubs[++i] = model.afterRemove(fn); }); return () => unsubs.every(unsub => unsub()); } } } exports.Trilogy = Trilogy; var hooks_1 = require("./hooks"); Object.defineProperty(exports, "EventCancellation", { enumerable: true, get: function () { return hooks_1.EventCancellation; } }); var model_2 = require("./model"); Object.defineProperty(exports, "Model", { enumerable: true, get: function () { return model_2.default; } }); tslib_1.__exportStar(require("./types"), exports); /** * Initialize a new datastore instance, creating a SQLite database file at * the provided path if it does not yet exist, or reading it if it does. * * @param path File path or `':memory:'` for memory-only storage * @param options Configuration for this trilogy instance */ exports.connect = (path, options) => new Trilogy(path, options);