UNPKG

@abw/badger-database

Version:
1,632 lines (1,631 loc) 98.5 kB
import { splitHash, isArray, splitList, isObject, fail, format, joinListAnd, isString, noValue, hasValue, extract, doNothing, isBoolean, remove, isFunction, isNull, isInteger, isFloat, firstValue } from "@abw/badger-utils"; import proxymise from "proxymise"; import { Pool } from "tarn"; const defaultIdColumn = "id"; const bitSplitter = /:/; const singleWord = /^\w+$/; const allColumns = "*"; const whereTrue = "true"; const unknown = "unknown"; const space = " "; const equals = "="; const comma = ", "; const newline = "\n"; const blank = ""; const singleQuote = "'"; const doubleQuote = '"'; const backtick = "`"; const lparen = "("; const rparen = ")"; const WITH = "WITH"; const INSERT = "INSERT"; const SELECT = "SELECT"; const UPDATE = "UPDATE"; const DELETE = "DELETE"; const FROM = "FROM"; const IN = "IN"; const NOT_IN = "NOT IN"; const INTO = "INTO"; const SET = "SET"; const VALUES = "VALUES"; const JOIN = "JOIN"; const LEFT_JOIN = "LEFT JOIN"; const RIGHT_JOIN = "RIGHT JOIN"; const INNER_JOIN = "INNER JOIN"; const FULL_JOIN = "FULL JOIN"; const WHERE = "WHERE"; const GROUP_BY = "GROUP BY"; const ORDER_BY = "ORDER BY"; const HAVING = "HAVING"; const LIMIT = "LIMIT"; const OFFSET = "OFFSET"; const RETURNING = "RETURNING"; const AS = "AS"; const ON = "ON"; const AND = "AND"; const ASC = "ASC"; const DESC = "DESC"; const BEGIN = "BEGIN"; const ROLLBACK = "ROLLBACK"; const COMMIT = "COMMIT"; const MATCH_DATABASE_URL = /^(\w+):\/\/(?:(?:(\w+)(?::([^@\/]+))?@)?(\w+)(?::(\d+))?\/)?(\w+)/; const MATCH_DATABASE_ELEMENTS = { engine: 1, user: 2, password: 3, host: 4, port: 5, database: 6 }; const DATABASE_CONNECTION_ALIASES = { username: "user", pass: "password", hostname: "host", file: "filename", name: "database", engineOptions: "options" }; const VALID_CONNECTION_KEYS = splitHash( // TODO: rename connectionString to url/uri? "engine user password host port database filename connectionString options" ); const VALID_CONNECTION_ALIASES = splitHash( "username pass hostname file name" ); const VALID_TABLE_COLUMN_KEYS = splitHash( "id readonly required fixed key type column tableColumn" ); const MATCH_VALID_FRAGMENT_KEY = /^(id|readonly|required|fixed|key|(type|column)=.*)/; const toArray = (item) => isArray(item) ? item : [item]; const article = (noun) => noun.match(/^[aeiou]/i) ? "an" : "a"; const ANSIStart = "\x1B["; const ANSIEnd = "m"; const ANSIColors = { reset: 0, bold: 1, bright: 1, dark: 2, black: 0, red: 1, green: 2, yellow: 3, blue: 4, magenta: 5, cyan: 6, grey: 7, white: 8, fg: 30, bg: 40 }; const ANSIRGB = { fg: (rgb) => `38;2;${rgb.r};${rgb.g};${rgb.b}`, bg: (rgb) => `48;2;${rgb.r};${rgb.g};${rgb.b}` }; const isRGB = (color2) => { const triple = splitList(color2); return triple.length === 3 ? { r: triple[0], g: triple[1], b: triple[2] } : null; }; const isHex = (color2) => { const match = color2.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); return match ? { r: parseInt(match[1], 16), g: parseInt(match[2], 16), b: parseInt(match[3], 16) } : null; }; const ANSIescapeCodes = (color2, base = "fg") => { const codes = []; const pair = color2.split(/ /, 2); const hue = pair.pop(); const code = (base ? ANSIColors[base] : 0) + ANSIColors[hue]; codes.push(code); if (pair.length) { const shade = pair.length ? pair.shift() : "dark"; codes.push(ANSIColors[shade]); } return ANSIStart + codes.join(";") + ANSIEnd; }; const ANSIRGBescapeCodes = (color2, base = "fg") => ANSIStart + ANSIRGB[base](color2) + ANSIEnd; const ANSIescapeCode = (color2, base = "fg") => { const rgb = isHex(color2) || isRGB(color2); return rgb ? ANSIRGBescapeCodes(rgb, base) : ANSIescapeCodes(color2, base); }; const ANSIescape = (colors = {}) => { const col = isObject(colors) ? colors : { fg: colors }; let escapes = []; if (col.bg) { escapes.push(ANSIescapeCode(col.bg, "bg")); } if (col.fg) { escapes.push(ANSIescapeCode(col.fg, "fg")); } return escapes.join(""); }; const ANSIresetCode = ANSIescapeCode("reset"); const ANSIreset = () => ANSIresetCode; const color = (colors) => (...text) => ANSIescape(colors) + text.join("") + ANSIresetCode; const palette = (palette2) => Object.entries(palette2).reduce( (palette3, [key, value]) => { palette3[key] = color(value); return palette3; }, {} ); const black = color("black"); const red = color("red"); const green = color("green"); const yellow = color("yellow"); const blue = color("blue"); const magenta = color("magenta"); const cyan = color("cyan"); const grey = color("grey"); const white = color("white"); const brightBlack = color("bright black"); const brightRed = color("bright red"); const brightGreen = color("bright green"); const brightYellow = color("bright yellow"); const brightBlue = color("bright blue"); const brightMagenta = color("bright magenta"); const brightCyan = color("bright cyan"); const brightGrey = color("bright grey"); const brightWhite = color("bright white"); const darkBlack = color("dark black"); const darkRed = color("dark red"); const darkGreen = color("dark green"); const darkYellow = color("dark yellow"); const darkBlue = color("dark blue"); const darkMagenta = color("dark magenta"); const darkCyan = color("dark cyan"); const darkGrey = color("dark grey"); const darkWhite = color("dark white"); const missing = (item) => fail(`No "${item}" specified`); const invalid = (item, value) => fail(`Invalid "${item}" specified: ${value}`); const notImplemented$2 = (method, module) => fail(`${method} is not implemented in ${module}`); const notImplementedInModule = (module) => (method) => notImplemented$2(method, module); const notImplementedInBaseClass = (module) => notImplementedInModule(`the ${module} base class`); class CustomError extends Error { // public message: string constructor(message) { super(message); this.name = this.constructor.name; } } class SQLParseError extends Error { constructor(query, args) { super(args.message); this.name = this.constructor.name; this.query = query; this.type = args.type; this.code = args.code; this.position = args.position; if (args.stack) { this.stack = args.stack; } } } class EngineDriverError extends CustomError { } const throwEngineDriver = (module, error) => { throw new EngineDriverError( `Failed to load "${module}" engine driver module. Have you installed it? Error: ` + error.message ); }; class UnexpectedRowCount extends CustomError { } class ColumnValidationError extends CustomError { } class InsertValidationError extends CustomError { } class DeletedRecordError extends CustomError { } class QueryBuilderError extends CustomError { } class TransactionError extends CustomError { } function unexpectedRowCount(n, action = "returned") { throw new UnexpectedRowCount(`${n} rows were ${action} when one was expected`); } const thrower = (formats, error = Error) => (fmt, data) => { const message = format( formats[fmt] || fail("Invalid message format: ", fmt), data ); throw new error(message); }; const COLUMN_VALIDATION_ERRORS = { unknown: 'Unknown "<column>" column in the <table> table', readonly: 'The "<column>" column is readonly in the <table> table', fixed: 'The "<column>" column is fixed in the <table> table', required: 'Missing required column "<column>" for the <table> table', multipleIds: 'Multiple columns are marked as "id" in the <table> table (<ids>)', noColumns: "No columns specified for the <table> table", invalidKey: "Invalid column specified as a key for the <table> table: <key>", invalidColumns: "Invalid columns specified for the <table> table: <columns>", invalidColumn: "Invalid column specification for <table>.<column> (<reason>)", invalidColumnSpec: "Invalid column specification for <table>.<column>: <spec> (<reason>)" }; const throwColumnValidationError = thrower( COLUMN_VALIDATION_ERRORS, ColumnValidationError ); const DELETE_ERRORS = { action: "Cannot <action> deleted <table> record #<id>" }; const throwDeletedRecordError = thrower( DELETE_ERRORS, DeletedRecordError ); const isObjKey = (key, obj) => key in obj; const isValidTableColumnKey = (key) => isObjKey(key, VALID_TABLE_COLUMN_KEYS); const isValidTableColumnObject = (o) => isObject(o) && Object.keys(o).every(isValidTableColumnKey); const invalidTableColumnObjectKeys = (o) => Object.keys(o).filter((k) => !isValidTableColumnKey(k)); const isValidColumnFragment = (fragment) => Boolean(fragment.match(MATCH_VALID_FRAGMENT_KEY)); const areValidColumnFragments = (fragments) => fragments.every(isValidColumnFragment); const invalidColumnFragments = (fragments) => fragments.filter((f) => !isValidColumnFragment(f)); const prepareColumnFragments = (table, column, fragments) => { return fragments.reduce( (result, fragment) => { const kv = fragment.split("=", 2); const key = kv.shift(); result[key] = kv.length ? kv[0] : true; return result; }, { column, tableColumn: `${table}.${column}` } ); }; const splitColumnFragments = (table, column, spec) => { const fragments = spec.split(bitSplitter); if (areValidColumnFragments(fragments)) { return fragments; } else { throwInvalidColumnFragments(table, column, spec, fragments); } }; const throwInvalidColumnFragments = (table, column, spec, fragments) => { const invalid2 = invalidColumnFragments(fragments); const istr = joinListAnd(invalid2.map((i) => `'${i}'`)); const reason = `${istr} ${invalid2.length > 1 ? "are" : "is"} not valid`; throwColumnValidationError( "invalidColumnSpec", { table, column, spec, reason } ); }; const prepareColumn = (table, column, spec) => { if (isString(spec)) { return prepareColumnFragments( table, column, splitColumnFragments(table, column, spec) ); } else if (isValidTableColumnObject(spec)) { column = spec.column ||= column; spec.tableColumn = `${table}.${column}`; return spec; } else if (isObject(spec)) { const invalid2 = invalidTableColumnObjectKeys(spec); const istr = joinListAnd(invalid2.map((i) => `'${i}'`)); const reason = invalid2.length > 1 ? `${istr} are not valid keys` : `${istr} is not a valid key`; throwColumnValidationError( "invalidColumn", { table, column, reason } ); } else if (noValue(spec)) { return { column, tableColumn: `${table}.${column}` }; } else { throwColumnValidationError( "invalidColumnSpec", { table, column, spec, reason: `${typeof spec} is not a valid type` } ); } }; const prepareColumns = (table, columns) => { if (noValue(columns)) { throwColumnValidationError("noColumns", { table }); } if (isString(columns)) { return prepareColumnsString(table, columns); } else if (isArray(columns)) { return prepareColumnsArray(table, columns); } else if (isObject(columns)) { return prepareColumnsObject(table, columns); } else { return throwColumnValidationError("invalidColumns", { table, columns }); } }; const prepareColumnsString = (table, columns) => prepareColumnsArray( table, splitList(columns) ); const prepareColumnsArray = (table, columns) => { return columns.reduce( (columns2, spec) => { const fragments = spec.split(bitSplitter); const column = fragments.shift(); if (areValidColumnFragments(fragments)) { columns2[column] = prepareColumnFragments(table, column, fragments); } else { throwInvalidColumnFragments(table, column, spec, fragments); } return columns2; }, {} ); }; const prepareColumnsObject = (table, columns) => { return Object.entries(columns).reduce( (columns2, [column, spec]) => ({ ...columns2, [column]: prepareColumn(table, column, spec) }), {} ); }; const prepareKeys = (table, spec, columns) => { if (spec.id) { return { keys: [spec.id], id: spec.id }; } const ids = Object.keys(columns).filter( (key) => columns[key].id ); if (ids.length > 1) { return throwColumnValidationError( "multipleIds", { table, ids: joinListAnd(ids.map((i) => `"${i}"`)) } ); } else if (ids.length) { return { keys: ids, id: ids[0] }; } const keys = spec.keys ? splitList(spec.keys) : Object.keys(columns).filter((key) => columns[key].key); if (keys.length) { return { keys }; } return { id: defaultIdColumn, keys: [defaultIdColumn] }; }; const databaseConfig = (config) => { const database = config.database || (config.env ? configEnv(config.env, { prefix: config.envPrefix }) : missing("database")); const connection = isString(database) ? parseDatabaseString(database) : extractDatabaseConfig(database); if (config.engineOptions) { connection.options = config.engineOptions; } if (config.pool) { connection.pool = config.pool; } return connection; }; const extractDatabaseConfig = (config) => { const connection = Object.keys(VALID_CONNECTION_KEYS).reduce( (connection2, key) => { if (hasValue(config[key])) { connection2[key] = config[key]; } return connection2; }, {} ); Object.entries(DATABASE_CONNECTION_ALIASES).reduce( (connection2, [alias, key]) => { if (hasValue(config[alias])) { connection2[key] = config[alias]; } return connection2; }, connection ); return connection; }; const configEnv = (env, options = {}) => { const prefix = options.prefix || "DATABASE"; const uscore = prefix.match(/_$/) ? "" : "_"; const regex = new RegExp(`^${prefix}${uscore}`); return hasValue(env[prefix]) ? parseDatabaseString(env[prefix]) : extract( env, regex, { key: (key) => key.replace(regex, "").toLowerCase() } ); }; const parseDatabaseString = (string) => { let config = {}; let match; if (match = string.match(/^sqlite:\/\/(.*)/)) { config.engine = "sqlite"; config.filename = match[1]; } else if (string === "sqlite:memory") { config.engine = "sqlite"; config.filename = ":memory:"; } else if (match = string.match(MATCH_DATABASE_URL)) { Object.entries(MATCH_DATABASE_ELEMENTS).map( ([key, index]) => { const value = match[index]; if (hasValue(value)) { config[key] = value; } } ); if (config.engine.match(/^postgres(ql)?/)) { config.engine = "postgres"; config.connectionString = string.replace(/^postgres:/, "postgresql:"); } } else { invalid("database", string); } return config; }; function Debugger(enabled, prefix = "", color2) { return enabled ? prefix ? (format2, ...args) => console.log( "%s" + prefix + "%s" + format2, color2 ? ANSIescape(color2) : "", ANSIreset(), ...args ) : console.log.bind(console) : doNothing; } function addDebug(obj, enabled, prefix = "", color2) { Object.assign( obj, { debug: Debugger(enabled, prefix, color2) } ); } const debugWidth = 16; let debug = { database: { debug: false, prefix: "Database", color: "bright magenta" }, engine: { debug: false, prefix: "Engine", color: "red" }, query: { debug: false, prefix: "Query", color: "cyan" }, tables: { debug: false, prefix: "Tables", color: "blue" }, table: { debug: false, prefix: "Table", color: "bright cyan" }, record: { debug: false, prefix: "Record", color: "green" }, builder: { debug: false, prefix: "Builder", color: "yellow" }, transaction: { debug: false, prefix: "Transaction", color: "bright red" }, test: { debug: false, prefix: "Test", color: "green" } }; const invalidDebugItem = (item) => fail(`Invalid debug item "${item}" specified`); const setDebug = (options) => { Object.entries(options).map( ([key, value]) => { const set = debug[key] || invalidDebugItem(key); if (isBoolean(value)) { set.debug = value; } else if (isObject(value)) { Object.assign(set, value); } } ); }; const getDebug = (name, ...configs) => { const defaults2 = debug[name] || invalidDebugItem(name); return Object.assign( {}, defaults2, ...configs ); }; const addDebugMethod = (object, name, ...configs) => { const options = getDebug(name, ...configs); const enabled = options.debug; const prefix = options.debugPrefix || options.prefix; const color2 = options.debugColor || options.color; const preline = prefix.length > debugWidth - 2 ? prefix + "\n" + "".padEnd(debugWidth, "-") + "> " : (prefix + " ").padEnd(debugWidth, "-") + "> "; addDebug(object, enabled, preline, color2); Object.assign(object, { debugData: DataDebugger(enabled, preline, color2) }); }; function DataDebugger(enabled, prefix, color2, length = debugWidth) { return enabled ? (message, data = {}) => { console.log( "%s" + prefix + "%s" + message, color2 ? ANSIescape(color2) : "", color2 ? ANSIreset() : "" ); Object.entries(data).map( ([key, value]) => console.log( "%s" + key.padStart(length, " ") + ":%s", color2 ? ANSIescape(color2) : "", color2 ? ANSIreset() : "", value ) ); } : doNothing; } const inOrNotIn = { [IN]: IN, [NOT_IN]: NOT_IN }; const isIn = (value) => inOrNotIn[value.toUpperCase().replaceAll(/\s+/g, " ")]; const aliasMethods = (object, aliases) => Object.entries(aliases).map( ([alias, method]) => object[alias] = object[method] || fail(`Invalid alias "${alias}" references non-existent method "${method}"`) ); const isQuery = (query) => isString(query) || query instanceof Builder; const expandFragments = (query, queryable, maxDepth = 16) => { query = query.trim(); let sql2 = query; let runaway = 0; let expanded = []; while (true) { let replaced = false; sql2 = sql2.replace( /<(\w+?)>/g, (_match, word) => { replaced = true; expanded.push(word); return queryable.fragment(word); } ); if (!replaced) { break; } if (++runaway >= maxDepth) { fail( `Maximum SQL expansion limit (maxDepth=${maxDepth}) exceeded: `, expanded.join(" -> ") ); } } return sql2; }; const relationStringRegex = /^(\w+)\s*([-~=#]>)\s*(\w+)\.(\w+)$/; const relationType = { "~>": "any", "->": "one", "=>": "many", "#>": "map" }; const relationAliases = { localKey: "from", local_key: "from", remoteKey: "to", remote_key: "to", orderBy: "order", order_by: "order" }; const relationConfig = (table, name, config) => { if (isString(config)) { config = parseRelationString(config); } else if (isString(config.relation)) { config = { ...parseRelationString(config.relation), ...config }; } Object.entries(relationAliases).map( ([key, value]) => { if (hasValue(config[key])) { config[value] ||= remove(config, key); } } ); if (!config.load) { ["type", "table", "to", "from"].forEach( (key) => { if (noValue(config[key])) { fail(`Missing "${key}" in ${name} relation for ${table} table`); } } ); } config.name = `${table}.${name}`; return config; }; const parseRelationString = (string) => { const match = string.match(relationStringRegex); return match ? { from: match[1], type: relationType[match[2]] || fail('Invalid type "', match[2], '" specified in relation: ', string), table: match[3], to: match[4] } : fail("Invalid relation string specified: ", string); }; const whereRelation = (record, spec) => { const lkey = spec.from; const rkey = spec.to; let where = spec.where || {}; if (lkey && rkey) { where[rkey] = record.row[lkey]; } return where; }; const spaceAfter = (string) => string && string.length ? string + space : blank; const spaceBefore = (string) => string && string.length ? space + string : blank; const spaceAround = (string) => string && string.length ? space + string + space : blank; const parens = (string) => string && string.length ? lparen + string + rparen : blank; const sql = (sql2) => ({ sql: sql2 }); let Builders = {}; let Generators = {}; const defaultContext = () => ({ setValues: [], whereValues: [], havingValues: [], placeholder: 1 }); const notImplemented$1 = notImplementedInBaseClass("Builder"); class Builder { static generateSQL(values) { const keyword = this.keyword; const joint = this.joint; return spaceAfter(keyword) + (isArray(values) ? values.join(joint) : values); } constructor(parent, ...args) { this.parent = parent; this.args = args; this.messages = this.constructor.messages; this.method = this.constructor.buildMethod; this.slot = this.constructor.contextSlot || this.method; this.initBuilder(...args); addDebugMethod(this, "builder", { debugPrefix: this.method && `Builder:${this.method}` }); } initBuilder() { } async one(args, options) { const sql2 = this.sql(); const db = this.lookupDatabase(); const values = this.allValues(args); this.debugData("one()", { sql: sql2, values }); return db.one(sql2, values, options); } async any(args, options) { const sql2 = this.sql(); const db = this.lookupDatabase(); const values = this.allValues(args); this.debugData("any()", { sql: sql2, values }); return db.any(sql2, values, options); } async all(args, options = {}) { const sql2 = this.sql(); const db = this.lookupDatabase(); const values = this.allValues(args); this.debugData("all()", { sql: sql2, values }); return db.all(sql2, values, options); } async run(args, options = {}) { const sql2 = this.sql(); const db = this.lookupDatabase(); const values = this.allValues(args); this.debugData("all()", { sql: sql2, values }); return db.run(sql2, values, { ...options, sanitizeResult: true }); } contextValues() { const { setValues, whereValues, havingValues } = this.resolveChain(); return { setValues, whereValues, havingValues }; } allValues(where = []) { const { setValues, whereValues, havingValues } = this.resolveChain(); if (isFunction(where)) { return where(setValues, whereValues, havingValues); } return [...setValues, ...whereValues, ...havingValues, ...where]; } setValues(...values) { if (values.length) { this.context.setValues = [ ...this.context.setValues, ...values ]; } return this.context.setValues; } whereValues(...values) { if (values.length) { this.context.whereValues = [ ...this.context.whereValues, ...values ]; } return this.context.whereValues; } havingValues(...values) { if (values.length) { this.context.havingValues = [ ...this.context.havingValues, ...values ]; } return this.context.havingValues; } sql() { const context = this.resolveChain(); return Object.entries(Generators).sort((a, b) => a[1][1] - b[1][1]).filter(([slot]) => context[slot]).map(([slot, entry]) => entry[0].generateSQL(context[slot], context)).filter((i) => hasValue(i)).join(newline); } // resolve the complete chain from top to bottom resolveChain() { return this.context || this.resolve( this.parent ? this.parent.resolveChain() : defaultContext() ); } // resolve a link in the chain and merge into parent context resolve(context, args = {}) { const slot = this.slot; this.context = { ...context, ...args }; const values = this.resolveLink(); if (values && values.length) { this.context[slot] = [...this.context[slot] || [], ...values]; } return this.context; } // resolve a link in the chain resolveLink() { return this.args.map( (item) => isObject(item) && item.sql ? item.sql : this.resolveLinkItem(item) ).flat(); } // resolve an individual argument for a link in the chain resolveLinkItem(item) { if (isString(item)) { return this.resolveLinkString(item); } else if (isArray(item)) { return this.resolveLinkArray(item); } else if (isFunction(item)) { return item(this); } else if (isObject(item)) { return this.resolveLinkObject(item); } else if (noValue(item)) { return this.resolveLinkNothing(item); } fail("Invalid query builder value: ", item); } resolveLinkString() { notImplemented$1("resolveLinkString()"); } resolveLinkArray() { notImplemented$1("resolveLinkArray()"); } resolveLinkObject() { notImplemented$1("resolveLinkObject()"); } resolveLinkNothing() { return []; } // utility methods lookup(key, error) { return this[key] || (this.parent ? this.parent.lookup(key) : fail(error || `Missing item in query chain: ${key}`)); } lookupDatabase() { return this.context.database || this.lookup("database"); } lookupTable() { return this.context.table || this.lookup("table"); } quote(item) { return this.lookupDatabase().quote(item); } quoteTableColumns(table, columns, prefix) { const func = table ? prefix ? (column) => this.quoteTableColumnAs(table, column, prefix + column) : (column) => this.quoteTableColumn(table, column) : prefix ? (column) => this.quoteColumnAs(column, prefix + column) : (column) => this.quote(column); return splitList(columns).map(func); } tableColumn(table, column) { return column.match(/\./) ? column : `${table}.${column}`; } quoteTableColumn(table, column) { return this.quote( this.tableColumn(table, column) ); } quoteTableAs(table, as) { return [ this.quote(table), this.quote(as) ].join(" AS "); } quoteTableColumnAs(table, column, as) { return [ this.quoteTableColumn(table, column), this.quote(as) ].join(" AS "); } quoteColumnAs(column, as) { return [ this.quote(column), this.quote(as) ].join(" AS "); } errorMsg(msgFormat, args) { const method = this.method || unknown; return this.error( format( this.messages?.[msgFormat] || fail("Invalid message format: ", msgFormat), { method, ...args } ) ); } toString() { return this.sql(); } error(...args) { const etype = this.errorType || QueryBuilderError; throw new etype(args.join("")); } } class After extends Builder { static buildMethod = "after"; static buildOrder = 100; // TODO } class Before extends Builder { static buildMethod = "before"; static buildOrder = 0; // TODO } class Select extends Builder { static buildMethod = "select"; static buildOrder = 20; static subMethods = "select columns from table prefix join where having group groupBy order orderBy limit offset range returning"; static keyword = SELECT; static joint = comma; static messages = { array: 'Invalid array with <n> items specified for query builder "<method>" component. Expected [column, alias] or [table, column, alias].', object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "columns", "column", "table", "prefix" and "as".' }; resolveLinkString(columns, table, prefix) { return this.quoteTableColumns(table, columns, prefix); } resolveLinkArray(columns) { if (columns.length === 2) { return this.quoteColumnAs(...columns); } else if (columns.length === 3) { return this.quoteTableColumnAs(...columns); } this.errorMsg("array", { n: columns.length }); } resolveLinkObject(column) { if (column.column && column.as) { return column.table ? this.quoteTableColumnAs( column.table, column.column, column.as ) : this.quoteColumnAs( column.column, column.as ); } const cols = column.column || column.columns; if (cols) { return this.resolveLinkString(cols, column.table, column.prefix); } this.errorMsg("object", { keys: Object.keys(column).sort().join(", ") }); } } class Columns extends Select { static buildMethod = "columns"; static contextSlot = "select"; initBuilder() { } resolveLinkString(columns, table = this.lookupTable(), prefix = this.context.prefix) { return super.resolveLinkString(columns, table, prefix); } resolveLinkArray(columns) { const table = this.lookupTable(); if (columns.length === 2) { return this.quoteTableColumnAs(table, ...columns); } else if (columns.length === 3) { return this.quoteTableColumnAs(...columns); } this.errorMsg("array", { n: columns.length }); } resolveLinkObject(column) { const table = this.lookupTable(); if (column.column && column.as) { return this.quoteTableColumnAs( column.table || table, column.column, column.as ); } const cols = column.column || column.columns; if (cols) { return this.resolveLinkString(cols, column.table || table, column.prefix || this.context.prefix); } this.errorMsg("object", { keys: Object.keys(column).sort().join(", ") }); } } let Database$1 = class Database extends Builder { initBuilder(database) { this.database = database; } resolve(context) { return { database: this.database, ...context }; } }; class Delete extends Builder { static buildMethod = "delete"; static buildOrder = 19; static subMethods = "from join where order order_by limit returning"; static keyword = DELETE; static joint = comma; static messages = { object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "columns", "column", "table" and "prefix".' }; static generateSQL(values) { const keyword = this.keyword; const joint = this.joint; return values && values.length ? spaceAfter(keyword) + (isArray(values) ? values.join(joint) : values) : keyword; } resolveLink() { if (this.args.length) { return super.resolveLink(); } else { this.context.delete = []; } } resolveLinkString(columns, table, prefix) { return this.quoteTableColumns(table, columns, prefix); } resolveLinkObject(column) { const cols = column.column || column.columns; if (cols) { return this.resolveLinkString(cols, column.table, column.prefix); } this.errorMsg("object", { keys: Object.keys(column).sort().join(", ") }); } } class From extends Builder { static buildMethod = "from"; static buildOrder = 30; static keyword = FROM; static joint = comma; static messages = { array: 'Invalid array with <n> items specified for query builder "<method>" component. Expected [table, alias].', object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "tables", "table" and "as".' }; initBuilder(...tables) { const table = tables.at(-1); if (isString(table)) { this.tableName = splitList(table).at(-1); } else if (isArray(table) && table.length === 2) { this.tableName = table[1]; } else if (isObject(table) && table.as) { this.tableName = table.as; } } resolve(context) { return super.resolve( context, // if we've got a table defined then add it to the context this.tableName ? { table: this.tableName } : void 0 ); } resolveLinkString(tables) { return splitList(tables).map( (table) => this.quote(table) ); } resolveLinkArray(table) { return table.length === 2 ? this.quoteTableAs(...table) : this.errorMsg("array", { n: table.length }); } resolveLinkObject(table) { if (table.table) { return table.as ? this.quoteTableAs(table.table, table.as) : this.quote(table.table); } else if (table.tables) { return this.resolveLinkString(table.tables); } this.errorMsg("object", { keys: Object.keys(table).sort().join(", ") }); } } class Group extends Builder { static buildMethod = "group"; static buildAlias = "groupBy"; static buildOrder = 60; static keyword = GROUP_BY; static joint = comma; static messages = { array: 'Invalid array with <n> items specified for query builder "<method>" component. Expected [column].', object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "columns" and "column".' }; resolveLinkString(group) { return splitList(group).map( (column) => this.quote(column) ); } resolveLinkArray(group) { if (group.length === 1) { return this.quote(group[0]); } this.errorMsg("array", { n: group.length }); } resolveLinkObject(group) { if (group.column) { return this.quote(group.column); } else if (group.columns) { return this.resolveLinkString(group.columns); } this.errorMsg("object", { keys: Object.keys(group).sort().join(", ") }); } } class Where extends Builder { static buildMethod = "where"; static buildOrder = 50; static keyword = WHERE; static joint = space + AND + space; static messages = { array: 'Invalid array with <n> items specified for query builder "<method>" component. Expected [column, value] or [column, operator, value].', object: 'Invalid value array with <n> items specified for query builder "<method>" component. Expected [value] or [operator, value].' }; resolveLinkString(columns) { const database = this.lookupDatabase(); return splitList(columns).map( (column) => database.engine.formatWherePlaceholder( column, void 0, this.context.placeholder++ ) ); } resolveLinkArray(criteria) { const database = this.lookupDatabase(); if (criteria.length === 2) { let match; if (isArray(criteria[1])) { const inOrNotIn2 = isIn(criteria[1][0]); if (inOrNotIn2) { const inValues = toArray(criteria[1][1]); this.addValues(...inValues); return this.resolveIn(criteria[0], inOrNotIn2, inValues); } if (hasValue(criteria[1][1])) { this.addValues(criteria[1][1]); } match = [criteria[1][0], void 0]; } else { this.addValues(criteria[1]); } return database.engine.formatWherePlaceholder( criteria[0], match, this.context.placeholder++ ); } else if (criteria.length === 3) { const inOrNotIn2 = isIn(criteria[1]); if (inOrNotIn2) { const inValues = toArray(criteria[2]); this.addValues(...inValues); return this.resolveIn(criteria[0], inOrNotIn2, inValues); } if (hasValue(criteria[2])) { this.addValues(criteria[2]); } return database.engine.formatWherePlaceholder( criteria[0], [criteria[1], criteria[2]], this.context.placeholder++ ); } else { this.errorMsg("array", { n: criteria.length }); } } resolveLinkObject(criteria) { const database = this.lookupDatabase(); let values = []; const result = Object.entries(criteria).map( ([column, value]) => { if (isArray(value)) { if (value.length === 2) { const inOrNotIn2 = isIn(value[0]); const inValues = toArray(value[1]); if (inOrNotIn2) { values.push(...inValues); return this.resolveIn(column, inOrNotIn2, inValues); } values.push(value[1]); } else if (value.length !== 1) { this.errorMsg("object", { n: value.length }); } } else if (isNull(value)) { return database.engine.formatWhereNull( column ); } else { values.push(value); } return database.engine.formatWherePlaceholder( column, value, this.context.placeholder++ ); } ); if (values.length) { this.addValues(...values); } return result; } resolveIn(column, operator, values) { const database = this.lookupDatabase(); const ph = this.context.placeholder; this.context.placeholder += values.length; return database.engine.formatWhereInPlaceholder( column, operator, values, ph ); } addValues(...values) { this.whereValues(...values); } } class Having extends Where { static buildMethod = "having"; static buildOrder = 70; static keyword = HAVING; // Everything works the same as for Where, EXCEPT for the fact that we save // values in a separate list. Any where() values go in this.context.values, // and having() values go in this.context.havingValues so that we can make // sure that these values are always provided at the end of the query. // e.g. db.select(...).where({a:10}).having({c:30}).where({b:20}) will // generate a query like 'SELECT ... WHERE a=? AND b=? ... HAVING c=?'. // The where() values for a and b (10, 20) must come before the having() // value for b (30) addValues(...values) { this.havingValues(...values); } } class Insert extends Builder { static buildMethod = "insert"; static buildOrder = 17; static subMethods = "into join values returning"; static keyword = INSERT; static joint = comma; static messages = { object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "columns" and "column".' }; static generateSQL() { return this.keyword; } resolveLink() { if (this.args.length) { return super.resolveLink(); } else { this.context.insert = []; } } resolveLinkString(columns) { return this.quoteTableColumns(void 0, columns); } resolveLinkArray(columns) { return this.quoteTableColumns(void 0, columns); } resolveLinkObject(column) { const cols = column.column || column.columns; if (cols) { return this.resolveLinkString(cols, column.table); } this.errorMsg("object", { keys: Object.keys(column).sort().join(", ") }); } } class Into extends Builder { static buildMethod = "into"; static buildOrder = 29; static keyword = INTO; static joint = comma; static generateSQL(values, context) { const keyword = this.keyword; const joint = this.joint; const database = context.database; const columns = context.insert || []; let place = context.placeholder; const into = spaceAfter(keyword) + (isArray(values) ? values.join(joint) : values); const cols = columns.length ? spaceBefore(parens(columns.join(comma))) : blank; const vals = columns.length ? newline + spaceAfter(VALUES) + parens( columns.map( () => database.engine.formatPlaceholder( place++ ) ).join(comma) ) : blank; return into + cols + vals; } initBuilder(...tables) { if (tables.length === 1 && isString(tables[0])) { this.tableName = tables[0]; } } resolve(context) { return super.resolve( context, // if we've got a table defined then add it to the context this.tableName ? { table: this.tableName } : void 0 ); } resolveLinkString(table) { return [ this.quote(table) ]; } } const tableColumnRegex = /^(\w+)\.(\w+)$/; const joinRegex = /^(.*?)\s*(<?=>?)\s*(\w+)\.(\w+)(?:\s+as\s+(\w+))?$/; const joinElements = { from: 1, type: 2, table: 3, to: 4, as: 5 }; const joinTypes = { default: JOIN, inner: INNER_JOIN, "=": JOIN, left: LEFT_JOIN, "<=": LEFT_JOIN, right: RIGHT_JOIN, "=>": RIGHT_JOIN, full: FULL_JOIN, "<=>": FULL_JOIN }; class Join extends Builder { static buildMethod = "join"; static buildOrder = 40; static keyword = blank; static joint = newline; static messages = { type: 'Invalid join type "<joinType>" specified for query builder "<method>" component. Valid types are "left", "right", "inner" and "full".', string: 'Invalid join string "<join>" specified for query builder "<method>" component. Expected "from=table.to".', object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "type", "table", "from" and "to".', array: 'Invalid array with <n> items specified for query builder "<method>" component. Expected [type, from, table, to], [from, table, to] or [from, table.to].' }; resolveLinkString(join) { let match = join.match(joinRegex); let config = {}; if (match) { Object.entries(joinElements).map( ([key, index]) => config[key] = match[index] ); return this.resolveLinkObject(config); } this.errorMsg("string", { join }); } resolveLinkArray(join) { if (join.length === 4) { const [type, from, table, to] = join; return this.resolveLinkObject({ type, from, table, to }); } else if (join.length === 3) { const [from, table, to] = join; return this.resolveLinkObject({ from, table, to }); } else if (join.length === 2) { const match = join[1].match(tableColumnRegex); if (match) { const from = join[0]; const [, table, to] = match; return this.resolveLinkObject({ from, table, to }); } } this.errorMsg("array", { n: join.length }); } resolveLinkObject(join) { const type = joinTypes[join.type || "default"] || this.errorMsg("type", { joinType: join.type }); if (join.table && join.from && join.to) { return join.as ? this.constructJoinAs( type, join.from, join.table, join.as, this.tableColumn(join.as, join.to) ) : this.constructJoin( type, join.from, join.table, this.tableColumn(join.table, join.to) ); } else if (join.from && join.to) { const match = join.to.match(tableColumnRegex); if (match) { return join.as ? this.constructJoinAs( type, join.from, match[1], join.as, this.tableColumn(join.as, match[2]) ) : this.constructJoin( type, join.from, match[1], this.tableColumn(match[1], match[2]) ); } } this.errorMsg("object", { keys: Object.keys(join).sort().join(", ") }); } constructJoin(type, from, table, to) { return spaceAfter(type) + this.quote(table) + spaceAround(ON) + this.quote(from) + spaceAround(equals) + this.quote(to); } constructJoinAs(type, from, table, as, to) { return spaceAfter(type) + this.quote(table) + spaceAround(AS) + this.quote(as) + spaceAround(ON) + this.quote(from) + spaceAround(equals) + this.quote(to); } } class Simple extends Builder { static buildMethod = "simple"; static generateSQL(values) { const keyword = this.keyword; return spaceAfter(keyword) + (isArray(values) ? values.at(-1) : values); } initBuilder(value) { this.value = value; } resolve(context) { this.context = { ...context, [this.slot]: this.value }; return this.context; } } class Limit extends Simple { static buildMethod = "limit"; static buildOrder = 90; static keyword = LIMIT; initBuilder(limit) { this.value = limit; } } class Offset extends Simple { static buildMethod = "offset"; static buildOrder = 95; static keyword = OFFSET; initBuilder(offset) { this.value = offset; } } class Order extends Builder { static buildMethod = "order"; static buildAlias = "orderBy"; static buildOrder = 80; static keyword = ORDER_BY; static joint = comma; static messages = { array: 'Invalid array with <n> items specified for query builder "<method>" component. Expected [column, direction] or [column].', object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "columns", "column", "direction", "dir", "asc" and "desc".' }; resolveLinkString(order, dir) { return splitList(order).map( (column) => this.constructOrder(column) ).join(", ") + (dir ? space + dir : ""); } resolveLinkArray(order) { if (order.length === 2 || order.length === 1) { return this.constructOrder(...order); } this.errorMsg("array", { n: order.length }); } resolveLinkObject(order) { const dir = order.direction || order.dir || order.desc && DESC || order.asc && ASC; if (order.column) { return this.constructOrder(order.column, dir); } else if (order.columns) { return this.resolveLinkString(order.columns, dir); } this.errorMsg("object", { keys: Object.keys(order).sort().join(", ") }); } constructOrder(column, dir) { return this.quote(column) + (dir ? space + dir : ""); } } class Prefix extends Builder { static buildMethod = "prefix"; initBuilder(prefix) { this.prefix = prefix; } resolve(context) { this.context = { ...context, prefix: this.prefix }; return this.context; } } class Range extends Builder { static buildMethod = "range"; static messages = { arg: 'Invalid argument specified for query builder "<method>" component. Expected (from, to), (from) or object.', args: 'Invalid arguments with <n> items specified for query builder "<method>" component. Expected (from, to), (from) or object.', object: 'Invalid object with "<keys>" properties specified for query builder "<method>" component. Valid properties are "from", "to", "limit" and "offset".' }; initBuilder(...args) { if (args.length === 2) { this.args = this.twoNumberArgs(...args); } else if (args.length === 1) { const arg = args[0]; if (isInteger(arg)) { this.args = this.oneNumberArg(arg); } else if (isObject(arg)) { this.args = this.objectArgs(arg); } else { this.errorMsg("arg"); } } else { this.errorMsg("args", { n: args.length }); } } twoNumberArgs(from, to) { return { offset: from, limit: to - from + 1 }; } oneNumberArg(from) { return { offset: from }; } objectArgs(args) { if (args.from && args.to) { return this.twoNumberArgs(args.from, args.to); } else if (args.from) { return this.oneNumberArg(args.from); } else if (args.to) { return { limit: args.to + 1 }; } else if (args.limit || args.offset) { return { limit: args.limit, offset: args.offset }; } else { this.errorMsg("object", { keys: Object.keys(args).sort().join(", ") }); } } resolve(context) { this.context = { ...context, ...this.args }; return this.context; } } class Returning extends Select { static buildMethod = "returning"; static buildOrder = 96; static keyword = RETURNING; } class Set extends Where { static buildMethod = "set"; static buildOrder = 45; static keyword = SET; static joint = comma; static messages = { array: 'Invalid array with <n> items specified for query builder "<method>" component. Expected [column, value].', object: 'Invalid value array with <n> items specified for query builder "<method>" component. Expected [value] or [operator, value].' }; // This works in a similar way to Where, EXCEPT for the fact that // we save values in a separate list, this.context.setValues. addValues(...values) { this.setValues(...values); } resolveLinkObject(criteria) { const database = this.lookupDatabase(); let values = []; const result = Object.entries(criteria).map( ([column, value]) => { values.push(value); return database.engine.formatWherePlaceholder( column, // the 'value' here can be a JSON array which confuses the // formatWherePlaceholder() method into thinking it's a comparison // which is valid in where() but not in set() "", this.context.placeholder++ ); } ); if (values.length) { this.addValues(...values); } return result; } resolveLinkArray(criteria) { if (criteria.length == 2) { this.addValues(criteria[1]); return this.lookupDatabase().engine.formatSetPlaceholder( criteria[0], this.context.placeholder++ ); } else { this.errorMsg("array", { n: criteria.length }); } } } let Table$1 = class Table extends Builder { static buildMethod = "table"; initBuilder(table) { this.tableName = table; } resolve(context) { this.context = { ...context, table: this.tableName }; return this.context; } }; class Update extends From { static buildMethod = "update"; static buildOrder = 18; static subMethods = "join set where order order_by limit returning"; static keyword = UPDATE; static joint = comma; } class Values extends Builder { static buildMethod = "values"; static buildOrder = 0; // values() is used to provide pre-defined values for the INSERT INTO clause. // It adds the values to setValues() when the link is resolved but doesn't // generate any output - see Into.js for where the VALUES clause is created resolveLinkItem(item) { if (isInteger(item) || isFloat(item)) { return this.resolveLinkString(item); } return super.resolveLinkItem(item); } resolveLinkString(value) { this.setValues(value); return []; } resolveLinkArray(values) { values.forEach( (value) => { this.setValues(value); } ); return []; } } const builderProxy = (builders, parent, options = {}) => new Proxy( parent, { get(target, prop) { if (prop === "toString") { return target.toString.bind(target); } const bclass = builders[prop]; if (!bclass) { return Reflect.get(target, prop); }