This is the template project that's checked out and configured when you run the bando-up command from ljsthw-bandolier. This is where the code really lives.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bandolier-template/lib/ormish.js

285 lines
9.2 KiB

import config from '../knexfile.cjs';
import knexConfig from 'knex';
import assert from 'assert';
import { attachPaginate } from 'knex-paginate';
export const knex = knexConfig(config.development);
// run the PERF_TRICKS to configure sqlite3 when thing start, really need to make this
// a configuration and only do it with sqlite3, but for now just get this done
if(config.development.client === "sqlite3") {
const PERF_TRICKS = [
"pragma journal_mode = WAL", // use a WAL journal to not block writers/readers
"pragma synchronous = normal", // set concurrency to normal now that we have WAL
"pragma temp_store = memory", // use RAM to make the temporary indices
// "pragma mmap_size = 30000000000", // use 30GB of mmap to store DB, not useful in multi-process settings
// "pragma page_size = 32768", // improve performance only if you store large stuff
"pragma vacuum", // give it a vacuum, this could impact startup time significantly
"pragma optimize", // optimize it, but should be done periodically
];
for(let sql of PERF_TRICKS) {
await knex.raw(sql);
}
}
attachPaginate();
export const SCHEMA = {};
const load_schema = async () => {
const raw = await knex("sqlite_schema").where({"type": "table"}).select(['type', 'name']);
for(let table of raw) {
table._columns = await knex(table.name).columnInfo();
SCHEMA[table.name] = table;
}
}
await load_schema();
/**
* In some cases (like the generic admin) you need to get validation
* rules but you don't have a specific class to work with. This function
* is called by Model.validation and you can call it directly to get rules
* for a database table.
*
* @param name string - the table name.
* @param rules Object - default rules with empty "" for the rules you want filled in
* @param all boolean - set this to true if you want everything
* @param no_id boolean - defaults to true, set false if you also want the id
* @return Object - the resulting rules to use with Validator
*/
export const validation = (name, rules, all=false, no_id=true) => {
assert(rules, "rules parameter is required and will be modified");
const schema = SCHEMA[name];
assert(schema, `There is no schema for table named ${name}. Did you forget to migrate:latest?`);
for(let [key, opts] of Object.entries(schema._columns)) {
// most of the time you don't want the id
if(no_id && key === "id") continue;
if(all || rules[key] === "") {
let required = opts.nullable || opts.defaultValue ? "nullable|" : "required|";
switch(opts.type) {
case "varchar": // fallthrough
case "text":
// some databases have an email type but we'll also look for ones named email
if(key === "email") {
rules[key] = required + "email";
} else {
rules[key] = required + "string";
}
if(opts.maxLength) {
rules[key] += `|min:0|max:${opts.maxLength}`;
}
break;
case "boolean":
rules[key] = required + "boolean";
break;
case "integer":
case "float":
rules[key] = required + "numeric";
break;
case "date":
// BUG: validator doesn't do proper date formatting yet
// rules[key] = required + "date";
break;
case "email":
// not all databases have this
rules[key] = required + "email";
break;
default:
rules[key] = required;
}
}
}
return rules;
}
export class Model {
constructor(attr) {
assert(attr, "Must give attributes.");
Object.assign(this, attr);
}
static from(attr, also_remove=undefined) {
return new this(this.clean(attr, also_remove));
}
get schema() {
return this.constructor.schema;
}
static clean(attr, also_remove=undefined) {
assert(attr, "Must give attributes to clean.");
let clean_entries = Object.entries(attr)
.filter(([k,v]) => k in this.schema);
if(also_remove) also_remove.forEach(k => delete clean_entries[k]);
return Object.fromEntries(clean_entries);
}
get table_name() {
return this.constructor.table_name;
}
/**
* Returns an object of basic rules meant for lib/api.js:validate
* based on what's in the database. It's meant to be an easy to
* pass in starter which you can augment. It expects a set of rules
* with keys you want configured. Any key that's set to an empty string ""
* will be filled in with a minimum rule to match the database schema.
*
* It's designed to be called once at the top of an api/ handler to get
* a basic set of rules. You could also run it to print out the rules then
* simply write the rules directly where you need them.
*
* @param rules {Object} - rules specifier
*/
static validation(rules) {
return validation(this.table_name, rules);
}
async destroy() {
assert(this.table_name !== undefined, "You must set class variable table_name.");
await knex(this.table_name).
where({id: this.id}).
del();
}
async has_one(model, where, columns) {
assert(where.id !== undefined, `where must at least have id for has_one ${model.table_name} you have ${JSON.stringify(where)}`);
return await model.first(where, columns);
}
async has_many(model, where, columns) {
return await model.all(where, columns);
}
async many_to_many(model, through_table) {
// SECURITY: doing string interpolation which might allow injecting SQL
let query = knex(model.table_name).where("id", "in",
knex(through_table).select(`${model.table_name}_id as id`).where(`${this.table_name}_id`, "=", this.id));
let rows = await query;
let results = [];
for(let r of rows) {
results.push(new model(await model.clean(r)));
}
return results;
}
static async count(where, columns) {
// the knex count api returns a DB specific result, so we need
// to specify what we want, which is count:
const spec = { count: columns || ['id']};
let res = await knex(this.table_name).where(where).count(spec);
if(res.length == 1) {
// single result, just give the count
return res[0].count;
} else {
console.warn("Your call to count in", this.table_name, "using where=", where, "columns: ", columns, "returned a weird result:", res);
return res; // weird result let them deal with it
}
}
static async insert(attr) {
assert(this.table_name !== undefined, "You must set class variable table_name.");
assert(attr, `You must give some attr to insert into ${this.table_name}`);
let res = await knex(this.table_name).insert(attr);
assert(res, `Failed to get an id from the insert for ${this.table_name}`);
attr.id = res[0];
return new this(attr);
}
/**
* Implements an upsert (insert but update on conflict) for Postgres, MySQL, and SQLite3 only.
*
* @param { Object } attr - The attributes to insert or update.
* @param { string } conflict_key - The key that can cause a conflict then update.
* @param { boolean } merge - Defaults to true and will change the record. false will ignore and not update on conflict.
* @return { number } - id or undefined
*/
static async upsert(attr, conflict_key, merge=true) {
assert(conflict_key !== undefined, `You forgot to set the conflict_key on upsert to table ${this.table_name}`);
let result = undefined;
// TODO: allow specifying returns for databases that support it
if(merge) {
result = await knex(this.table_name).insert(attr).onConflict(conflict_key).merge();
} else {
result = await knex(this.table_name).insert(attr).onConflict(conflict_key).ignore();
}
// returns the id of the row or undefined
return result !== undefined ? result[0] : undefined;
}
static async update(where, what) {
assert(where, "You must give a where options.");
return knex(this.table_name).where(where).update(what);
}
static async first(where, columns) {
assert(where, "You must give a where options.");
let attr = undefined;
if(columns) {
attr = await knex(this.table_name).column(columns).first().where(where);
} else {
attr = await knex(this.table_name).first().where(where);
}
return attr !== undefined ? new this(attr) : attr;
}
static async delete(where) {
assert(where, "You must give a where options.");
return knex(this.table_name).where(where).del();
}
static async all(where, columns) {
assert(where, "You must give a where options.");
let results = [];
if(columns) {
results = await knex(this.table_name).column(columns).where(where).select();
} else {
results = await knex(this.table_name).where(where);
}
let final = results.map(r => new this(r));
return final;
}
static from_table(table_name) {
let m = class extends Model { };
m.table_name = table_name;
assert(SCHEMA, "schema is not loaded!");
assert(SCHEMA[table_name], `table named ${table_name} not in SCHEMA: ${ Object.keys(SCHEMA) }`);
m.schema = SCHEMA[table_name]._columns;
return m;
}
static async exists(where) {
let res = await knex(this.table_name).select('id').where(where).limit(1).first();
return res ? res.id : false;
}
}
export default { knex, SCHEMA, Model };