@abw/badger-database
Version:
Javascript database abstraction layer
1,632 lines (1,631 loc) • 98.5 kB
JavaScript
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);
}