diff --git a/deno.json b/deno.json index 29271d8..a2a3c62 100644 --- a/deno.json +++ b/deno.json @@ -29,5 +29,9 @@ "include": [ "./tests" ] + }, + "compilerOptions": { + "experimentalDecorators": true, + "noImplicitOverride": true } } diff --git a/src/queryBuilder/FunctionBuilder.js b/src/queryBuilder/FunctionBuilder.js deleted file mode 100644 index a281856..0000000 --- a/src/queryBuilder/FunctionBuilder.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const { RawBuilder, normalizeRawArgs } = require('./RawBuilder'); -const { asSingle, isNumber } = require('../utils/objectUtils'); - -class FunctionBuilder extends RawBuilder {} - -function fn(...argsIn) { - const { sql, args } = normalizeRawArgs(argsIn); - return new FunctionBuilder(`${sql}(${args.map(() => '?').join(', ')})`, args); -} - -for (const func of ['coalesce', 'concat', 'sum', 'avg', 'min', 'max', 'count', 'upper', 'lower']) { - fn[func] = (...args) => fn(func.toUpperCase(), args); -} - -fn.now = (precision) => { - precision = parseInt(asSingle(precision), 10); - - if (isNaN(precision) || !isNumber(precision)) { - precision = 6; - } - - // We need to use a literal precision instead of a binding here - // for the CURRENT_TIMESTAMP to work. This is okay here since we - // make sure `precision` is a number. There's no chance of SQL - // injection here. - return new FunctionBuilder(`CURRENT_TIMESTAMP(${precision})`, []); -}; - -module.exports = { - FunctionBuilder, - fn, -}; diff --git a/src/queryBuilder/FunctionBuilder.ts b/src/queryBuilder/FunctionBuilder.ts new file mode 100644 index 0000000..1f9ad9b --- /dev/null +++ b/src/queryBuilder/FunctionBuilder.ts @@ -0,0 +1,49 @@ +import { normalizeRawArgs, RawBuilder } from './RawBuilder.ts'; +import { asSingle, isNumber } from '../utils/object.ts'; +import { nany } from '../ninja.ts'; + +class FunctionBuilder extends RawBuilder {} + +const keywords = [ + 'coalesce', + 'concat', + 'sum', + 'avg', + 'min', + 'max', + 'count', + 'upper', + 'lower', + // deno-lint-ignore no-explicit-any +].reduce((c: any, p: string) => { + c[p] = true; + return c; +}, {}); + +function fn(...argsIn: [string, ...nany[]]) { + const { sql, args } = normalizeRawArgs(argsIn); + return new FunctionBuilder(`${sql}(${args.map(() => '?').join(', ')})`, args); +} + +export function createFunctionBuilder( + name: string, +): (...args: nany[]) => FunctionBuilder { + if (name === 'now') { + return (precision: string | string[]) => { + let p = parseInt(asSingle(precision), 10); + + if (isNaN(p) || !isNumber(p)) { + p = 6; + } + + // We need to use a literal precision instead of a binding here + // for the CURRENT_TIMESTAMP to work. This is okay here since we + // make sure `precision` is a number. There's no chance of SQL + // injection here. + return new FunctionBuilder(`CURRENT_TIMESTAMP(${p})`, []); + }; + } else if (keywords[name]) { + return (...args: nany[]) => fn(name, args); + } + throw new Error(`Function ${name} not supported.`); +} diff --git a/src/queryBuilder/InternalOptions.js b/src/queryBuilder/InternalOptions.js deleted file mode 100644 index 1083d3d..0000000 --- a/src/queryBuilder/InternalOptions.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -class InternalOptions { - constructor() { - this.skipUndefined = false; - this.keepImplicitJoinProps = false; - this.returnImmediatelyValue = undefined; - this.isInternalQuery = false; - this.debug = false; - this.schema = undefined; - } - - clone() { - const copy = new this.constructor(); - - copy.skipUndefined = this.skipUndefined; - copy.keepImplicitJoinProps = this.keepImplicitJoinProps; - copy.returnImmediatelyValue = this.returnImmediatelyValue; - copy.isInternalQuery = this.isInternalQuery; - copy.debug = this.debug; - copy.schema = this.schema; - - return copy; - } -} - -module.exports = { - InternalOptions, -}; diff --git a/src/queryBuilder/QueryBuilderContext.js b/src/queryBuilder/QueryBuilderContext.js deleted file mode 100644 index 23a3599..0000000 --- a/src/queryBuilder/QueryBuilderContext.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const { QueryBuilderContextBase } = require('./QueryBuilderContextBase'); - -class QueryBuilderContext extends QueryBuilderContextBase { - constructor(builder) { - super(builder); - - this.runBefore = []; - this.runAfter = []; - this.onBuild = []; - } - - clone() { - const ctx = super.clone(); - - ctx.runBefore = this.runBefore.slice(); - ctx.runAfter = this.runAfter.slice(); - ctx.onBuild = this.onBuild.slice(); - - return ctx; - } -} - -module.exports = { - QueryBuilderContext, -}; diff --git a/src/queryBuilder/QueryBuilderContext.ts b/src/queryBuilder/QueryBuilderContext.ts index 742f9f9..9986653 100644 --- a/src/queryBuilder/QueryBuilderContext.ts +++ b/src/queryBuilder/QueryBuilderContext.ts @@ -1,7 +1,9 @@ import { nany } from '../ninja.ts'; import { QueryBuilderContextBase } from './QueryBuilderContextBase.ts'; +import { IModel } from './QueryBuilderOperationSupport.ts'; -export class QueryBuilderContext extends QueryBuilderContextBase { +export class QueryBuilderContext + extends QueryBuilderContextBase { runBefore: nany[]; runAfter: nany[]; onBuild: nany[]; @@ -14,14 +16,15 @@ export class QueryBuilderContext extends QueryBuilderContextBase { this.onBuild = []; } - clone() { - const ctx = new QueryBuilderContext(); - super.cloneInto(ctx); - - ctx.runBefore = this.runBefore.slice(); - ctx.runAfter = this.runAfter.slice(); - ctx.onBuild = this.onBuild.slice(); + override clone(): QueryBuilderContext { + return this.cloneInto(new QueryBuilderContext()); + } - return ctx; + override cloneInto(clone: QueryBuilderContext): QueryBuilderContext { + super.cloneInto(clone); + clone.runBefore = this.runBefore.slice(); + clone.runAfter = this.runAfter.slice(); + clone.onBuild = this.onBuild.slice(); + return clone; } } diff --git a/src/queryBuilder/QueryBuilderContextBase.js b/src/queryBuilder/QueryBuilderContextBase.js deleted file mode 100644 index 92b830e..0000000 --- a/src/queryBuilder/QueryBuilderContextBase.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -const { InternalOptions } = require('./InternalOptions'); - -class QueryBuilderContextBase { - constructor(builder) { - this.userContext = builder ? new builder.constructor.QueryBuilderUserContext(builder) : null; - this.options = builder ? new this.constructor.InternalOptions() : null; - this.knex = null; - - this.aliasMap = null; - this.tableMap = null; - } - - static get InternalOptions() { - return InternalOptions; - } - - clone() { - const ctx = new this.constructor(); - - ctx.userContext = this.userContext; - ctx.options = this.options.clone(); - ctx.knex = this.knex; - - ctx.aliasMap = this.aliasMap; - ctx.tableMap = this.tableMap; - - return ctx; - } -} - -module.exports = { - QueryBuilderContextBase, -}; diff --git a/src/queryBuilder/QueryBuilderContextBase.ts b/src/queryBuilder/QueryBuilderContextBase.ts index 0cb5e1a..3e573a1 100644 --- a/src/queryBuilder/QueryBuilderContextBase.ts +++ b/src/queryBuilder/QueryBuilderContextBase.ts @@ -1,16 +1,19 @@ import { Knex } from 'knex'; -import { nany } from '../ninja.ts'; import { InternalOptions } from './InternalOptions.ts'; import { QueryBuilderUserContext } from './QueryBuilderUserContext.ts'; +import { + IModel, + QueryBuilderOperationSupport, +} from './QueryBuilderOperationSupport.ts'; -export class QueryBuilderContextBase { - userContext?: QueryBuilderUserContext; +export class QueryBuilderContextBase { + userContext?: QueryBuilderUserContext; options?: InternalOptions; knex?: Knex; - aliasMap?: Map; - tableMap?: Map; + aliasMap?: Map; + tableMap?: Map; - constructor(builder?: nany) { + constructor(builder?: QueryBuilderOperationSupport) { this.userContext = builder ? new QueryBuilderUserContext(builder) : undefined; @@ -21,13 +24,16 @@ export class QueryBuilderContextBase { return InternalOptions; } - cloneInto( - newContext: QueryBuilderContextBase, - ): void { - newContext.userContext = this.userContext; - newContext.options = this.options?.clone(); - newContext.knex = this.knex; - newContext.aliasMap = this.aliasMap; - newContext.tableMap = this.tableMap; + clone(): QueryBuilderContextBase { + return this.cloneInto(new QueryBuilderContextBase()); + } + + cloneInto(context: QueryBuilderContextBase): QueryBuilderContextBase { + context.userContext = this.userContext; + context.options = this.options?.clone(); + context.knex = this.knex; + context.aliasMap = this.aliasMap; + context.tableMap = this.tableMap; + return context; } } diff --git a/src/queryBuilder/QueryBuilderOperationSupport.js b/src/queryBuilder/QueryBuilderOperationSupport.js deleted file mode 100644 index 35797b1..0000000 --- a/src/queryBuilder/QueryBuilderOperationSupport.js +++ /dev/null @@ -1,535 +0,0 @@ -'use strict'; - -const { isString, isFunction, isRegExp, mergeMaps, last } = require('../utils/objectUtils'); -const { QueryBuilderContextBase } = require('./QueryBuilderContextBase'); -const { QueryBuilderUserContext } = require('./QueryBuilderUserContext'); -const { deprecate } = require('../utils/deprecate'); - -const AllSelector = () => true; -const SelectSelector = - /^(select|columns|column|distinct|count|countDistinct|min|max|sum|sumDistinct|avg|avgDistinct)$/; -const WhereSelector = /^(where|orWhere|andWhere|find\w+)/; -const OnSelector = /^(on|orOn|andOn)/; -const OrderBySelector = /orderBy/; -const JoinSelector = /(join|joinRaw|joinRelated)$/i; -const FromSelector = /^(from|into|table)$/; - -class QueryBuilderOperationSupport { - constructor(...args) { - this.constructor.init(this, ...args); - } - - static init(self, modelClass) { - self._modelClass = modelClass; - self._operations = []; - self._context = new this.QueryBuilderContext(self); - self._parentQuery = null; - self._isPartialQuery = false; - self._activeOperations = []; - } - - static forClass(modelClass) { - return new this(modelClass); - } - - static get AllSelector() { - return AllSelector; - } - - static get QueryBuilderContext() { - return QueryBuilderContextBase; - } - - static get QueryBuilderUserContext() { - return QueryBuilderUserContext; - } - - static get SelectSelector() { - return SelectSelector; - } - - static get WhereSelector() { - return WhereSelector; - } - - static get OnSelector() { - return OnSelector; - } - - static get JoinSelector() { - return JoinSelector; - } - - static get FromSelector() { - return FromSelector; - } - - static get OrderBySelector() { - return OrderBySelector; - } - - modelClass() { - return this._modelClass; - } - - context(obj) { - const ctx = this._context; - - if (arguments.length === 0) { - return ctx.userContext; - } else { - ctx.userContext = ctx.userContext.newMerge(this, obj); - return this; - } - } - - clearContext() { - const ctx = this._context; - ctx.userContext = new this.constructor.QueryBuilderUserContext(this); - return this; - } - - internalContext(ctx) { - if (arguments.length === 0) { - return this._context; - } else { - this._context = ctx; - return this; - } - } - - internalOptions(opt) { - if (arguments.length === 0) { - return this._context.options; - } else { - const oldOpt = this._context.options; - this._context.options = Object.assign(oldOpt, opt); - return this; - } - } - - isPartial(isPartial) { - if (arguments.length === 0) { - return this._isPartialQuery; - } else { - this._isPartialQuery = isPartial; - return this; - } - } - - isInternal() { - return this.internalOptions().isInternalQuery; - } - - tableNameFor(tableName, newTableName) { - const ctx = this.internalContext(); - const tableMap = ctx.tableMap; - - if (isString(newTableName)) { - ctx.tableMap = tableMap || new Map(); - ctx.tableMap.set(tableName, newTableName); - return this; - } else { - return (tableMap && tableMap.get(tableName)) || tableName; - } - } - - aliasFor(tableName, alias) { - const ctx = this.internalContext(); - const aliasMap = ctx.aliasMap; - - if (isString(alias)) { - ctx.aliasMap = aliasMap || new Map(); - ctx.aliasMap.set(tableName, alias); - return this; - } else { - return (aliasMap && aliasMap.get(tableName)) || null; - } - } - - tableRefFor(tableName) { - return this.aliasFor(tableName) || this.tableNameFor(tableName); - } - - childQueryOf(query, { fork, isInternalQuery } = {}) { - if (query) { - let currentCtx = this.context(); - let ctx = query.internalContext(); - - if (fork) { - ctx = ctx.clone(); - } - - if (isInternalQuery) { - ctx.options.isInternalQuery = true; - } - - this._parentQuery = query; - this.internalContext(ctx); - this.context(currentCtx); - - // Use the parent's knex if there was no knex in `ctx`. - if (this.unsafeKnex() === null) { - this.knex(query.unsafeKnex()); - } - } - - return this; - } - - subqueryOf(query) { - if (query) { - if (this._isPartialQuery) { - // Merge alias and table name maps for "partial" subqueries. - const ctx = this.internalContext(); - - ctx.aliasMap = mergeMaps(query.internalContext().aliasMap, ctx.aliasMap); - ctx.tableMap = mergeMaps(query.internalContext().tableMap, ctx.tableMap); - } - - this._parentQuery = query; - - if (this.unsafeKnex() === null) { - this.knex(query.unsafeKnex()); - } - } - - return this; - } - - parentQuery() { - return this._parentQuery; - } - - knex(...args) { - if (args.length === 0) { - const knex = this.unsafeKnex(); - - if (!knex) { - throw new Error( - `no database connection available for a query. You need to bind the model class or the query to a knex instance.`, - ); - } - - return knex; - } else { - this._context.knex = args[0]; - return this; - } - } - - unsafeKnex() { - return this._context.knex || this._modelClass.knex() || null; - } - - clear(operationSelector) { - const operationsToRemove = new Set(); - - this.forEachOperation(operationSelector, (op) => { - // If an ancestor operation has already been removed, - // there's no need to remove the children anymore. - if (!op.isAncestorInSet(operationsToRemove)) { - operationsToRemove.add(op); - } - }); - - for (const op of operationsToRemove) { - this.removeOperation(op); - } - - return this; - } - - toFindQuery() { - const findQuery = this.clone(); - const operationsToReplace = []; - const operationsToRemove = []; - - findQuery.forEachOperation( - (op) => op.hasToFindOperation(), - (op) => { - const findOp = op.toFindOperation(findQuery); - - if (!findOp) { - operationsToRemove.push(op); - } else { - operationsToReplace.push({ op, findOp }); - } - }, - ); - - for (const op of operationsToRemove) { - findQuery.removeOperation(op); - } - - for (const { op, findOp } of operationsToReplace) { - findQuery.replaceOperation(op, findOp); - } - - return findQuery; - } - - clearSelect() { - return this.clear(SelectSelector); - } - - clearWhere() { - return this.clear(WhereSelector); - } - - clearOrder() { - return this.clear(OrderBySelector); - } - - copyFrom(queryBuilder, operationSelector) { - const operationsToAdd = new Set(); - - queryBuilder.forEachOperation(operationSelector, (op) => { - // If an ancestor operation has already been added, - // there is no need to add - if (!op.isAncestorInSet(operationsToAdd)) { - operationsToAdd.add(op); - } - }); - - for (const op of operationsToAdd) { - const opClone = op.clone(); - - // We may be moving nested operations to the root. Clear - // any links to the parent operations. - opClone.parentOperation = null; - opClone.adderHookName = null; - - // We don't use `addOperation` here because we don't what to - // call `onAdd` or add these operations as child operations. - this._operations.push(opClone); - } - - return this; - } - - has(operationSelector) { - return !!this.findOperation(operationSelector); - } - - forEachOperation(operationSelector, callback, match = true) { - const selector = buildFunctionForOperationSelector(operationSelector); - - for (const op of this._operations) { - if (selector(op) === match && callback(op) === false) { - break; - } - - const childRes = op.forEachDescendantOperation((op) => { - if (selector(op) === match && callback(op) === false) { - return false; - } - }); - - if (childRes === false) { - break; - } - } - - return this; - } - - findOperation(operationSelector) { - let op = null; - - this.forEachOperation(operationSelector, (it) => { - op = it; - return false; - }); - - return op; - } - - findLastOperation(operationSelector) { - let op = null; - - this.forEachOperation(operationSelector, (it) => { - op = it; - }); - - return op; - } - - everyOperation(operationSelector) { - let every = true; - - this.forEachOperation( - operationSelector, - () => { - every = false; - return false; - }, - false, - ); - - return every; - } - - callOperationMethod(operation, hookName, args) { - try { - operation.removeChildOperationsByHookName(hookName); - - this._activeOperations.push({ - operation, - hookName, - }); - - return operation[hookName](...args); - } finally { - this._activeOperations.pop(); - } - } - - async callAsyncOperationMethod(operation, hookName, args) { - operation.removeChildOperationsByHookName(hookName); - - this._activeOperations.push({ - operation, - hookName, - }); - - try { - return await operation[hookName](...args); - } finally { - this._activeOperations.pop(); - } - } - - addOperation(operation, args) { - const ret = this.addOperationUsingMethod('push', operation, args); - return ret; - } - - addOperationToFront(operation, args) { - return this.addOperationUsingMethod('unshift', operation, args); - } - - addOperationUsingMethod(arrayMethod, operation, args) { - const shouldAdd = this.callOperationMethod(operation, 'onAdd', [this, args]); - - if (shouldAdd) { - if (this._activeOperations.length) { - const { operation: parentOperation, hookName } = last(this._activeOperations); - parentOperation.addChildOperation(hookName, operation); - } else { - this._operations[arrayMethod](operation); - } - } - - return this; - } - - removeOperation(operation) { - if (operation.parentOperation) { - operation.parentOperation.removeChildOperation(operation); - } else { - const index = this._operations.indexOf(operation); - - if (index !== -1) { - this._operations.splice(index, 1); - } - } - - return this; - } - - replaceOperation(operation, newOperation) { - if (operation.parentOperation) { - operation.parentOperation.replaceChildOperation(operation, newOperation); - } else { - const index = this._operations.indexOf(operation); - - if (index !== -1) { - this._operations[index] = newOperation; - } - } - - return this; - } - - clone() { - return this.baseCloneInto(new this.constructor(this.unsafeKnex())); - } - - baseCloneInto(builder) { - builder._modelClass = this._modelClass; - builder._operations = this._operations.map((it) => it.clone()); - builder._context = this._context.clone(); - builder._parentQuery = this._parentQuery; - builder._isPartialQuery = this._isPartialQuery; - - // Don't copy the active operation stack. We never continue (nor can we) - // a query from the exact mid-hook-call state. - builder._activeOperations = []; - - return builder; - } - - toKnexQuery(knexBuilder = this.knex().queryBuilder()) { - this.executeOnBuild(); - return this.executeOnBuildKnex(knexBuilder); - } - - executeOnBuild() { - this.forEachOperation(true, (op) => { - if (op.hasOnBuild()) { - this.callOperationMethod(op, 'onBuild', [this]); - } - }); - } - - executeOnBuildKnex(knexBuilder) { - this.forEachOperation(true, (op) => { - if (op.hasOnBuildKnex()) { - const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [knexBuilder, this]); - // Default to the input knex builder for backwards compatibility - // with QueryBuilder.onBuildKnex hooks. - knexBuilder = newKnexBuilder || knexBuilder; - } - }); - - return knexBuilder; - } - - toString() { - return this.toKnexQuery().toString(); - } - - toSql() { - return this.toString(); - } - - skipUndefined() { - deprecate('skipUndefined() is deprecated and will be removed in objection 4.0'); - this.internalOptions().skipUndefined = true; - return this; - } -} - -function buildFunctionForOperationSelector(operationSelector) { - if (operationSelector === true) { - return AllSelector; - } else if (isRegExp(operationSelector)) { - return (op) => operationSelector.test(op.name); - } else if (isString(operationSelector)) { - return (op) => op.name === operationSelector; - } else if ( - isFunction(operationSelector) && - operationSelector.isObjectionQueryBuilderOperationClass - ) { - return (op) => op.is(operationSelector); - } else if (isFunction(operationSelector)) { - return operationSelector; - } else { - return () => false; - } -} - -module.exports = { - QueryBuilderOperationSupport, -}; diff --git a/src/queryBuilder/QueryBuilderOperationSupport.ts b/src/queryBuilder/QueryBuilderOperationSupport.ts new file mode 100644 index 0000000..a40d97c --- /dev/null +++ b/src/queryBuilder/QueryBuilderOperationSupport.ts @@ -0,0 +1,679 @@ +import { + isFunction, + isRegExp, + isString, + last, + mergeMaps, +} from '../utils/object.ts'; +import { QueryBuilderContextBase } from './QueryBuilderContextBase.ts'; +import { QueryBuilderUserContext } from './QueryBuilderUserContext.ts'; +import { deprecate } from '../utils/deprecate.ts'; +import { nany } from '../ninja.ts'; +import { InternalOptions } from './InternalOptions.ts'; +import { Knex } from 'knex'; +import { + HasAllHooks, + HasToFindOperation, + QueryBuilderOperation, +} from './operations/QueryBuilderOperation.ts'; + +const AllSelector: OperationSelector = () => true; +const SelectSelector: OperationSelector = + /^(select|columns|column|distinct|count|countDistinct|min|max|sum|sumDistinct|avg|avgDistinct)$/; +const WhereSelector: OperationSelector = /^(where|orWhere|andWhere|find\w+)/; +const OnSelector: OperationSelector = /^(on|orOn|andOn)/; +const OrderBySelector: OperationSelector = /orderBy/; +const JoinSelector: OperationSelector = /(join|joinRaw|joinRelated)$/i; +const FromSelector: OperationSelector = /^(from|into|table)$/; + +export interface IModel { + knex(): Knex | undefined; +} + +export type OperationSelector = + | boolean + | string + | RegExp + | QueryBuilderOperation + | ((op: QueryBuilderOperation) => boolean); + +export class QueryBuilderOperationSupport< + T extends IModel, +> { + #modelClass: T; + #operations: QueryBuilderOperation[]; + #context: QueryBuilderContextBase; + #parentQuery?: QueryBuilderOperationSupport; + #isPartialQuery: boolean; + #activeOperations: { + operation: QueryBuilderOperation; + hookName: keyof HasAllHooks; + }[]; + + constructor(modelClass: T) { + this.#modelClass = modelClass; + this.#operations = []; + this.#context = new QueryBuilderOperationSupport.QueryBuilderContext(this); + this.#isPartialQuery = false; + this.#activeOperations = []; + } + + static forClass(modelClass: T) { + return new this(modelClass); + } + + static get AllSelector() { + return AllSelector; + } + + static get QueryBuilderContext() { + return QueryBuilderContextBase; + } + + static get QueryBuilderUserContext() { + return QueryBuilderUserContext; + } + + static get SelectSelector() { + return SelectSelector; + } + + static get WhereSelector() { + return WhereSelector; + } + + static get OnSelector() { + return OnSelector; + } + + static get JoinSelector() { + return JoinSelector; + } + + static get FromSelector() { + return FromSelector; + } + + static get OrderBySelector() { + return OrderBySelector; + } + + modelClass(): T { + return this.#modelClass; + } + + context(): QueryBuilderUserContext | undefined; + context(obj: QueryBuilderUserContext): this; + context( + obj?: QueryBuilderUserContext, + ): QueryBuilderUserContext | undefined | this { + const ctx = this.#context; + + if (!obj) { + return ctx.userContext; + } else { + ctx.userContext = ctx.userContext?.newMerge(this, obj); + return this; + } + } + + clearContext() { + const ctx = this.#context; + ctx.userContext = new QueryBuilderOperationSupport.QueryBuilderUserContext( + this, + ); + return this; + } + + internalContext(): QueryBuilderContextBase; + internalContext(ctx: QueryBuilderContextBase): this; + internalContext( + ctx?: QueryBuilderContextBase, + ): QueryBuilderContextBase | this { + if (!ctx) { + return this.#context; + } else { + this.#context = ctx; + return this; + } + } + + internalOptions(): InternalOptions | undefined; + internalOptions(opt: InternalOptions): this; + internalOptions( + opt?: InternalOptions, + ): InternalOptions | undefined | this { + if (!opt) { + return this.#context.options; + } else { + const oldOpt = this.#context.options; + this.#context.options = Object.assign(oldOpt ?? {}, opt); + return this; + } + } + + isPartial(): boolean; + isPartial(isPartial: boolean): this; + isPartial(isPartial?: boolean): boolean | this { + if (isPartial === undefined) { + return this.#isPartialQuery; + } else { + this.#isPartialQuery = isPartial; + return this; + } + } + + isInternal(): boolean | undefined { + return this.internalOptions()?.isInternalQuery; + } + + tableNameFor(tableName: string): string; + tableNameFor(tableName: string, newTableName: string): this; + tableNameFor(tableName: string, newTableName?: string): string | this { + const ctx = this.internalContext(); + const tableMap = ctx.tableMap; + + if (isString(newTableName)) { + ctx.tableMap = tableMap || new Map(); + ctx.tableMap.set(tableName, newTableName); + return this; + } else { + return (tableMap && tableMap.get(tableName)) || tableName; + } + } + + aliasFor(tableName: string): string | undefined; + aliasFor(tableName: string, alias: string): QueryBuilderOperationSupport; + aliasFor( + tableName: string, + alias?: string, + ): string | QueryBuilderOperationSupport | undefined { + const ctx = this.internalContext(); + const aliasMap = ctx.aliasMap; + + if (isString(alias)) { + ctx.aliasMap = aliasMap || new Map(); + ctx.aliasMap.set(tableName, alias); + return this; + } else { + return (aliasMap && aliasMap.get(tableName)) || undefined; + } + } + + tableRefFor(tableName: string): string { + return this.aliasFor(tableName) || this.tableNameFor(tableName); + } + + /** + * Sets the current query as a child query of the provided query. + * + * @param query - The parent query to set as the current query's parent. + * @param options - Optional parameters for configuring the child query. + * @param options.fork - If true, creates a fork of the parent query's internal context. + * @param options.isInternalQuery - If true, marks the child query as an internal query. + * @returns The current QueryBuilderOperationSupport instance. + */ + childQueryOf( + query?: QueryBuilderOperationSupport, + { fork, isInternalQuery }: { fork?: boolean; isInternalQuery?: boolean } = + {}, + ): this { + if (query) { + const currentCtx = this.context(); + let ctx = query.internalContext(); + + if (fork) { + const newCtx = ctx.clone(); + ctx = newCtx; + } + + if (isInternalQuery && ctx.options) { + ctx.options.isInternalQuery = true; + } + + this.#parentQuery = query; + this.internalContext(ctx); + if (currentCtx) { + this.context(currentCtx); // TODO: Why is this needed? + } + + // Use the parent's knex if there was no knex in `ctx`. + if (!this.unsafeKnex() && query.unsafeKnex()) { + this.knex(query.unsafeKnex() as Knex); + } + } + + return this; + } + + /** + * Sets the current query as a subquery of the provided query. + * + * @param query - The query to set as the parent query. + * @returns The current instance of the QueryBuilderOperationSupport class. + */ + subqueryOf(query: QueryBuilderOperationSupport): this { + if (query) { + if (this.#isPartialQuery) { + // Merge alias and table name maps for "partial" subqueries. + const ctx = this.internalContext(); + const queryCtx = query.internalContext(); + + if (queryCtx.aliasMap) { + ctx.aliasMap = ctx.aliasMap + ? mergeMaps( + queryCtx.aliasMap, + ctx.aliasMap, + ) + : queryCtx.aliasMap; + } + if (queryCtx.tableMap) { + ctx.tableMap = ctx.tableMap + ? mergeMaps( + queryCtx.tableMap, + ctx.tableMap, + ) + : queryCtx.tableMap; + } + } + + this.#parentQuery = query; + + if (!this.unsafeKnex() && query.unsafeKnex()) { + this.knex(query.unsafeKnex() as Knex); + } + } + + return this; + } + + parentQuery(): QueryBuilderOperationSupport | undefined { + return this.#parentQuery; + } + + knex(): Knex; + knex(instance: Knex): this; + knex(instance?: Knex): Knex | this { + if (!instance) { + const knex = this.unsafeKnex(); + + if (!knex) { + throw new Error( + `no database connection available for a query. You need to bind the model class or the query to a knex instance.`, + ); + } + + return knex; + } else { + this.#context.knex = instance; + return this; + } + } + + unsafeKnex(): Knex | undefined { + return this.#context.knex || this.#modelClass.knex() || undefined; + } + + clear(operationSelector: OperationSelector): this { + const operationsToRemove = new Set(); + + this.forEachOperation(operationSelector, (op: QueryBuilderOperation) => { + // If an ancestor operation has already been removed, + // there's no need to remove the children anymore. + if (!op.isAncestorInSet(operationsToRemove)) { + operationsToRemove.add(op); + } + }); + + for (const op of operationsToRemove) { + this.removeOperation(op); + } + + return this; + } + + toFindQuery(): nany { // TODO: return QueryBuilder + const findQuery = this.clone(); + const operationsToReplace: { + op: QueryBuilderOperation; + findOp: QueryBuilderOperation; + }[] = []; + const operationsToRemove: QueryBuilderOperation[] = []; + + findQuery.forEachOperation( + (op: QueryBuilderOperation) => op.hasHook('toFindOperation'), + (op: QueryBuilderOperation) => { + const findOp = (op as QueryBuilderOperation & HasToFindOperation) + .toFindOperation(findQuery); + + if (!findOp) { + operationsToRemove.push(op); + } else { + operationsToReplace.push({ op, findOp }); + } + }, + ); + + for (const op of operationsToRemove) { + findQuery.removeOperation(op); + } + + for (const { op, findOp } of operationsToReplace) { + findQuery.replaceOperation(op, findOp); + } + + return findQuery; + } + + clearSelect(): this { + return this.clear(SelectSelector); + } + + clearWhere(): this { + return this.clear(WhereSelector); + } + + clearOrder(): this { + return this.clear(OrderBySelector); + } + + copyFrom( + queryBuilder: QueryBuilderOperationSupport, + operationSelector: OperationSelector, + ): this { + const operationsToAdd = new Set(); + + queryBuilder.forEachOperation( + operationSelector, + (op: QueryBuilderOperation) => { + // If an ancestor operation has already been added, + // there is no need to add + if (!op.isAncestorInSet(operationsToAdd)) { + operationsToAdd.add(op); + } + }, + ); + + for (const op of operationsToAdd) { + const opClone = op.clone(); + + // We may be moving nested operations to the root. Clear + // any links to the parent operations. + opClone.parentOperation = undefined; + opClone.adderHookName = undefined; + + // We don't use `addOperation` here because we don't what to + // call `onAdd` or add these operations as child operations. + this.#operations.push(opClone); + } + + return this; + } + + has(operationSelector: OperationSelector) { + return !!this.findOperation(operationSelector); + } + + forEachOperation( + operationSelector: OperationSelector, + callback: (op: QueryBuilderOperation) => boolean | void, + match: boolean = true, + ): boolean | this { + const selector = buildFunctionForOperationSelector(operationSelector); + + for (const op of this.#operations) { + if (selector(op) === match && callback(op) === false) { + break; + } + + const childRes = op.forEachDescendantOperation( + (op: QueryBuilderOperation) => { + if (selector(op) === match && callback(op) === false) { + return false; + } + }, + ); + + if (childRes === false) { + break; + } + } + + return this; + } + + findOperation( + operationSelector: OperationSelector, + ): QueryBuilderOperation | null { + let op = null; + + this.forEachOperation(operationSelector, (it) => { + op = it; + return false; + }); + + return op; + } + + findLastOperation( + operationSelector: OperationSelector, + ): QueryBuilderOperation | null { + let op = null; + + this.forEachOperation(operationSelector, (it) => { + op = it; + }); + + return op; + } + + everyOperation(operationSelector: OperationSelector): boolean { + let every = true; + + this.forEachOperation( + operationSelector, + () => { + every = false; + return false; + }, + false, + ); + + return every; + } + + callOperationMethod( + operation: QueryBuilderOperation, + hookName: keyof HasAllHooks, + ...args: nany[] + ) { + try { + operation.removeChildOperationsByHookName(hookName); + + this.#activeOperations.push({ + operation, + hookName, + }); + + // deno-lint-ignore no-explicit-any + return (operation as any)[hookName](...args); + } finally { + this.#activeOperations.pop(); + } + } + + async callAsyncOperationMethod( + operation: QueryBuilderOperation, + hookName: keyof HasAllHooks, + ...args: nany[] + ) { + operation.removeChildOperationsByHookName(hookName); + + this.#activeOperations.push({ + operation, + hookName, + }); + + try { + // deno-lint-ignore no-explicit-any + return await (operation as any)[hookName](...args); + } finally { + this.#activeOperations.pop(); + } + } + + addOperation(operation: QueryBuilderOperation, ...args: nany[]) { + const ret = this.addOperationUsingMethod('push', operation, args); + return ret; + } + + addOperationToFront(operation: QueryBuilderOperation, ...args: nany[]) { + return this.addOperationUsingMethod('unshift', operation, args); + } + + addOperationUsingMethod( + arrayMethod: keyof Array, + operation: QueryBuilderOperation, + ...args: nany[] + ): this { + const shouldAdd = this.callOperationMethod(operation, 'onAdd', [ + this, + args, + ]); + + if (shouldAdd) { + if (this.#activeOperations.length) { + const { operation: parentOperation, hookName } = last( + this.#activeOperations, + ); + parentOperation.addChildOperation(hookName, operation); + } else { + // deno-lint-ignore no-explicit-any + (this.#operations as any)[arrayMethod](operation); + } + } + + return this; + } + + removeOperation(operation: QueryBuilderOperation) { + if (operation.parentOperation) { + operation.parentOperation.removeChildOperation(operation); + } else { + const index = this.#operations.indexOf(operation); + + if (index !== -1) { + this.#operations.splice(index, 1); + } + } + + return this; + } + + replaceOperation( + operation: QueryBuilderOperation, + newOperation: QueryBuilderOperation, + ) { + if (operation.parentOperation) { + operation.parentOperation.replaceChildOperation(operation, newOperation); + } else { + const index = this.#operations.indexOf(operation); + + if (index !== -1) { + this.#operations[index] = newOperation; + } + } + + return this; + } + + clone() { + return this.baseCloneInto( + new QueryBuilderOperationSupport(this.#modelClass), + ); + } + + baseCloneInto(builder: QueryBuilderOperationSupport) { + builder.#modelClass = this.#modelClass; + builder.#operations = this.#operations.map((it) => it.clone()); + builder.#context = this.#context.clone(); + builder.#parentQuery = this.#parentQuery; + builder.#isPartialQuery = this.#isPartialQuery; + + // Don't copy the active operation stack. We never continue (nor can we) + // a query from the exact mid-hook-call state. + builder.#activeOperations = []; + + return builder; + } + + toKnexQuery(knexBuilder = this.knex().queryBuilder()) { + this.executeOnBuild(); + return this.executeOnBuildKnex(knexBuilder); + } + + executeOnBuild() { + this.forEachOperation(true, (op) => { + if (op.hasHook('onBuild')) { + this.callOperationMethod(op, 'onBuild', [this]); + } + }); + } + + executeOnBuildKnex(knexBuilder: Knex.QueryBuilder) { + this.forEachOperation(true, (op) => { + if (op.hasHook('onBuildKnex')) { + const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [ + knexBuilder, + this, + ]); + // Default to the input knex builder for backwards compatibility + // with QueryBuilder.onBuildKnex hooks. + knexBuilder = newKnexBuilder || knexBuilder; + } + }); + + return knexBuilder; + } + + toString() { + return this.toKnexQuery().toString(); + } + + toSql() { + return this.toString(); + } + + /** + * @deprecated skipUndefined() is deprecated and will be removed in objection 4.0 + */ + @deprecate('It will be removed in objection 4.0') + skipUndefined(): this { + const internalOptions = this.internalOptions(); + if (internalOptions) { + internalOptions.skipUndefined = true; + } + return this; + } +} + +function buildFunctionForOperationSelector( + operationSelector: OperationSelector, +): (op: QueryBuilderOperation) => boolean { + if (operationSelector === true) { + return AllSelector as (op: QueryBuilderOperation) => boolean; + } else if (isRegExp(operationSelector)) { + return (op: QueryBuilderOperation) => operationSelector.test(op.name ?? ''); + } else if (isString(operationSelector)) { + return (op: QueryBuilderOperation) => op.name === operationSelector; + } else if ( + isFunction(operationSelector) && + operationSelector instanceof QueryBuilderOperation + ) { + return (op: QueryBuilderOperation) => op.is(operationSelector); + } else if (isFunction(operationSelector)) { + return operationSelector as (op: QueryBuilderOperation) => boolean; + } else { + return () => false; + } +} diff --git a/src/queryBuilder/QueryBuilderUserContext.js b/src/queryBuilder/QueryBuilderUserContext.js deleted file mode 100644 index 7461096..0000000 --- a/src/queryBuilder/QueryBuilderUserContext.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const SYMBOL_BUILDER = Symbol(); - -class QueryBuilderUserContext { - constructor(builder) { - // This should never ever be accessed outside this class. We only - // store it so that we can access builder.knex() lazily. - this[SYMBOL_BUILDER] = builder; - } - - get transaction() { - return this[SYMBOL_BUILDER].knex(); - } - - newFromObject(builder, obj) { - const ctx = new this.constructor(builder); - Object.assign(ctx, obj); - return ctx; - } - - newMerge(builder, obj) { - const ctx = new this.constructor(builder); - Object.assign(ctx, this, obj); - return ctx; - } -} - -module.exports = { - QueryBuilderUserContext, -}; diff --git a/src/queryBuilder/QueryBuilderUserContext.ts b/src/queryBuilder/QueryBuilderUserContext.ts index 795fd43..e7a3f78 100644 --- a/src/queryBuilder/QueryBuilderUserContext.ts +++ b/src/queryBuilder/QueryBuilderUserContext.ts @@ -1,10 +1,13 @@ -import { nany } from '../ninja.ts'; import { Knex } from 'knex'; +import { + IModel, + QueryBuilderOperationSupport, +} from './QueryBuilderOperationSupport.ts'; -export class QueryBuilderUserContext { - #builder: nany; +export class QueryBuilderUserContext { + #builder: QueryBuilderOperationSupport; - constructor(builder: nany) { + constructor(builder: QueryBuilderOperationSupport) { this.#builder = builder; } @@ -12,13 +15,19 @@ export class QueryBuilderUserContext { return this.#builder.knex(); } - newFromObject(builder: nany, obj: unknown): QueryBuilderUserContext { + newFromObject( + builder: QueryBuilderOperationSupport, + obj: QueryBuilderUserContext, + ): QueryBuilderUserContext { const ctx = new QueryBuilderUserContext(builder); Object.assign(ctx, obj); return ctx; } - newMerge(builder: nany, obj: unknown): QueryBuilderUserContext { + newMerge( + builder: QueryBuilderOperationSupport, + obj: QueryBuilderUserContext, + ): QueryBuilderUserContext { const ctx = new QueryBuilderUserContext(builder); Object.assign(ctx, this, obj); return ctx; diff --git a/src/queryBuilder/RawBuilder.js b/src/queryBuilder/RawBuilder.js deleted file mode 100644 index 57a92d4..0000000 --- a/src/queryBuilder/RawBuilder.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -const { isPlainObject } = require('../utils/objectUtils'); -const { buildArg } = require('../utils/buildUtils'); - -class RawBuilder { - constructor(sql, args) { - this._sql = `${sql}`; - this._args = args; - this._as = null; - } - - get alias() { - return this._as; - } - - as(as) { - this._as = as; - return this; - } - - toKnexRaw(builder) { - let args = null; - let sql = this._sql; - - if (this._args.length === 1 && isPlainObject(this._args[0])) { - args = buildObject(this._args[0], builder); - - if (this._as) { - args.__alias__ = this._as; - sql += ' as :__alias__:'; - } - } else { - args = buildArray(this._args, builder); - - if (this._as) { - args.push(this._as); - sql += ' as ??'; - } - } - - return builder.knex().raw(sql, args); - } -} - -Object.defineProperties(RawBuilder.prototype, { - isObjectionRawBuilder: { - enumerable: false, - writable: false, - value: true, - }, -}); - -function buildArray(arr, builder) { - return arr.map((it) => buildArg(it, builder)); -} - -function buildObject(obj, builder) { - return Object.keys(obj).reduce((args, key) => { - args[key] = buildArg(obj[key], builder); - return args; - }, {}); -} - -function normalizeRawArgs(argsIn) { - const [sql, ...restArgs] = argsIn; - - if (restArgs.length === 1 && Array.isArray(restArgs[0])) { - return { - sql, - args: restArgs[0], - }; - } else { - return { - sql, - args: restArgs, - }; - } -} - -function raw(...argsIn) { - const { sql, args } = normalizeRawArgs(argsIn); - return new RawBuilder(sql, args); -} - -module.exports = { - RawBuilder, - normalizeRawArgs, - raw, -}; diff --git a/src/queryBuilder/ReferenceBuilder.js b/src/queryBuilder/ReferenceBuilder.js deleted file mode 100644 index 71bb9ff..0000000 --- a/src/queryBuilder/ReferenceBuilder.js +++ /dev/null @@ -1,252 +0,0 @@ -'use strict'; - -const { parseFieldExpression } = require('../utils/parseFieldExpression'); -const { isObject } = require('../utils/objectUtils'); - -class ReferenceBuilder { - constructor(expr) { - this._expr = expr; - this._parsedExpr = null; - this._column = null; - this._table = null; - this._cast = null; - this._toJson = false; - this._table = null; - this._alias = null; - this._modelClass = null; - - // This `if` makes it possible for `clone` to skip - // parsing the expression again. - if (expr !== null) { - this._parseExpression(expr); - } - } - - get parsedExpr() { - return this._parsedExpr; - } - - get column() { - return this._column; - } - - set column(column) { - this._column = column; - } - - get alias() { - return this._alias; - } - - set alias(alias) { - this._alias = alias; - } - - get tableName() { - return this._table; - } - - set tableName(table) { - this._table = table; - } - - get modelClass() { - return this._modelClass; - } - - set modelClass(modelClass) { - this._modelClass = modelClass; - } - - get isPlainColumnRef() { - return ( - (!this._parsedExpr || this._parsedExpr.access.length === 0) && !this._cast && !this._toJson - ); - } - - get expression() { - return this._expr; - } - - get cast() { - return this._cast; - } - - fullColumn(builder) { - const table = this.tableName - ? this.tableName - : this.modelClass - ? builder.tableRefFor(this.modelClass) - : null; - - if (table) { - return `${table}.${this.column}`; - } else { - return this.column; - } - } - - castText() { - return this.castTo('text'); - } - - castInt() { - return this.castTo('integer'); - } - - castBigInt() { - return this.castTo('bigint'); - } - - castFloat() { - return this.castTo('float'); - } - - castDecimal() { - return this.castTo('decimal'); - } - - castReal() { - return this.castTo('real'); - } - - castBool() { - return this.castTo('boolean'); - } - - castJson() { - this._toJson = true; - return this; - } - - castTo(sqlType) { - this._cast = sqlType; - return this; - } - - from(table) { - this._table = table; - return this; - } - - table(table) { - this._table = table; - return this; - } - - model(modelClass) { - this._modelClass = modelClass; - return this; - } - - as(alias) { - this._alias = alias; - return this; - } - - clone() { - const clone = new this.constructor(null); - - clone._expr = this._expr; - clone._parsedExpr = this._parsedExpr; - clone._column = this._column; - clone._table = this._table; - clone._cast = this._cast; - clone._toJson = this._toJson; - clone._alias = this._alias; - clone._modelClass = this._modelClass; - - return clone; - } - - toKnexRaw(builder) { - return builder.knex().raw(...this._createRawArgs(builder)); - } - - _parseExpression(expr) { - this._parsedExpr = parseFieldExpression(expr); - this._column = this._parsedExpr.column; - this._table = this._parsedExpr.table; - } - - _createRawArgs(builder) { - let bindings = []; - let sql = this._createReferenceSql(builder, bindings); - - sql = this._maybeCast(sql, bindings); - sql = this._maybeToJsonb(sql, bindings); - sql = this._maybeAlias(sql, bindings); - - return [sql, bindings]; - } - - _createReferenceSql(builder, bindings) { - bindings.push(this.fullColumn(builder)); - - if (this._parsedExpr.access.length > 0) { - const extractor = this._cast ? '#>>' : '#>'; - const jsonFieldRef = this._parsedExpr.access.map((field) => field.ref).join(','); - return `??${extractor}'{${jsonFieldRef}}'`; - } else { - return '??'; - } - } - - _maybeCast(sql) { - if (this._cast) { - return `CAST(${sql} AS ${this._cast})`; - } else { - return sql; - } - } - - _maybeToJsonb(sql) { - if (this._toJson) { - return `to_jsonb(${sql})`; - } else { - return sql; - } - } - - _maybeAlias(sql, bindings) { - if (this._shouldAlias()) { - bindings.push(this._alias); - return `${sql} as ??`; - } else { - return sql; - } - } - - _shouldAlias() { - if (!this._alias) { - return false; - } else if (!this.isPlainColumnRef) { - return true; - } else { - // No need to alias if we are dealing with a simple column reference - // and the alias is the same as the column name. - return this._alias !== this._column; - } - } -} - -Object.defineProperties(ReferenceBuilder.prototype, { - isObjectionReferenceBuilder: { - enumerable: false, - writable: false, - value: true, - }, -}); - -const ref = (reference) => { - if (isObject(reference) && reference.isObjectionReferenceBuilder) { - return reference; - } else { - return new ReferenceBuilder(reference); - } -}; - -module.exports = { - ReferenceBuilder, - ref, -}; diff --git a/src/queryBuilder/ReferenceBuilder.ts b/src/queryBuilder/ReferenceBuilder.ts new file mode 100644 index 0000000..64480a1 --- /dev/null +++ b/src/queryBuilder/ReferenceBuilder.ts @@ -0,0 +1,249 @@ +import { + ParsedExpression, + parseFieldExpression, +} from '../utils/parseFieldExpression.ts'; +import { isObject } from '../utils/object.ts'; +import { IModel } from './QueryBuilderOperationSupport.ts'; +import { nany } from '../ninja.ts'; +import { Knex } from 'knex'; + +class ReferenceBuilder { + #expr: string; + #parsedExpr?: ParsedExpression; + #column?: string; + #table?: string; + #cast?: string; + #toJson: boolean; + #alias?: string; + #modelClass?: T; + + constructor(expr: string) { + this.#expr = expr; + this.#toJson = false; + + // This `if` makes it possible for `clone` to skip + // parsing the expression again. + if (expr !== null) { + this.#parseExpression(expr); + } + } + + get parsedExpr(): ParsedExpression | undefined { + return this.#parsedExpr; + } + + get column(): string | undefined { + return this.#column; + } + + set column(column: string) { + this.#column = column; + } + + get alias(): string | undefined { + return this.#alias; + } + + set alias(alias: string) { + this.#alias = alias; + } + + get tableName(): string | undefined { + return this.#table; + } + + set tableName(table: string) { + this.#table = table; + } + + get modelClass(): T | undefined { + return this.#modelClass; + } + + set modelClass(modelClass: T) { + this.#modelClass = modelClass; + } + + get isPlainColumnRef() { + return ( + (!this.#parsedExpr || this.#parsedExpr.access.length === 0) && + !this.#cast && !this.#toJson + ); + } + + get expression(): string { + return this.#expr; + } + + get cast(): string | undefined { + return this.#cast; + } + + fullColumn(builder: nany): string | undefined { // TODO: type + const table = this.tableName + ? this.tableName + : this.modelClass + ? builder.tableRefFor(this.modelClass) + : null; + + if (table) { + return `${table}.${this.column}`; + } else { + return this.column; + } + } + + castText(): this { + return this.castTo('text'); + } + + castInt(): this { + return this.castTo('integer'); + } + + castBigInt(): this { + return this.castTo('bigint'); + } + + castFloat(): this { + return this.castTo('float'); + } + + castDecimal(): this { + return this.castTo('decimal'); + } + + castReal(): this { + return this.castTo('real'); + } + + castBool(): this { + return this.castTo('boolean'); + } + + castJson(): this { + this.#toJson = true; + return this; + } + + castTo(sqlType: string): this { + this.#cast = sqlType; + return this; + } + + from(table: string): this { + this.#table = table; + return this; + } + + table(table: string): this { + this.#table = table; + return this; + } + + model(modelClass: T): this { + this.#modelClass = modelClass; + return this; + } + + as(alias: string): this { + this.#alias = alias; + return this; + } + + clone() { + const clone = new ReferenceBuilder(''); + + clone.#expr = this.#expr; + clone.#parsedExpr = this.#parsedExpr; + clone.#column = this.#column; + clone.#table = this.#table; + clone.#cast = this.#cast; + clone.#toJson = this.#toJson; + clone.#alias = this.#alias; + clone.#modelClass = this.#modelClass; + + return clone; + } + + toKnexRaw(builder: nany): Knex.RawBuilder { // TODO: type + return builder.knex().raw(...this.#createRawArgs(builder)); + } + + #parseExpression(expr: string) { + this.#parsedExpr = parseFieldExpression(expr); + this.#column = this.#parsedExpr.column; + this.#table = this.#parsedExpr.table; + } + + #createRawArgs(builder: nany): [string, nany[]] { // TODO: type + const bindings: nany[] = []; + let sql = this.#createReferenceSql(builder, bindings); + + sql = this.#maybeCast(sql); + sql = this.#maybeToJsonb(sql); + sql = this.#maybeAlias(sql, bindings); + + return [sql, bindings]; + } + + #createReferenceSql(builder: nany, bindings: nany[]) { // TODO: type + bindings.push(this.fullColumn(builder)); + + if (this.#parsedExpr?.access.length) { + const extractor = this.#cast ? '#>>' : '#>'; + const jsonFieldRef = this.#parsedExpr.access.map((field) => field.ref) + .join(','); + return `??${extractor}'{${jsonFieldRef}}'`; + } else { + return '??'; + } + } + + #maybeCast(sql: string): string { + if (this.#cast) { + return `CAST(${sql} AS ${this.#cast})`; + } else { + return sql; + } + } + + #maybeToJsonb(sql: string): string { + if (this.#toJson) { + return `to_jsonb(${sql})`; + } else { + return sql; + } + } + + #maybeAlias(sql: string, bindings: nany[]): string { + if (this.#shouldAlias()) { + bindings.push(this.#alias); + return `${sql} as ??`; + } else { + return sql; + } + } + + #shouldAlias() { + if (!this.#alias) { + return false; + } else if (!this.isPlainColumnRef) { + return true; + } else { + // No need to alias if we are dealing with a simple column reference + // and the alias is the same as the column name. + return this.#alias !== this.#column; + } + } +} + +export function ref( + reference: ReferenceBuilder | string, +): ReferenceBuilder { + if (isObject(reference) && reference instanceof ReferenceBuilder) { + return reference; + } else { + return new ReferenceBuilder(reference as string); + } +} diff --git a/src/queryBuilder/StaticHookArguments.js b/src/queryBuilder/StaticHookArguments.ts similarity index 67% rename from src/queryBuilder/StaticHookArguments.js rename to src/queryBuilder/StaticHookArguments.ts index 5bc47df..292f4b5 100644 --- a/src/queryBuilder/StaticHookArguments.js +++ b/src/queryBuilder/StaticHookArguments.ts @@ -1,30 +1,34 @@ -'use strict'; - -const { asArray } = require('../utils/objectUtils'); +import { nany } from '../ninja.ts'; +import { asArray } from '../utils/object.ts'; +import { QueryBuilderOperationSupport } from './QueryBuilderOperationSupport.ts'; const BUILDER_SYMBOL = Symbol(); -class StaticHookArguments { - constructor({ builder, result = null }) { +export class StaticHookArguments { + [BUILDER_SYMBOL]: nany; // TODO: nany is not a valid type + result?: nany[]; + + constructor({ builder, result }: { + builder: nany; + result?: nany; + }) { // The builder should never be accessed through the arguments. // Hide it as well as possible to discourage people from // digging it out. - Object.defineProperty(this, BUILDER_SYMBOL, { - value: builder, - }); - - Object.defineProperty(this, 'result', { - value: asArray(result), - }); + this[BUILDER_SYMBOL] = builder; + this.result = asArray(result); } - static create(args) { + static create( + args: { builder: nany; result?: nany }, + ) { return new StaticHookArguments(args); } get asFindQuery() { return () => { - return this[BUILDER_SYMBOL].toFindQuery().clearWithGraphFetched().runAfter(asArray); + return this[BUILDER_SYMBOL].toFindQuery().clearWithGraphFetched() + .runAfter(asArray); }; } @@ -79,7 +83,7 @@ class StaticHookArguments { get cancelQuery() { const args = this; - return (cancelValue) => { + return (cancelValue: nany) => { // TODO: nany is not a valid type const builder = this[BUILDER_SYMBOL]; if (cancelValue === undefined) { @@ -97,38 +101,34 @@ class StaticHookArguments { } } -function getRelation(op) { +function getRelation(op: nany) { // TODO: nany is not a valid type return op.relation; } -function hasRelation(op) { +function hasRelation(op: nany) { return !!getRelation(op); } -function getModelOptions(op) { +function getModelOptions(op: nany) { return op.modelOptions; } -function hasModelOptions(op) { +function hasModelOptions(op: nany) { return !!getModelOptions(op); } -function getItems(op) { +function getItems(op: nany) { return op.instance || (op.owner && op.owner.isModels && op.owner.modelArray); } -function hasItems(op) { +function hasItems(op: nany) { return !!getItems(op); } -function getInputItems(op) { +function getInputItems(op: nany) { return op.models || op.model; } -function hasInputItems(op) { +function hasInputItems(op: nany) { return !!getInputItems(op); } - -module.exports = { - StaticHookArguments, -}; diff --git a/src/queryBuilder/ValueBuilder.js b/src/queryBuilder/ValueBuilder.js deleted file mode 100644 index 4e95f83..0000000 --- a/src/queryBuilder/ValueBuilder.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -const { asArray, isObject } = require('../utils/objectUtils'); -const { buildArg } = require('../utils/buildUtils'); - -class ValueBuilder { - constructor(value) { - this._value = value; - this._cast = null; - // Cast objects and arrays to json by default. - this._toJson = isObject(value); - this._toArray = false; - this._alias = null; - } - - get cast() { - return this._cast; - } - - castText() { - return this.castTo('text'); - } - - castInt() { - return this.castTo('integer'); - } - - castBigInt() { - return this.castTo('bigint'); - } - - castFloat() { - return this.castTo('float'); - } - - castDecimal() { - return this.castTo('decimal'); - } - - castReal() { - return this.castTo('real'); - } - - castBool() { - return this.castTo('boolean'); - } - - castJson() { - this._toArray = false; - this._toJson = true; - this._cast = 'jsonb'; - return this; - } - - castTo(sqlType) { - this._cast = sqlType; - return this; - } - - asArray() { - this._toJson = false; - this._toArray = true; - return this; - } - - as(alias) { - this._alias = alias; - return this; - } - - toKnexRaw(builder) { - return builder.knex().raw(...this._createRawArgs(builder)); - } - - _createRawArgs(builder) { - let sql = null; - let bindings = []; - - if (this._toJson) { - bindings.push(JSON.stringify(this._value)); - sql = '?'; - } else if (this._toArray) { - const values = asArray(this._value); - bindings.push(...values.map((it) => buildArg(it, builder))); - sql = `ARRAY[${values.map(() => '?').join(', ')}]`; - } else { - bindings.push(this._value); - sql = '?'; - } - - if (this._cast) { - sql = `CAST(${sql} AS ${this._cast})`; - } - - if (this._alias) { - bindings.push(this._alias); - sql = `${sql} as ??`; - } - - return [sql, bindings]; - } -} - -function val(val) { - return new ValueBuilder(val); -} - -module.exports = { - ValueBuilder, - val, -}; diff --git a/src/queryBuilder/ValueBuilder.ts b/src/queryBuilder/ValueBuilder.ts new file mode 100644 index 0000000..0a20648 --- /dev/null +++ b/src/queryBuilder/ValueBuilder.ts @@ -0,0 +1,143 @@ +import { asArray, isObject } from '../utils/object.ts'; +import { buildArg } from '../utils/build.ts'; +import { Knex } from 'knex'; +import { nany } from '../ninja.ts'; + +type PrimitiveValue = + | string + | number + | boolean + | Date + | Deno.Buffer + | string[] + | number[] + | boolean[] + | Date[] + | Deno.Buffer[] + | null; + +interface PrimitiveValueObject { + [key: string]: PrimitiveValue; +} + +export type AnyValue = + | PrimitiveValue + | PrimitiveValue[] + | PrimitiveValueObject + | PrimitiveValueObject[]; + +export class ValueBuilder { + #value: AnyValue; + #cast?: string; + #toJson: boolean; + #toArray: boolean; + #alias?: string; + + constructor( + value: AnyValue, + ) { + this.#value = value; + // Cast objects and arrays to json by default. + this.#toJson = isObject(value); + this.#toArray = false; + } + + get cast() { + return this.#cast; + } + + castText(): this { + return this.castTo('text'); + } + + castInt(): this { + return this.castTo('integer'); + } + + castBigInt(): this { + return this.castTo('bigint'); + } + + castFloat(): this { + return this.castTo('float'); + } + + castDecimal(): this { + return this.castTo('decimal'); + } + + castReal(): this { + return this.castTo('real'); + } + + castBool(): this { + return this.castTo('boolean'); + } + + castJson(): this { + this.#toArray = false; + this.#toJson = true; + this.#cast = 'jsonb'; + return this; + } + + castTo(sqlType: string): this { + this.#cast = sqlType; + return this; + } + + asArray(): this { + this.#toJson = false; + this.#toArray = true; + return this; + } + + as(alias: string): this { + this.#alias = alias; + return this; + } + + toKnexRaw(builder: nany): Knex.RawBuilder { // TODO: Use QueryBuilder type + return builder.knex().raw(...this.#createRawArgs(builder)); + } + + // deno-lint-ignore no-explicit-any + #createRawArgs(builder: nany): [string, any[]] { + // deno-lint-ignore no-explicit-any + const bindings: any[] = []; + let sql: string | null = null; + + if (this.#toJson) { + bindings.push(JSON.stringify(this.#value)); + sql = '?'; + } else if (this.#toArray) { + const values = asArray(this.#value as PrimitiveValue[]); + bindings.push(...values.map((it) => buildArg(it, builder))); + sql = `ARRAY[${values.map(() => '?').join(', ')}]`; + } else { + bindings.push(this.#value); + sql = '?'; + } + + if (this.#cast) { + sql = `CAST(${sql} AS ${this.#cast})`; + } + + if (this.#alias) { + bindings.push(this.#alias); + sql = `${sql} as ??`; + } + + return [sql, bindings]; + } +} + +/** + * Creates a new `ValueBuilder` instance with the specified value. + * + * @param val - The value to be used in the `ValueBuilder`. + * @returns A new `ValueBuilder` instance. + */ +export function val(val: AnyValue) { + return new ValueBuilder(val); +} diff --git a/src/queryBuilder/operations/DelegateOperation.js b/src/queryBuilder/operations/DelegateOperation.js deleted file mode 100644 index 2440166..0000000 --- a/src/queryBuilder/operations/DelegateOperation.js +++ /dev/null @@ -1,137 +0,0 @@ -'use strict'; - -const { QueryBuilderOperation } = require('./QueryBuilderOperation'); - -// Operation that simply delegates all calls to the operation passed -// to to the constructor in `opt.delegate`. -class DelegateOperation extends QueryBuilderOperation { - constructor(name, opt) { - super(name, opt); - - this.delegate = opt.delegate; - } - - is(OperationClass) { - return super.is(OperationClass) || this.delegate.is(OperationClass); - } - - onAdd(builder, args) { - return this.delegate.onAdd(builder, args); - } - - onBefore1(builder, result) { - return this.delegate.onBefore1(builder, result); - } - - hasOnBefore1() { - return this.onBefore1 !== DelegateOperation.prototype.onBefore1 || this.delegate.hasOnBefore1(); - } - - onBefore2(builder, result) { - return this.delegate.onBefore2(builder, result); - } - - hasOnBefore2() { - return this.onBefore2 !== DelegateOperation.prototype.onBefore2 || this.delegate.hasOnBefore2(); - } - - onBefore3(builder, result) { - return this.delegate.onBefore3(builder, result); - } - - hasOnBefore3() { - return this.onBefore3 !== DelegateOperation.prototype.onBefore3 || this.delegate.hasOnBefore3(); - } - - onBuild(builder) { - return this.delegate.onBuild(builder); - } - - hasOnBuild() { - return this.onBuild !== DelegateOperation.prototype.onBuild || this.delegate.hasOnBuild(); - } - - onBuildKnex(knexBuilder, builder) { - return this.delegate.onBuildKnex(knexBuilder, builder); - } - - hasOnBuildKnex() { - return ( - this.onBuildKnex !== DelegateOperation.prototype.onBuildKnex || this.delegate.hasOnBuildKnex() - ); - } - - onRawResult(builder, result) { - return this.delegate.onRawResult(builder, result); - } - - hasOnRawResult() { - return ( - this.onRawResult !== DelegateOperation.prototype.onRawResult || this.delegate.hasOnRawResult() - ); - } - - onAfter1(builder, result) { - return this.delegate.onAfter1(builder, result); - } - - hasOnAfter1() { - return this.onAfter1 !== DelegateOperation.prototype.onAfter1 || this.delegate.hasOnAfter1(); - } - - onAfter2(builder, result) { - return this.delegate.onAfter2(builder, result); - } - - hasOnAfter2() { - return this.onAfter2 !== DelegateOperation.prototype.onAfter2 || this.delegate.hasOnAfter2(); - } - - onAfter3(builder, result) { - return this.delegate.onAfter3(builder, result); - } - - hasOnAfter3() { - return this.onAfter3 !== DelegateOperation.prototype.onAfter3 || this.delegate.hasOnAfter3(); - } - - queryExecutor(builder) { - return this.delegate.queryExecutor(builder); - } - - hasQueryExecutor() { - return ( - this.queryExecutor !== DelegateOperation.prototype.queryExecutor || - this.delegate.hasQueryExecutor() - ); - } - - onError(builder, error) { - return this.delegate.onError(builder, error); - } - - hasOnError() { - return this.onError !== DelegateOperation.prototype.onError || this.delegate.hasOnError(); - } - - toFindOperation(builder) { - return this.delegate.toFindOperation(builder); - } - - hasToFindOperation() { - return ( - this.hasToFindOperation !== DelegateOperation.prototype.hasToFindOperation || - this.delegate.hasToFindOperation() - ); - } - - clone() { - const clone = super.clone(); - clone.delegate = this.delegate && this.delegate.clone(); - return clone; - } -} - -module.exports = { - DelegateOperation, -}; diff --git a/src/queryBuilder/operations/DelegateOperation.ts b/src/queryBuilder/operations/DelegateOperation.ts new file mode 100644 index 0000000..718a1ab --- /dev/null +++ b/src/queryBuilder/operations/DelegateOperation.ts @@ -0,0 +1,139 @@ +import { Knex } from 'knex'; +import { nany } from '../../ninja.ts'; +import { + HasAllHooks, + HasOnAdd, + HasOnAfter1, + HasOnAfter2, + HasOnAfter3, + HasOnBefore1, + HasOnBefore2, + HasOnBefore3, + HasOnBuild, + HasOnBuildKnex, + HasOnError, + HasOnRawResult, + HasQueryExecutor, + HasToFindOperation, + QueryBuilderOperation, +} from './QueryBuilderOperation.ts'; + +// Operation that simply delegates all calls to the operation passed +// to to the constructor in `opt.delegate`. +export class DelegateOperation extends QueryBuilderOperation + implements HasAllHooks { + delegate: QueryBuilderOperation; + + constructor( + name: string | undefined, + opt: { delegate: QueryBuilderOperation }, + ) { + super(name, opt); + + this.delegate = opt.delegate; + } + + override is( + OperationClass: Function, + ): this is T { + return super.is(OperationClass) || this.delegate.is(OperationClass); + } + + override hasHook(hookName: keyof HasAllHooks): boolean { + return this[hookName] !== DelegateOperation.prototype[hookName] || + this.delegate.hasHook(hookName); + } + + onAdd(builder: nany, ...args: nany[]): nany { + return (this.delegate as QueryBuilderOperation & HasOnAdd).onAdd( + builder, + args, + ); + } + + onBefore1(builder: nany, result: nany) { + return (this.delegate as QueryBuilderOperation & HasOnBefore1).onBefore1( + builder, + result, + ); + } + + onBefore2(builder: nany, result: nany) { + return (this.delegate as QueryBuilderOperation & HasOnBefore2).onBefore2( + builder, + result, + ); + } + + onBefore3(builder: nany, result: nany) { + return (this.delegate as QueryBuilderOperation & HasOnBefore3).onBefore3( + builder, + result, + ); + } + + onBuild(builder: nany): nany { + return (this.delegate as QueryBuilderOperation & HasOnBuild).onBuild( + builder, + ); + } + + onBuildKnex(knexBuilder: Knex.QueryBuilder, builder: nany): nany { + return (this.delegate as QueryBuilderOperation & HasOnBuildKnex) + .onBuildKnex(knexBuilder, builder); + } + + onRawResult(builder: nany, result: nany): nany { + return (this.delegate as QueryBuilderOperation & HasOnRawResult) + .onRawResult(builder, result); + } + + onAfter1(builder: nany, result: nany): nany { + return (this.delegate as QueryBuilderOperation & HasOnAfter1).onAfter1( + builder, + result, + ); + } + + onAfter2(builder: nany, result: nany): nany { + return (this.delegate as QueryBuilderOperation & HasOnAfter2).onAfter2( + builder, + result, + ); + } + + onAfter3(builder: nany, result: nany): nany { + return (this.delegate as QueryBuilderOperation & HasOnAfter3).onAfter3( + builder, + result, + ); + } + + queryExecutor(builder: nany): nany { + return (this.delegate as QueryBuilderOperation & HasQueryExecutor) + .queryExecutor(builder); + } + + onError(builder: nany, error: Error): nany { + return (this.delegate as QueryBuilderOperation & HasOnError).onError( + builder, + error, + ); + } + + toFindOperation(builder: nany): nany { + return (this.delegate as QueryBuilderOperation & HasToFindOperation) + .toFindOperation(builder); + } + + override clone() { + const clone = new DelegateOperation(this.name, { delegate: this.delegate }); + return this.cloneInto(clone); + } + + override cloneInto(clone: DelegateOperation): DelegateOperation { + super.cloneInto(clone); + clone.delegate = this.delegate && this.delegate.clone(); + return clone; + } +} diff --git a/src/queryBuilder/operations/QueryBuilderOperation.js b/src/queryBuilder/operations/QueryBuilderOperation.ts similarity index 58% rename from src/queryBuilder/operations/QueryBuilderOperation.js rename to src/queryBuilder/operations/QueryBuilderOperation.ts index 4bc833f..ea56216 100644 --- a/src/queryBuilder/operations/QueryBuilderOperation.js +++ b/src/queryBuilder/operations/QueryBuilderOperation.ts @@ -1,97 +1,55 @@ -'use strict'; - -const hookNameToHasMethodName = { - onAdd: 'hasOnAdd', - onBefore1: 'hasOnBefore1', - onBefore2: 'hasOnBefore2', - onBefore3: 'hasOnBefore3', - onBuild: 'hasOnBuild', - onBuildKnex: 'hasOnBuildKnex', - onRawResult: 'hasOnRawResult', - queryExecutor: 'hasQueryExecutor', - onAfter1: 'hasOnAfter1', - onAfter2: 'hasOnAfter2', - onAfter3: 'hasOnAfter3', - onError: 'hasOnError', -}; - -// An abstract base class for all query builder operations. QueryBuilderOperations almost always -// correspond to a single query builder method call. For example SelectOperation could be added when -// a `select` method is called. -// -// QueryBuilderOperation is just a bunch of query execution lifecycle hooks that subclasses -// can (but don't have to) implement. -// -// Basically a query builder is nothing but an array of QueryBuilderOperations. When the query is -// executed the hooks are called in the order explained below. The hooks are called so that a -// certain hook is called for _all_ operations before the next hook is called. For example if -// a builder has 5 operations, onBefore1 hook is called for each of them (and their results are awaited) -// before onBefore2 hook is called for any of the operations. -class QueryBuilderOperation { - constructor(name = null, opt = {}) { - this.name = name; - this.opt = opt; - - // From which hook was this operation added as a child - // operation. - this.adderHookName = null; - - // The parent operation that added this operation. - this.parentOperation = null; - - // Operations this operation added in any of its hooks. - this.childOperations = []; - } - - is(OperationClass) { - return this instanceof OperationClass; - } - - hasHook(hookName) { - return this[hookNameToHasMethodName[hookName]](); - } +import { nany } from '../../ninja.ts'; +import { QueryBuilderOperationSupport } from '../QueryBuilderOperationSupport.ts'; +import { knex } from 'knex'; +export interface HasOnAdd { // This is called immediately when a query builder method is called. // // This method must be synchronous. // This method should never call any methods that add operations to the builder. - onAdd(builder, args) { - return true; - } - hasOnAdd() { - return true; - } + onAdd( + builder: QueryBuilderOperationSupport, + ...args: nany[] + ): boolean; +} +export interface HasOnBefore1 { // This is called as the first thing when the query is executed but before // the actual database operation (knex query) is executed. // // This method can be asynchronous. // You may call methods that add operations to to the builder. - onBefore1(builder, result) {} - hasOnBefore1() { - return this.onBefore1 !== QueryBuilderOperation.prototype.onBefore1; - } + onBefore1( + builder: QueryBuilderOperationSupport, + result: unknown, + ): unknown | Promise; +} +export interface HasOnBefore2 { // This is called as the second thing when the query is executed but before // the actual database operation (knex query) is executed. // // This method can be asynchronous. // You may call methods that add operations to to the builder. - onBefore2(builder, result) {} - hasOnBefore2() { - return this.onBefore2 !== QueryBuilderOperation.prototype.onBefore2; - } + onBefore2( + builder: QueryBuilderOperationSupport, + result: unknown, + ): unknown | Promise; +} +export interface HasOnBefore3 { // This is called as the third thing when the query is executed but before // the actual database operation (knex query) is executed. // // This method can be asynchronous. // You may call methods that add operations to to the builder. - onBefore3(builder, result) {} - hasOnBefore3() { - return this.onBefore3 !== QueryBuilderOperation.prototype.onBefore3; - } + onBefore3( + builder: QueryBuilderOperationSupport, + result: unknown, + ): unknown | Promise; +} +export interface HasOnBuild { // This is called as the last thing when the query is executed but before // the actual database operation (knex query) is executed. If your operation // needs to call other query building operations (methods that add QueryBuilderOperations) @@ -99,11 +57,10 @@ class QueryBuilderOperation { // // This method must be synchronous. // You may call methods that add operations to to the builder. - onBuild(builder) {} - hasOnBuild() { - return this.onBuild !== QueryBuilderOperation.prototype.onBuild; - } + onBuild(builder: QueryBuilderOperationSupport): void; +} +export interface HasOnBuildKnex { // This is called when the knex query is built. Here you should only call knex // methods. You may call getters and other immutable methods of the `builder` // but you should never call methods that add QueryBuilderOperations. @@ -111,100 +68,162 @@ class QueryBuilderOperation { // This method must be synchronous. // This method should never call any methods that add operations to the builder. // This method should always return the knex query builder. - onBuildKnex(knexBuilder, builder) { - return knexBuilder; - } - hasOnBuildKnex() { - return this.onBuildKnex !== QueryBuilderOperation.prototype.onBuildKnex; - } + onBuildKnex( + knexBuilder: knex.QueryBuilder, + builder: QueryBuilderOperationSupport, + ): knex.QueryBuilder; +} +export interface HasOnRawResult { // The raw knex result is passed to this method right after the database query // has finished. This method may modify it and return the modified rows. The // rows are automatically converted to models (if possible) after this hook // is called. // // This method can be asynchronous. - onRawResult(builder, rows) { - return rows; - } - hasOnRawResult() { - return this.onRawResult !== QueryBuilderOperation.prototype.onRawResult; - } + onRawResult( + builder: QueryBuilderOperationSupport, + rows: unknown[], + ): unknown | Promise; +} +export interface HasOnAfter1 { // This is called as the first thing after the query has been executed and // rows have been converted to model instances. // // This method can be asynchronous. - onAfter1(builder, result) { - return result; - } - hasOnAfter1() { - return this.onAfter1 !== QueryBuilderOperation.prototype.onAfter1; - } + onAfter1( + builder: QueryBuilderOperationSupport, + result: unknown, + ): unknown | Promise; +} +export interface HasOnAfter2 { // This is called as the second thing after the query has been executed and // rows have been converted to model instances. // // This method can be asynchronous. - onAfter2(builder, result) { - return result; - } - hasOnAfter2() { - return this.onAfter2 !== QueryBuilderOperation.prototype.onAfter2; - } + onAfter2( + builder: QueryBuilderOperationSupport, + result: unknown, + ): unknown | Promise; +} +export interface HasOnAfter3 { // This is called as the third thing after the query has been executed and // rows have been converted to model instances. // // This method can be asynchronous. - onAfter3(builder, result) { - return result; - } - hasOnAfter3() { - return this.onAfter3 !== QueryBuilderOperation.prototype.onAfter3; - } + onAfter3( + builder: QueryBuilderOperationSupport, + result: unknown, + ): unknown | Promise; +} - // This method can be implemented to return another operation that will replace - // this one. This method is called after all `onBeforeX` and `onBuildX` hooks - // but before the database query is executed. +export interface HasQueryExecutor { + // This is called to execute the query and return the result. // - // This method must return a QueryBuilder instance. - queryExecutor(builder) {} - hasQueryExecutor() { - return this.queryExecutor !== QueryBuilderOperation.prototype.queryExecutor; - } + // This method can be asynchronous. + // You should call the appropriate method on the `builder` to execute the query. + queryExecutor( + builder: QueryBuilderOperationSupport, + ): unknown | Promise; +} +export interface HasOnError { // This is called if an error occurs in the query execution. // // This method must return a QueryBuilder instance. - onError(builder, error) {} - hasOnError() { - return this.onError !== QueryBuilderOperation.prototype.onError; - } + onError(builder: QueryBuilderOperationSupport, error: Error): void; +} +export interface HasToFindOperation { // Returns the "find" equivalent of this operation. // // For example an operation that finds an item and updates it // should return an operation that simply finds the item but // doesn't update anything. An insert operation should return // null since there is no find equivalent for it etc. - toFindOperation(builder) { - return this; + toFindOperation( + builder: QueryBuilderOperationSupport, + ): QueryBuilderOperation | null; +} + +export type HasAllHooks = + & HasOnAdd + & HasOnBefore1 + & HasOnBefore2 + & HasOnBefore3 + & HasOnBuild + & HasOnBuildKnex + & HasOnRawResult + & HasOnAfter1 + & HasOnAfter2 + & HasOnAfter3 + & HasQueryExecutor + & HasOnError + & HasToFindOperation; + +export type HasOneOfHooks = + | HasOnAdd + | HasOnBefore1 + | HasOnBefore2 + | HasOnBefore3 + | HasOnBuild + | HasOnBuildKnex + | HasOnRawResult + | HasOnAfter1 + | HasOnAfter2 + | HasOnAfter3 + | HasQueryExecutor + | HasOnError + | HasToFindOperation; + +// An abstract base class for all query builder operations. QueryBuilderOperations almost always +// correspond to a single query builder method call. For example SelectOperation could be added when +// a `select` method is called. +// +// QueryBuilderOperation is just a bunch of query execution lifecycle hooks that subclasses +// can (but don't have to) implement. +// +// Basically a query builder is nothing but an array of QueryBuilderOperations. When the query is +// executed the hooks are called in the order explained below. The hooks are called so that a +// certain hook is called for _all_ operations before the next hook is called. For example if +// a builder has 5 operations, onBefore1 hook is called for each of them (and their results are awaited) +// before onBefore2 hook is called for any of the operations. +export class QueryBuilderOperation { + name?: string; + opt: nany; + // From which hook was this operation added as a child operation. + adderHookName?: keyof HasAllHooks; + // The parent operation that added this operation. + parentOperation?: QueryBuilderOperation; + // Operations this operation added in any of its hooks. + childOperations: QueryBuilderOperation[]; + + constructor(name: undefined | string = undefined, opt = {}) { + this.name = name; + this.opt = opt; + this.childOperations = []; + } + + is(opClass: Function): this is T { + return this instanceof opClass; } - hasToFindOperation() { - return this.toFindOperation !== QueryBuilderOperation.prototype.toFindOperation; + + hasHook(hookName: keyof HasAllHooks): boolean { + return hookName in this; } // Given a set of operations, returns true if any of this operation's // ancestor operations are included in the set. - isAncestorInSet(operationSet) { + isAncestorInSet(operationSet: Set): boolean { let ancestor = this.parentOperation; while (ancestor) { if (operationSet.has(ancestor)) { return true; } - ancestor = ancestor.parentOperation; } @@ -212,9 +231,12 @@ class QueryBuilderOperation { } // Takes a deep clone of this operation. - clone() { - const clone = new this.constructor(this.name, this.opt); + clone(): QueryBuilderOperation { + const clone = new QueryBuilderOperation(this.name, this.opt); + return this.cloneInto(clone); + } + cloneInto(clone: QueryBuilderOperation): QueryBuilderOperation { clone.adderHookName = this.adderHookName; clone.parentOperation = this.parentOperation; @@ -230,7 +252,10 @@ class QueryBuilderOperation { // Add an operation as a child operation. `hookName` must be the // name of the parent operation's hook that called this method. - addChildOperation(hookName, operation) { + addChildOperation( + hookName: keyof HasAllHooks, + operation: QueryBuilderOperation, + ) { operation.adderHookName = hookName; operation.parentOperation = this; @@ -238,34 +263,41 @@ class QueryBuilderOperation { } // Removes a single child operation. - removeChildOperation(operation) { + removeChildOperation(operation: QueryBuilderOperation) { const index = this.childOperations.indexOf(operation); if (index !== -1) { - operation.parentOperation = null; + operation.parentOperation = undefined; this.childOperations.splice(index, 1); } } // Replaces a single child operation. - replaceChildOperation(operation, newOperation) { + replaceChildOperation( + operation: QueryBuilderOperation, + newOperation: QueryBuilderOperation, + ) { const index = this.childOperations.indexOf(operation); if (index !== -1) { newOperation.adderHookName = operation.adderHookName; newOperation.parentOperation = this; - operation.parentOperation = null; + operation.parentOperation = undefined; this.childOperations[index] = newOperation; } } // Removes all child operations that were added from the `hookName` hook. - removeChildOperationsByHookName(hookName) { - this.childOperations = this.childOperations.filter((op) => op.adderHookName !== hookName); + removeChildOperationsByHookName(hookName: keyof HasAllHooks) { + this.childOperations = this.childOperations.filter((op) => + op.adderHookName !== hookName + ); } // Iterates through all descendant operations recursively. - forEachDescendantOperation(callback) { + forEachDescendantOperation( + callback: (op: QueryBuilderOperation) => boolean | void, + ): boolean { for (const operation of this.childOperations) { if (callback(operation) === false) { return false; @@ -279,15 +311,3 @@ class QueryBuilderOperation { return true; } } - -Object.defineProperties(QueryBuilderOperation, { - isObjectionQueryBuilderOperationClass: { - enumerable: false, - writable: false, - value: true, - }, -}); - -module.exports = { - QueryBuilderOperation, -}; diff --git a/src/types/class.ts b/src/types/class.ts new file mode 100644 index 0000000..b86d415 --- /dev/null +++ b/src/types/class.ts @@ -0,0 +1,2 @@ +// deno-lint-ignore no-explicit-any +export type Class = new (...args: any[]) => T; diff --git a/src/utils/deprecate.js b/src/utils/deprecate.js deleted file mode 100644 index cbd597d..0000000 --- a/src/utils/deprecate.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const LOGGED_DEPRECATIONS = new Set(); - -function deprecate(message) { - // Only log deprecation messages once. - if (!LOGGED_DEPRECATIONS.has(message)) { - LOGGED_DEPRECATIONS.add(message); - console.warn(message); - } -} - -module.exports = { - deprecate, -}; diff --git a/src/utils/deprecate.ts b/src/utils/deprecate.ts new file mode 100644 index 0000000..96eaa69 --- /dev/null +++ b/src/utils/deprecate.ts @@ -0,0 +1,22 @@ +const LOGGED_DEPRECATIONS = new Set(); + +/** + * Marks a method or property as deprecated and logs a deprecation warning message. + * @param reason - The reason for deprecating the method or property. + */ +export function deprecate(reason?: string) { + return function ( + // deno-lint-ignore no-explicit-any + target: any, + propertyKey: string, + _descriptor: PropertyDescriptor, + ) { + const message = + `${target.constructor.name}.${propertyKey} is deprecated. ${reason}`; + // Only log deprecation messages once. + if (!LOGGED_DEPRECATIONS.has(message)) { + LOGGED_DEPRECATIONS.add(message); + console.warn(message); + } + }; +} diff --git a/src/utils/knex.js b/src/utils/knex.js deleted file mode 100644 index c15b1fa..0000000 --- a/src/utils/knex.js +++ /dev/null @@ -1,78 +0,0 @@ -import { Knex } from "knex"; - -const { isObject, isFunction } = require("../utils/objectUtils"); - -function getDialect(knex: Knex | null) { - const type = typeof knex; - - return ( - (knex !== null && - (type === "object" || type === "function") && - knex.client && - knex.client.dialect) || - null - ); -} - -function isPostgres(knex) { - return getDialect(knex) === "postgresql"; -} - -function isOracle(knex) { - const dialect = getDialect(knex); - return dialect === "oracle" || dialect === "oracledb"; -} - -function isMySql(knex) { - const dialect = getDialect(knex); - return dialect === "mysql" || dialect === "mysql2"; -} - -function isSqlite(knex) { - return getDialect(knex) === "sqlite3"; -} - -function isMsSql(knex) { - return getDialect(knex) === "mssql"; -} - -function isKnexQueryBuilder(value) { - return ( - hasConstructor(value) && - isFunction(value.select) && - isFunction(value.column) && - value.select === value.column && - "client" in value - ); -} - -function isKnexJoinBuilder(value) { - return hasConstructor(value) && value.grouping === "join" && - "joinType" in value; -} - -function isKnexRaw(value) { - return hasConstructor(value) && value.isRawInstance && "client" in value; -} - -function isKnexTransaction(knex) { - return !!getDialect(knex) && isFunction(knex.commit) && - isFunction(knex.rollback); -} - -function hasConstructor(value) { - return isObject(value) && isFunction(value.constructor); -} - -module.exports = { - getDialect, - isPostgres, - isMySql, - isSqlite, - isMsSql, - isOracle, - isKnexQueryBuilder, - isKnexJoinBuilder, - isKnexRaw, - isKnexTransaction, -}; diff --git a/src/utils/knex.ts b/src/utils/knex.ts new file mode 100644 index 0000000..31f6fd9 --- /dev/null +++ b/src/utils/knex.ts @@ -0,0 +1,67 @@ +import { Knex } from 'knex'; +import { isFunction, isObject } from '../utils/object.ts'; + +export function getDialect(knex: Knex | null): string | null { + const type = typeof knex; + return ( + (knex !== null && + (type === 'object' || type === 'function') && + knex.client && + knex.client.dialect) || + null + ); +} + +export function isPostgres(knex: Knex | null): boolean { + return getDialect(knex) === 'postgresql'; +} + +export function isOracle(knex: Knex | null): boolean { + const dialect = getDialect(knex); + return dialect === 'oracle' || dialect === 'oracledb'; +} + +export function isMySql(knex: Knex | null): boolean { + const dialect = getDialect(knex); + return dialect === 'mysql' || dialect === 'mysql2'; +} + +export function isSqlite(knex: Knex | null): boolean { + return getDialect(knex) === 'sqlite3'; +} + +export function isMsSql(knex: Knex | null): boolean { + return getDialect(knex) === 'mssql'; +} + +// deno-lint-ignore no-explicit-any +export function isKnexQueryBuilder(value: any): boolean { + return ( + hasConstructor(value) && + isFunction(value.select) && + isFunction(value.column) && + value.select === value.column && + 'client' in value + ); +} + +// deno-lint-ignore no-explicit-any +export function isKnexJoinBuilder(value: any): boolean { + return hasConstructor(value) && value.grouping === 'join' && + 'joinType' in value; +} + +// deno-lint-ignore no-explicit-any +export function isKnexRaw(value: any): boolean { + return hasConstructor(value) && value.isRawInstance && 'client' in value; +} + +export function isKnexTransaction(knex: Knex.Transaction | null): boolean { + return !!getDialect(knex) && isFunction(knex?.commit) && + isFunction(knex.rollback); +} + +// deno-lint-ignore no-explicit-any +function hasConstructor(value: any): boolean { + return isObject(value) && isFunction(value.constructor); +} diff --git a/src/utils/object.ts b/src/utils/object.ts index 34e0acd..e8acc1b 100644 --- a/src/utils/object.ts +++ b/src/utils/object.ts @@ -433,12 +433,12 @@ export function isSafeKey(key: unknown): key is string | number { export function mergeMaps( map1: Map, map2: Map, -): Map { - const map = new Map(map1); +): Map { + const map = new Map(map1); if (map2) { for (const key of map2.keys()) { - map.set(key, map2.get(key)); + map.set(key, map2.get(key) as V); } } diff --git a/typings/objection/index.d.ts b/typings/objection/index.d.ts new file mode 100644 index 0000000..8ec8519 --- /dev/null +++ b/typings/objection/index.d.ts @@ -0,0 +1,1973 @@ +/// + +// Type definitions for Objection.js +// Project: +// +// Contributions by: +// * Matthew McEachen +// * Sami Koskimäki +// * Mikael Lepistö +// * Joseph T Lapp +// * Drew R. +// * Karl Blomster +// * And many others: See + +import Ajv, { Options as AjvOptions } from 'ajv'; +import * as dbErrors from 'db-errors'; +import { Knex } from 'knex'; + +// Export the entire Objection namespace. +export = Objection; + +declare namespace Objection { + const raw: RawFunction; + const val: ValueFunction; + const ref: ReferenceFunction; + const fn: FunctionFunction; + + const compose: ComposeFunction; + const mixin: MixinFunction; + + const snakeCaseMappers: SnakeCaseMappersFactory; + const knexSnakeCaseMappers: KnexSnakeCaseMappersFactory; + + const transaction: transaction; + const initialize: initialize; + + const DBError: typeof dbErrors.DBError; + const DataError: typeof dbErrors.DataError; + const CheckViolationError: typeof dbErrors.CheckViolationError; + const UniqueViolationError: typeof dbErrors.UniqueViolationError; + const ConstraintViolationError: typeof dbErrors.ConstraintViolationError; + const ForeignKeyViolationError: typeof dbErrors.ForeignKeyViolationError; + const NotNullViolationError: typeof dbErrors.NotNullViolationError; + + export interface RawBuilder extends Aliasable {} + + export interface RawFunction extends RawInterface {} + export interface RawInterface { + (sql: string, ...bindings: any[]): R; + } + + export interface ValueBuilder extends Castable {} + export interface ValueFunction { + ( + value: PrimitiveValue | PrimitiveValue[] | PrimitiveValueObject | PrimitiveValueObject[], + ): ValueBuilder; + } + + export interface ReferenceBuilder extends Castable { + from(tableReference: string): this; + } + export interface ReferenceFunction { + (expression: string): ReferenceBuilder; + } + + export interface FunctionBuilder extends Castable {} + export interface SqlFunctionShortcut { + (...args: any[]): FunctionBuilder; + } + export interface FunctionFunction { + (functionName: string, ...arguments: any[]): FunctionBuilder; + + now(precision: number): FunctionBuilder; + now(): FunctionBuilder; + + coalesce: SqlFunctionShortcut; + concat: SqlFunctionShortcut; + sum: SqlFunctionShortcut; + avg: SqlFunctionShortcut; + min: SqlFunctionShortcut; + max: SqlFunctionShortcut; + count: SqlFunctionShortcut; + upper: SqlFunctionShortcut; + lower: SqlFunctionShortcut; + } + + export interface ComposeFunction { + (...plugins: Plugin[]): Plugin; + (plugins: Plugin[]): Plugin; + } + + export interface Plugin { + (modelClass: M): M; + } + + export interface MixinFunction { + (modelClass: MC, ...plugins: Plugin[]): MC; + (modelClass: MC, plugins: Plugin[]): MC; + } + + interface Aliasable { + as(alias: string): this; + } + + interface Castable extends Aliasable { + castText(): this; + castInt(): this; + castBigInt(): this; + castFloat(): this; + castDecimal(): this; + castReal(): this; + castBool(): this; + castJson(): this; + castArray(): this; + asArray(): this; + castType(sqlType: string): this; + castTo(sqlType: string): this; + } + + type Raw = RawBuilder | Knex.Raw; + type Operator = string; + type ColumnRef = string | Raw | ReferenceBuilder; + type TableRef = ColumnRef | AnyQueryBuilder | CallbackVoid; + + type PrimitiveValue = + | string + | number + | boolean + | Date + | Buffer + | string[] + | number[] + | boolean[] + | Date[] + | Buffer[] + | null; + + type Expression = T | Raw | ReferenceBuilder | ValueBuilder | AnyQueryBuilder; + + type Id = string | number | BigInt | Buffer; + type CompositeId = Id[]; + type MaybeCompositeId = Id | CompositeId; + + interface ExpressionObject { + [key: string]: Expression; + } + + interface PrimitiveValueObject { + [key: string]: PrimitiveValue; + } + + interface CallbackVoid { + (this: T, arg: T): void; + } + + type Identity = (value: T) => T; + type AnyQueryBuilder = QueryBuilder; + type AnyModelConstructor = ModelConstructor; + type ModifierFunction = (qb: QB, ...args: any[]) => void; + type Modifier = + | ModifierFunction + | string + | string[] + | Record>; + type OrderByDirection = 'asc' | 'desc' | 'ASC' | 'DESC'; + type OrderByNulls = 'first' | 'last'; + + interface Modifiers { + [key: string]: Modifier; + } + + type RelationExpression = string | object; + + /** + * If T is an array, returns the item type, otherwise returns T. + */ + type ItemType = T extends Array ? T[number] : T; + + /** + * Type for keys of non-function properties of T. + */ + type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; + + /** + * Type that attempts to only select the user-defined model properties. + */ + type DataPropertyNames = Exclude, 'QueryBuilderType'>; + + /** + * Removes `undefined` from a type. + */ + type Defined = Exclude; + + /** + * A Pojo version of model. + */ + type ModelObject = Pick>; + + /** + * Any object that has some of the properties of model class T match this type. + */ + type PartialModelObject = { + [K in DataPropertyNames]?: Defined extends Model + ? T[K] + : Defined extends Array + ? I extends Model + ? I[] + : Expression + : Expression; + }; + + /** + * Additional optional parameters that may be used in graphs. + */ + type GraphParameters = { + '#dbRef'?: MaybeCompositeId; + '#ref'?: string; + '#id'?: string; + }; + + /** + * Just like PartialModelObject but this is applied recursively to relations. + */ + type PartialModelGraph = T extends any + ? { + [K in DataPropertyNames]?: null extends T[K] + ? PartialModelGraphField> | null // handle nullable BelongsToOneRelations + : PartialModelGraphField; + } + : never; + + type PartialModelGraphField = + Defined extends Model + ? PartialModelGraph> + : Defined extends Array + ? I extends Model + ? PartialModelGraph[] + : Expression + : Expression; + + /** + * Extracts the property names (excluding relations) of a model class. + */ + type ModelProps = Exclude< + { + [K in keyof T]?: Defined extends Model + ? never + : Defined extends Array + ? I extends Model + ? never + : K + : T[K] extends Function + ? never + : K; + }[keyof T], + undefined | 'QueryBuilderType' + >; + + /** + * Extracts the relation names of the a model class. + */ + type ModelRelations = Defined< + { + [K in keyof T]?: Defined extends Model + ? K + : Defined extends Array + ? I extends Model + ? K + : never + : never; + }[keyof T] + >; + + /** + * Given a model property type, returns a query builer type of + * correct kind if the property is a model or a model array. + */ + type RelatedQueryBuilder = T extends Model + ? SingleQueryBuilder> + : T extends Array + ? I extends Model + ? QueryBuilderType + : never + : never; + + /** + * Just like RelatedQueryBuilder but always returns an array + * query builder even if the property type is a model and not + * an array of models. + */ + type ArrayRelatedQueryBuilder = T extends Model + ? QueryBuilderType + : T extends Array + ? I extends Model + ? QueryBuilderType + : never + : never; + + /** + * Gets the query builder type for a model type. + */ + type QueryBuilderType = T['QueryBuilderType']; + + /** + * Gets the model type from a query builder type. + */ + type ModelType = T['ModelType']; + + /** + * Gets the result type from a query builder type. + */ + type ResultType = T['ResultType']; + + /** + * Gets the single item query builder type for a query builder. + */ + type SingleQueryBuilder = T['SingleQueryBuilderType']; + + /** + * Gets the single or undefined item query builder type for a query builder. + */ + type MaybeSingleQueryBuilder = QB['MaybeSingleQueryBuilderType']; + + /** + * Gets the multi-item query builder type for a query builder. + */ + type ArrayQueryBuilder = T['ArrayQueryBuilderType']; + + /** + * Gets the number query builder type for a query builder. + */ + type NumberQueryBuilder = T['NumberQueryBuilderType']; + + /** + * Gets the page query builder type for a query builder. + */ + type PageQueryBuilder = T['PageQueryBuilderType']; + + interface ForClassMethod { + (modelClass: ModelConstructor): QueryBuilderType; + } + + /** + * https://vincit.github.io/objection.js/api/types/#type-fieldexpression + */ + type FieldExpression = string; + + type JsonObjectOrFieldExpression = object | object[] | FieldExpression; + + type Selection = ColumnRef | AnyQueryBuilder | CallbackVoid; + + interface SelectMethod { + // These must come first so that we get autocomplete. + (...columns: ModelProps>[]): QB; + (columns: ModelProps>[]): QB; + + (...columns: Selection[]): QB; + (columns: Selection[]): QB; + + // Allows things like `select(1)`, not sure if we should be more specific here? + (...args: any[]): QB; + } + + interface AsMethod { + (alias: string): QB; + } + + interface FromMethod { + (table: TableRef): QB; + } + + interface FromRawMethod extends RawInterface {} + + interface JsonExtraction { + column: string | Raw | Knex.QueryBuilder; + path: string; + alias?: string; + singleValue?: boolean; + } + + interface JsonExtract { + // These must come first so that we get autocomplete. + ( + column: ModelProps>, + path: string, + alias?: string, + singleValue?: boolean, + ): QB; + + (column: ColumnRef, path: string, alias?: string, singleValue?: boolean): QB; + (column: JsonExtraction[] | any[][], singleValue?: boolean): QB; + } + + interface JsonSet { + // These must come first so that we get autocomplete. + ( + column: ModelProps>, + path: string, + value: any, + alias?: string, + ): QB; + + (column: ColumnRef, path: string, value: any, alias?: string): QB; + } + + interface JsonInsert { + // These must come first so that we get autocomplete. + ( + column: ModelProps>, + path: string, + value: any, + alias?: string, + ): QB; + + (column: ColumnRef, path: string, value: any, alias?: string): QB; + } + + interface JsonRemove { + // These must come first so that we get autocomplete. + (column: ModelProps>, path: string, alias?: string): QB; + + (column: ColumnRef, path: string, alias?: string): QB; + } + + interface WhereMethod { + // These must come first so that we get autocomplete. + ( + col: ModelProps>, + op: Operator, + expr: Expression, + ): QB; + + (col: ModelProps>, expr: Expression): QB; + + (col: ColumnRef, op: Operator, expr: Expression): QB; + (col: ColumnRef, expr: Expression): QB; + + (condition: boolean): QB; + (cb: CallbackVoid): QB; + (raw: Raw): QB; + (qb: QBA): QB; + + (obj: PartialModelObject>): QB; + // We must allow any keys in the object. The previous type + // is kind of useless, but maybe one day vscode and other + // tools can autocomplete using it. + (obj: object): QB; + } + + interface WhereRawMethod extends RawInterface {} + + interface WhereWrappedMethod { + (cb: CallbackVoid): QB; + } + + interface WhereExistsMethod { + (cb: CallbackVoid): QB; + (raw: Raw): QB; + (qb: QBA): QB; + } + + interface WhereInMethod { + // These must come first so that we get autocomplete. + (col: ModelProps>, expr: Expression): QB; + (col: ModelProps>, cb: CallbackVoid): QB; + (col: ModelProps>, qb: AnyQueryBuilder): QB; + + (col: ColumnRef | ColumnRef[], expr: readonly Expression[]): QB; + (col: ColumnRef | ColumnRef[], cb: CallbackVoid): QB; + (col: ColumnRef | ColumnRef[], qb: AnyQueryBuilder): QB; + } + + interface WhereBetweenMethod { + (column: ColumnRef, range: [Expression, Expression]): QB; + } + + interface WhereNullMethod { + (column: ColumnRef): QB; + } + + interface WhereColumnMethod { + // These must come first so that we get autocomplete. + (col1: ModelProps>, op: Operator, col2: ColumnRef): QB; + (col1: ModelProps>, col2: ColumnRef): QB; + + (col1: ColumnRef, op: Operator, col2: ColumnRef): QB; + (col1: ColumnRef, col2: ColumnRef): QB; + } + + interface WhereJsonObject { + // These must come first so that we get autocomplete. + (col: ModelProps>, value: any): QB; + + (col: ColumnRef, value: any): QB; + } + + interface WhereJsonPath { + // These must come first so that we get autocomplete. + ( + col: ModelProps>, + jsonPath: string, + operator: string, + value: any, + ): QB; + + (col: ColumnRef, jsonPath: string, operator: string, value: any): QB; + } + + interface WhereJsonMethod { + ( + fieldExpression: FieldExpression, + jsonObjectOrFieldExpression: JsonObjectOrFieldExpression, + ): QB; + } + + interface WhereFieldExpressionMethod { + (fieldExpression: FieldExpression): QB; + } + + interface WhereJsonExpressionMethod { + (fieldExpression: FieldExpression, keys: string | string[]): QB; + } + + interface WhereJsonField { + ( + fieldExpression: FieldExpression, + operator: string, + value: boolean | number | string | null, + ): QB; + } + + interface WhereCompositeMethod { + (column: ColumnRef[], op: Operator, expr: readonly Expression[]): QB; + (column: ColumnRef, expr: Expression): QB; + (column: ColumnRef, op: Operator, expr: Expression): QB; + (column: ColumnRef[], expr: readonly Expression[]): QB; + (column: ColumnRef[], qb: AnyQueryBuilder): QB; + } + + interface WhereInCompositeMethod { + (column: ColumnRef, expr: readonly Expression[]): QB; + (column: ColumnRef, qb: AnyQueryBuilder): QB; + (column: ColumnRef[], expr: readonly Expression[][]): QB; + (column: ColumnRef[], qb: AnyQueryBuilder): QB; + } + + type QBOrCallback = AnyQueryBuilder | CallbackVoid; + + interface BaseSetOperations { + (callbackOrBuilder: QBOrCallback, wrap?: boolean): QB; + (callbacksOrBuilders: QBOrCallback[], wrap?: boolean): QB; + } + + interface SetOperationsMethod extends BaseSetOperations { + (...callbacksOrBuilders: QBOrCallback[]): QB; + } + + interface UnionMethod extends BaseSetOperations { + (arg1: QBOrCallback, wrap?: boolean): QB; + (arg1: QBOrCallback, arg2: QBOrCallback, wrap?: boolean): QB; + (arg1: QBOrCallback, arg2: QBOrCallback, arg3: QBOrCallback, wrap?: boolean): QB; + ( + arg1: QBOrCallback, + arg2: QBOrCallback, + arg3: QBOrCallback, + arg4: QBOrCallback, + wrap?: boolean, + ): QB; + ( + arg1: QBOrCallback, + arg2: QBOrCallback, + arg3: QBOrCallback, + arg4: QBOrCallback, + arg5: QBOrCallback, + wrap?: boolean, + ): QB; + ( + arg1: QBOrCallback, + arg2: QBOrCallback, + arg3: QBOrCallback, + arg4: QBOrCallback, + arg5: QBOrCallback, + arg6: QBOrCallback, + wrap?: boolean, + ): QB; + ( + arg1: QBOrCallback, + arg2: QBOrCallback, + arg3: QBOrCallback, + arg4: QBOrCallback, + arg5: QBOrCallback, + arg6: QBOrCallback, + arg7: QBOrCallback, + wrap?: boolean, + ): QB; + } + + interface WithMethod { + (alias: string, expr: CallbackVoid | AnyQueryBuilder | Raw): QB; + } + + interface JoinRelatedOptions { + alias?: string | boolean; + aliases?: Record; + } + + interface JoinRelatedMethod { + (expr: RelationExpression>, opt?: JoinRelatedOptions): QB; + } + + interface JoinMethod { + (table: TableRef, leftCol: ColumnRef, op: Operator, rightCol: ColumnRef): QB; + (table: TableRef, leftCol: ColumnRef, rightCol: ColumnRef): QB; + (table: TableRef, cb: CallbackVoid): QB; + (table: TableRef, raw: Raw): QB; + (raw: Raw): QB; + } + + interface JoinRawMethod extends RawInterface {} + + interface IncrementDecrementMethod { + (column: string, amount?: number): QB; + } + + interface AggregateMethod { + (column: ColumnRef): QB; + } + + interface CountMethod { + (column?: ColumnRef, options?: { as: string }): QB; + (aliasToColumnDict: { [alias: string]: string | string[] }): QB; + (...columns: ColumnRef[]): QB; + } + + interface GroupByMethod { + (...columns: ColumnRef[]): QB; + (columns: ColumnRef[]): QB; + } + + interface OrderByDescriptor { + column: ColumnRef; + order?: OrderByDirection; + nulls?: OrderByNulls; + } + + type ColumnRefOrOrderByDescriptor = ColumnRef | OrderByDescriptor; + + interface OrderByMethod { + (column: ColumnRef, order?: OrderByDirection, nulls?: OrderByNulls): QB; + (columns: ColumnRefOrOrderByDescriptor[]): QB; + } + + interface OrderByRawMethod extends RawInterface {} + + interface FirstMethod { + ( + this: QB, + ): QB extends ArrayQueryBuilder ? MaybeSingleQueryBuilder : QB; + } + + type ForIdValue = MaybeCompositeId | AnyQueryBuilder; + + interface AllowGraphMethod { + (expr: RelationExpression>): QB; + } + + interface IdentityMethod { + (): QB; + } + + interface OneArgMethod { + (arg: T): QB; + } + + interface StringReturningMethod { + (): string; + } + + interface BooleanReturningMethod { + (): boolean; + } + + interface HasMethod { + (selector: string | RegExp): boolean; + } + + interface ClearMethod { + (selector: string | RegExp): QB; + } + + interface ColumnInfoMethod { + (): Promise; + } + + interface TableRefForMethod { + (modelClass: ModelClass | typeof Model): string; + } + + interface AliasForMethod { + (modelClassOrTableName: string | AnyModelConstructor, alias: string): QB; + } + + interface ModelClassMethod { + (): ModelClass; + } + + interface ReturningMethod { + ( + this: QB, + column: string | Raw | (string | Raw)[], + ): QB extends NumberQueryBuilder ? ArrayQueryBuilder : QB; + } + + interface TimeoutOptions { + cancel: boolean; + } + + interface TimeoutMethod { + (ms: number, options?: TimeoutOptions): QB; + } + + export interface Page { + total: number; + results: M[]; + } + + interface RunBeforeCallback { + (this: QB, result: any, query: QB): any; + } + + interface RunBeforeMethod { + (cb: RunBeforeCallback): QB; + } + + interface RunAfterCallback { + (this: QB, result: ResultType, query: QB): any; + } + + interface RunAfterMethod { + (cb: RunAfterCallback): QB; + } + + interface OnBuildMethod { + (cb: CallbackVoid): QB; + } + + interface OnBuildKnexCallback { + (this: QB, knexQuery: Knex.QueryBuilder, query: QB): void; + } + + interface OnBuildKnexMethod { + (cb: OnBuildKnexCallback): QB; + } + + interface OnErrorCallback { + (this: QB, error: Error, query: QB): any; + } + + interface OnErrorMethod { + (cb: OnErrorCallback): QB; + } + + export interface InsertGraphOptions { + relate?: boolean | string[]; + allowRefs?: boolean; + } + + interface InsertGraphMethod { + ( + this: QB, + graph: PartialModelGraph, + options?: InsertGraphOptions, + ): SingleQueryBuilder; + + ( + this: QB, + graph: PartialModelGraph[], + options?: InsertGraphOptions, + ): ArrayQueryBuilder; + } + + export interface UpsertGraphOptions { + relate?: boolean | string[]; + unrelate?: boolean | string[]; + insertMissing?: boolean | string[]; + update?: boolean | string[]; + noInsert?: boolean | string[]; + noUpdate?: boolean | string[]; + noDelete?: boolean | string[]; + noRelate?: boolean | string[]; + noUnrelate?: boolean | string[]; + allowRefs?: boolean; + } + + interface UpsertGraphMethod { + ( + this: QB, + graph: PartialModelGraph[], + options?: UpsertGraphOptions, + ): ArrayQueryBuilder; + + ( + this: QB, + graph: PartialModelGraph, + options?: UpsertGraphOptions, + ): SingleQueryBuilder; + } + + interface GraphExpressionObjectMethod { + (): any; + } + + export interface GraphOptions { + minimize?: boolean; + separator?: string; + aliases?: { [key: string]: string }; + joinOperation?: string; + maxBatchSize?: number; + } + + interface ModifyGraphMethod { + ( + expr: RelationExpression>, + modifier: Modifier>, + ): QB; + } + + interface ContextMethod { + (context: object): QB; + (): QueryContext; + } + + interface ClearContextMethod { + (): QB; + } + + interface ModifyMethod { + (modifier: Modifier | Modifier[], ...args: any[]): QB; + } + + interface ModifiersMethod { + (modifiers: Modifiers): QB; + (): QB; + } + + export interface Pojo { + [key: string]: any; + } + + export interface CatchablePromiseLike extends PromiseLike { + catch( + onrejected?: ((reason: any) => FR | PromiseLike) | undefined | null, + ): Promise; + } + + export class QueryBuilder implements CatchablePromiseLike { + static forClass: ForClassMethod; + + select: SelectMethod; + columns: SelectMethod; + column: SelectMethod; + distinct: SelectMethod; + distinctOn: SelectMethod; + as: AsMethod; + + from: FromMethod; + table: FromMethod; + into: FromMethod; + fromRaw: FromRawMethod; + + jsonExtract: JsonExtract; + jsonSet: JsonSet; + jsonInsert: JsonInsert; + jsonRemove: JsonRemove; + + where: WhereMethod; + andWhere: WhereMethod; + orWhere: WhereMethod; + whereNot: WhereMethod; + andWhereNot: WhereMethod; + orWhereNot: WhereMethod; + whereLike: WhereMethod; + andWhereLike: WhereMethod; + orWhereLike: WhereMethod; + whereILike: WhereMethod; + andWhereILike: WhereMethod; + orWhereILike: WhereMethod; + + whereRaw: WhereRawMethod; + orWhereRaw: WhereRawMethod; + andWhereRaw: WhereRawMethod; + + whereWrapped: WhereWrappedMethod; + havingWrapped: WhereWrappedMethod; + + whereExists: WhereExistsMethod; + orWhereExists: WhereExistsMethod; + whereNotExists: WhereExistsMethod; + orWhereNotExists: WhereExistsMethod; + + whereIn: WhereInMethod; + orWhereIn: WhereInMethod; + whereNotIn: WhereInMethod; + orWhereNotIn: WhereInMethod; + + whereBetween: WhereBetweenMethod; + orWhereBetween: WhereBetweenMethod; + andWhereBetween: WhereBetweenMethod; + whereNotBetween: WhereBetweenMethod; + orWhereNotBetween: WhereBetweenMethod; + andWhereNotBetween: WhereBetweenMethod; + + whereNull: WhereNullMethod; + orWhereNull: WhereNullMethod; + whereNotNull: WhereNullMethod; + orWhereNotNull: WhereNullMethod; + + whereColumn: WhereColumnMethod; + orWhereColumn: WhereColumnMethod; + andWhereColumn: WhereColumnMethod; + whereNotColumn: WhereColumnMethod; + orWhereNotColumn: WhereColumnMethod; + andWhereNotColumn: WhereColumnMethod; + + whereJsonObject: WhereJsonObject; + orWhereJsonObject: WhereJsonObject; + andWhereJsonObject: WhereJsonObject; + whereNotJsonObject: WhereJsonObject; + orWhereNotJsonObject: WhereJsonObject; + andWhereNotJsonObject: WhereJsonObject; + + whereJsonPath: WhereJsonPath; + orWhereJsonPath: WhereJsonPath; + andWhereJsonPath: WhereJsonPath; + + whereJsonSupersetOf: WhereJsonMethod; + andWhereJsonSupersetOf: WhereJsonMethod; + orWhereJsonSupersetOf: WhereJsonMethod; + whereJsonNotSupersetOf: WhereJsonMethod; + andWhereJsonNotSupersetOf: WhereJsonMethod; + orWhereJsonNotSupersetOf: WhereJsonMethod; + whereJsonSubsetOf: WhereJsonMethod; + andWhereJsonSubsetOf: WhereJsonMethod; + orWhereJsonSubsetOf: WhereJsonMethod; + whereJsonNotSubsetOf: WhereJsonMethod; + andWhereJsonNotSubsetOf: WhereJsonMethod; + orWhereJsonNotSubsetOf: WhereJsonMethod; + whereJsonIsArray: WhereFieldExpressionMethod; + orWhereJsonIsArray: WhereFieldExpressionMethod; + whereJsonNotArray: WhereFieldExpressionMethod; + orWhereJsonNotArray: WhereFieldExpressionMethod; + whereJsonIsObject: WhereFieldExpressionMethod; + orWhereJsonIsObject: WhereFieldExpressionMethod; + whereJsonNotObject: WhereFieldExpressionMethod; + orWhereJsonNotObject: WhereFieldExpressionMethod; + whereJsonHasAny: WhereJsonExpressionMethod; + orWhereJsonHasAny: WhereJsonExpressionMethod; + whereJsonHasAll: WhereJsonExpressionMethod; + orWhereJsonHasAll: WhereJsonExpressionMethod; + + having: WhereMethod; + andHaving: WhereMethod; + orHaving: WhereMethod; + + havingRaw: WhereRawMethod; + orHavingRaw: WhereRawMethod; + + havingIn: WhereInMethod; + orHavingIn: WhereInMethod; + havingNotIn: WhereInMethod; + orHavingNotIn: WhereInMethod; + + havingNull: WhereNullMethod; + orHavingNull: WhereNullMethod; + havingNotNull: WhereNullMethod; + orHavingNotNull: WhereNullMethod; + + havingExists: WhereExistsMethod; + orHavingExists: WhereExistsMethod; + havingNotExists: WhereExistsMethod; + orHavingNotExists: WhereExistsMethod; + + havingBetween: WhereBetweenMethod; + orHavingBetween: WhereBetweenMethod; + havingNotBetween: WhereBetweenMethod; + orHavingNotBetween: WhereBetweenMethod; + + whereComposite: WhereCompositeMethod; + whereInComposite: WhereInCompositeMethod; + whereNotInComposite: WhereInCompositeMethod; + + union: UnionMethod; + unionAll: UnionMethod; + intersect: SetOperationsMethod; + except: SetOperationsMethod; + + with: WithMethod; + withRecursive: WithMethod; + withWrapped: WithMethod; + withMaterialized: WithMethod; + withNotMaterialized: WithMethod; + + joinRelated: JoinRelatedMethod; + innerJoinRelated: JoinRelatedMethod; + outerJoinRelated: JoinRelatedMethod; + leftJoinRelated: JoinRelatedMethod; + leftOuterJoinRelated: JoinRelatedMethod; + rightJoinRelated: JoinRelatedMethod; + rightOuterJoinRelated: JoinRelatedMethod; + fullOuterJoinRelated: JoinRelatedMethod; + + join: JoinMethod; + joinRaw: JoinRawMethod; + innerJoin: JoinMethod; + leftJoin: JoinMethod; + leftOuterJoin: JoinMethod; + rightJoin: JoinMethod; + rightOuterJoin: JoinMethod; + outerJoin: JoinMethod; + fullOuterJoin: JoinMethod; + crossJoin: JoinMethod; + + count: CountMethod; + countDistinct: CountMethod; + min: AggregateMethod; + max: AggregateMethod; + sum: AggregateMethod; + sumDistinct: AggregateMethod; + avg: AggregateMethod; + avgDistinct: AggregateMethod; + increment: IncrementDecrementMethod; + decrement: IncrementDecrementMethod; + first: FirstMethod; + + orderBy: OrderByMethod; + orderByRaw: OrderByRawMethod; + + groupBy: GroupByMethod; + groupByRaw: RawInterface; + + findById(id: MaybeCompositeId): MaybeSingleQueryBuilder; + findByIds(ids: MaybeCompositeId[]): this; + findOne: WhereMethod>; + + execute(): Promise; + castTo(modelClass: ModelConstructor): QueryBuilderType; + castTo(): QueryBuilder; + + update(update: PartialModelObject): NumberQueryBuilder; + update(): NumberQueryBuilder; + updateAndFetch(update: PartialModelObject): SingleQueryBuilder; + updateAndFetchById( + id: MaybeCompositeId, + update: PartialModelObject, + ): SingleQueryBuilder; + + patch(update: PartialModelObject): NumberQueryBuilder; + patch(): NumberQueryBuilder; + patchAndFetch(update: PartialModelObject): SingleQueryBuilder; + patchAndFetchById( + id: MaybeCompositeId, + update: PartialModelObject, + ): SingleQueryBuilder; + + del(): NumberQueryBuilder; + delete(): NumberQueryBuilder; + deleteById(id: MaybeCompositeId): NumberQueryBuilder; + + insert(insert: PartialModelObject): SingleQueryBuilder; + insert(insert: PartialModelObject[]): ArrayQueryBuilder; + insert(): SingleQueryBuilder; + + onConflict(column?: string | string[] | true): this; + ignore(): this; + merge(merge?: PartialModelObject | string[]): this; + + insertAndFetch(insert: PartialModelObject): SingleQueryBuilder; + insertAndFetch(insert: PartialModelObject[]): ArrayQueryBuilder; + insertAndFetch(): SingleQueryBuilder; + + relate( + ids: MaybeCompositeId | MaybeCompositeId[] | PartialModelObject | PartialModelObject[], + ): NumberQueryBuilder; + + unrelate(): NumberQueryBuilder; + for(ids: ForIdValue | ForIdValue[]): this; + + withGraphFetched(expr: RelationExpression, options?: GraphOptions): this; + withGraphJoined(expr: RelationExpression, options?: GraphOptions): this; + + truncate(): Promise; + allowGraph: AllowGraphMethod; + + throwIfNotFound: ( + arg?: any, + ) => R extends Model | undefined ? SingleQueryBuilder> : this; + + returning: ReturningMethod; + forUpdate: IdentityMethod; + forShare: IdentityMethod; + forNoKeyUpdate: IdentityMethod; + forKeyShare: IdentityMethod; + skipLocked: IdentityMethod; + noWait: IdentityMethod; + skipUndefined: IdentityMethod; + debug: IdentityMethod; + alias: OneArgMethod; + aliasFor: AliasForMethod; + withSchema: OneArgMethod; + modelClass: ModelClassMethod; + tableNameFor: TableRefForMethod; + tableRefFor: TableRefForMethod; + reject: OneArgMethod; + resolve: OneArgMethod; + transacting: OneArgMethod; + connection: OneArgMethod; + timeout: TimeoutMethod; + columnInfo: ColumnInfoMethod; + + toKnexQuery>(): Knex.QueryBuilder; + knex(knex?: Knex): Knex; + clone(): this; + + page(page: number, pageSize: number): PageQueryBuilder; + range(): PageQueryBuilder; + range(start: number, end: number): PageQueryBuilder; + offset(offset: number): this; + limit(limit: number): this; + resultSize(): Promise; + + runBefore: RunBeforeMethod; + runAfter: RunAfterMethod; + + onBuild: OnBuildMethod; + onBuildKnex: OnBuildKnexMethod; + onError: OnErrorMethod; + + insertGraph: InsertGraphMethod; + insertGraphAndFetch: InsertGraphMethod; + + upsertGraph: UpsertGraphMethod; + upsertGraphAndFetch: UpsertGraphMethod; + + graphExpressionObject: GraphExpressionObjectMethod; + + modifyGraph: ModifyGraphMethod; + + context: ContextMethod; + clearContext: ClearContextMethod; + + modify: ModifyMethod; + modifiers: ModifiersMethod; + + isFind: BooleanReturningMethod; + isExecutable: BooleanReturningMethod; + isInsert: BooleanReturningMethod; + isUpdate: BooleanReturningMethod; + isDelete: BooleanReturningMethod; + isRelate: BooleanReturningMethod; + isUnrelate: BooleanReturningMethod; + isInternal: BooleanReturningMethod; + hasWheres: BooleanReturningMethod; + hasSelects: BooleanReturningMethod; + hasWithGraph: BooleanReturningMethod; + + has: HasMethod; + clear: ClearMethod; + + clearSelect: IdentityMethod; + clearOrder: IdentityMethod; + clearWhere: IdentityMethod; + clearWithGraph: IdentityMethod; + clearAllowGraph: IdentityMethod; + + ModelType: M; + ResultType: R; + + ArrayQueryBuilderType: QueryBuilder; + SingleQueryBuilderType: QueryBuilder; + MaybeSingleQueryBuilderType: QueryBuilder; + NumberQueryBuilderType: QueryBuilder; + PageQueryBuilderType: QueryBuilder>; + + then( + onfulfilled?: ((value: R) => R1 | PromiseLike) | undefined | null, + onrejected?: ((reason: any) => R2 | PromiseLike) | undefined | null, + ): Promise; + + catch( + onrejected?: ((reason: any) => FR | PromiseLike) | undefined | null, + ): Promise; + } + + type X = Promise; + + interface FetchGraphOptions { + transaction?: TransactionOrKnex; + skipFetched?: boolean; + } + + interface TraverserFunction { + (model: Model, parentModel: Model, relationName: string): void; + } + + type ArrayQueryBuilderThunk = () => ArrayQueryBuilder>; + type CancelQueryThunk = (result: any) => void; + + export interface StaticHookArguments { + asFindQuery: ArrayQueryBuilderThunk; + cancelQuery: CancelQueryThunk; + context: QueryContext; + transaction: TransactionOrKnex; + relation?: Relation; + modelOptions?: ModelOptions; + items: Model[]; + inputItems: M[]; + result?: R; + } + + export type Transaction = Knex.Transaction; + export type TransactionOrKnex = Transaction | Knex; + + export interface RelationMappings { + [relationName: string]: RelationMapping; + } + + export type RelationMappingsThunk = () => RelationMappings; + + type ModelClassFactory = () => AnyModelConstructor; + type ModelClassSpecifier = ModelClassFactory | AnyModelConstructor | string; + type RelationMappingHook = ( + model: M, + context: QueryContext, + ) => Promise | void; + type StringOrReferenceBuilder = string | ReferenceBuilder; + type RelationMappingColumnRef = StringOrReferenceBuilder | StringOrReferenceBuilder[]; + + export interface RelationMapping { + relation: RelationType; + modelClass: ModelClassSpecifier; + join: RelationJoin; + modify?: Modifier>; + filter?: Modifier>; + beforeInsert?: RelationMappingHook; + } + + export interface RelationJoin { + from: RelationMappingColumnRef; + to: RelationMappingColumnRef; + through?: RelationThrough; + } + + export interface RelationThrough { + from: RelationMappingColumnRef; + to: RelationMappingColumnRef; + extra?: string | string[] | Record; + modelClass?: ModelClassSpecifier; + modify?: Modifier>; + filter?: Modifier>; + beforeInsert?: RelationMappingHook; + } + + export interface RelationType extends Constructor {} + + export interface Relation { + name: string; + ownerModelClass: typeof Model; + relatedModelClass: typeof Model; + ownerProp: RelationProperty; + relatedProp: RelationProperty; + joinModelClass: typeof Model; + joinTable: string; + joinTableOwnerProp: RelationProperty; + joinTableRelatedProp: RelationProperty; + } + + export interface RelationProperty { + size: number; + modelClass: typeof Model; + props: string[]; + cols: string[]; + } + + export interface Relations { + [name: string]: Relation; + } + + export interface QueryContext { + transaction: Transaction; + [key: string]: any; + } + + export interface ModelOptions { + patch?: boolean; + skipValidation?: boolean; + old?: object; + } + + export interface CloneOptions { + shallow?: boolean; + } + + export interface ToJsonOptions extends CloneOptions { + virtuals?: boolean | string[]; + } + + export interface ValidatorContext { + [key: string]: any; + } + + export interface ValidatorArgs { + ctx: ValidatorContext; + model: Model; + json: Pojo; + options: ModelOptions; + } + + export class Validator { + beforeValidate(args: ValidatorArgs): void; + validate(args: ValidatorArgs): Pojo; + afterValidate(args: ValidatorArgs): void; + } + + export interface AjvConfig { + onCreateAjv(ajv: Ajv): void; + options?: AjvOptions; + } + + export class AjvValidator extends Validator { + constructor(config: AjvConfig); + } + + export interface SnakeCaseMappersOptions { + upperCase?: boolean; + underscoreBeforeDigits?: boolean; + underscoreBetweenUppercaseLetters?: boolean; + } + + export interface ColumnNameMappers { + parse(json: Pojo): Pojo; + format(json: Pojo): Pojo; + } + + export interface SnakeCaseMappersFactory { + (options?: SnakeCaseMappersOptions): ColumnNameMappers; + } + + export interface KnexMappers { + wrapIdentifier(identifier: string, origWrap: Identity): string; + postProcessResponse(response: any): any; + } + + export interface KnexSnakeCaseMappersFactory { + (options?: SnakeCaseMappersOptions): KnexMappers; + } + + export type ValidationErrorType = + | 'ModelValidation' + | 'RelationExpression' + | 'UnallowedRelation' + | 'InvalidGraph'; + + export class ValidationError extends Error { + constructor(args: CreateValidationErrorArgs & { modelClass?: ModelClass }); + + statusCode: number; + message: string; + data?: ErrorHash | any; + type: ValidationErrorType | string; + modelClass: ModelClass; + } + + export interface ValidationErrorItem { + message: string; + keyword: string; + params: Pojo; + } + + export interface ErrorHash { + [columnName: string]: ValidationErrorItem[]; + } + + export interface CreateValidationErrorArgs { + statusCode?: number; + message?: string; + data?: ErrorHash | any; + // This can be any string for custom errors. ValidationErrorType is there + // only to document the default values objection uses internally. + type: ValidationErrorType | string; + } + + export class NotFoundError extends Error { + constructor(args: CreateNotFoundErrorArgs & { modelClass?: ModelClass }); + + statusCode: number; + data?: any; + type: 'NotFound'; + modelClass: ModelClass; + } + + export interface CreateNotFoundErrorArgs { + statusCode?: number; + message?: string; + data?: any; + [key: string]: any; + } + + export interface TableMetadata { + columns: Array; + } + + export interface TableMetadataOptions { + table: string; + } + + export interface FetchTableMetadataOptions { + knex?: Knex; + force?: boolean; + table?: string; + } + + export interface Constructor { + new (): T; + } + + export interface ModelConstructor extends Constructor {} + + export interface ModelClass extends ModelConstructor { + QueryBuilder: typeof QueryBuilder; + + tableName: string; + idColumn: string | string[]; + jsonSchema: JSONSchema; + relationMappings: RelationMappings | RelationMappingsThunk; + modelPaths: string[]; + jsonAttributes: string[]; + virtualAttributes: string[]; + uidProp: string; + uidRefProp: string; + dbRefProp: string; + propRefRegex: RegExp; + pickJsonSchemaProperties: boolean; + relatedFindQueryMutates: boolean; + relatedInsertQueryMutates: boolean; + useLimitInFirst: boolean; + modifiers: Modifiers; + columnNameMappers: ColumnNameMappers; + + raw: RawFunction; + ref: ReferenceFunction; + fn: FunctionFunction; + + BelongsToOneRelation: RelationType; + HasOneRelation: RelationType; + HasManyRelation: RelationType; + ManyToManyRelation: RelationType; + HasOneThroughRelation: RelationType; + + defaultGraphOptions?: GraphOptions; + + query(this: Constructor, trxOrKnex?: TransactionOrKnex): QueryBuilderType; + + relatedQuery( + relationName: K, + trxOrKnex?: TransactionOrKnex, + ): ArrayRelatedQueryBuilder; + + relatedQuery( + relationName: string, + trxOrKnex?: TransactionOrKnex, + ): QueryBuilderType; + + fromJson(json: object, opt?: ModelOptions): M; + fromDatabaseJson(json: object): M; + + columnNameToPropertyName(columnName: string): string; + propertyNameToColumnName(propertyName: string): string; + + createValidator(): Validator; + createValidationError(args: CreateValidationErrorArgs): Error; + createNotFoundError(queryContext: QueryContext, args: CreateNotFoundErrorArgs): Error; + + tableMetadata(opt?: TableMetadataOptions): TableMetadata; + fetchTableMetadata(opt?: FetchTableMetadataOptions): Promise; + + knex(knex?: Knex): Knex; + knexQuery(): Knex.QueryBuilder; + startTransaction(knexOrTransaction?: TransactionOrKnex): Promise; + + transaction(callback: (trx: Transaction) => Promise): Promise; + transaction( + trxOrKnex: TransactionOrKnex, + callback: (trx: Transaction) => Promise, + ): Promise; + + bindKnex(trxOrKnex: TransactionOrKnex): this; + bindTransaction(trxOrKnex: TransactionOrKnex): this; + + fetchGraph( + modelOrObject: PartialModelObject, + expression: RelationExpression, + options?: FetchGraphOptions, + ): SingleQueryBuilder>; + + fetchGraph( + modelOrObject: PartialModelObject[], + expression: RelationExpression, + options?: FetchGraphOptions, + ): QueryBuilderType; + + getRelations(): Relations; + getRelation(name: string): Relation; + + traverse(models: Model | Model[], traverser: TraverserFunction): void; + traverse( + filterConstructor: ModelConstructor, + models: Model | Model[], + traverser: TraverserFunction, + ): void; + traverseAsync(models: Model | Model[], traverser: TraverserFunction): Promise; + traverseAsync( + filterConstructor: ModelConstructor, + models: Model | Model[], + traverser: TraverserFunction, + ): Promise; + + beforeFind(args: StaticHookArguments): any; + afterFind(args: StaticHookArguments): any; + beforeInsert(args: StaticHookArguments): any; + afterInsert(args: StaticHookArguments): any; + beforeUpdate(args: StaticHookArguments): any; + afterUpdate(args: StaticHookArguments): any; + beforeDelete(args: StaticHookArguments): any; + afterDelete(args: StaticHookArguments): any; + } + + export class Model { + static QueryBuilder: typeof QueryBuilder; + + static tableName: string; + static idColumn: string | string[]; + static jsonSchema: JSONSchema; + static relationMappings: RelationMappings | RelationMappingsThunk; + static modelPaths: string[]; + static jsonAttributes: string[]; + static virtualAttributes: string[]; + static uidProp: string; + static uidRefProp: string; + static dbRefProp: string; + static propRefRegex: RegExp; + static pickJsonSchemaProperties: boolean; + static relatedFindQueryMutates: boolean; + static relatedInsertQueryMutates: boolean; + static useLimitInFirst: boolean; + static modifiers: Modifiers; + static columnNameMappers: ColumnNameMappers; + + static raw: RawFunction; + static ref: ReferenceFunction; + static fn: FunctionFunction; + + static BelongsToOneRelation: RelationType; + static HasOneRelation: RelationType; + static HasManyRelation: RelationType; + static ManyToManyRelation: RelationType; + static HasOneThroughRelation: RelationType; + + static defaultGraphOptions?: GraphOptions; + + static query( + this: Constructor, + trxOrKnex?: TransactionOrKnex, + ): QueryBuilderType; + + static relatedQuery( + this: Constructor, + relationName: K, + trxOrKnex?: TransactionOrKnex, + ): ArrayRelatedQueryBuilder; + + static relatedQuery( + relationName: string, + trxOrKnex?: TransactionOrKnex, + ): QueryBuilderType; + + static fromJson(this: Constructor, json: object, opt?: ModelOptions): M; + static fromDatabaseJson(this: Constructor, json: object): M; + + static columnNameToPropertyName(columnName: string): string; + static propertyNameToColumnName(propertyName: string): string; + + static createValidator(): Validator; + static createValidationError(args: CreateValidationErrorArgs): Error; + static createNotFoundError(queryContext: QueryContext, args: CreateNotFoundErrorArgs): Error; + + static tableMetadata(opt?: TableMetadataOptions): TableMetadata; + static fetchTableMetadata(opt?: FetchTableMetadataOptions): Promise; + + static knex(knex?: Knex): Knex; + static knexQuery(): Knex.QueryBuilder; + static startTransaction(knexOrTransaction?: TransactionOrKnex): Promise; + + static transaction(callback: (trx: Transaction) => Promise): Promise; + static transaction( + trxOrKnex: TransactionOrKnex, + callback: (trx: Transaction) => Promise, + ): Promise; + + static bindKnex(this: M, trxOrKnex: TransactionOrKnex): M; + static bindTransaction(this: M, trxOrKnex: TransactionOrKnex): M; + + static fetchGraph( + this: Constructor, + modelOrObject: PartialModelObject, + expression: RelationExpression, + options?: FetchGraphOptions, + ): SingleQueryBuilder>; + + static fetchGraph( + this: Constructor, + modelOrObject: PartialModelObject[], + expression: RelationExpression, + options?: FetchGraphOptions, + ): QueryBuilderType; + + static getRelations(): Relations; + static getRelation(name: string): Relation; + + static traverse(models: Model | Model[], traverser: TraverserFunction): void; + static traverse( + filterConstructor: typeof Model, + models: Model | Model[], + traverser: TraverserFunction, + ): void; + static traverseAsync(models: Model | Model[], traverser: TraverserFunction): Promise; + static traverseAsync( + filterConstructor: typeof Model, + models: Model | Model[], + traverser: TraverserFunction, + ): Promise; + + static beforeFind(args: StaticHookArguments): any; + static afterFind(args: StaticHookArguments): any; + static beforeInsert(args: StaticHookArguments): any; + static afterInsert(args: StaticHookArguments): any; + static beforeUpdate(args: StaticHookArguments): any; + static afterUpdate(args: StaticHookArguments): any; + static beforeDelete(args: StaticHookArguments): any; + static afterDelete(args: StaticHookArguments): any; + + $modelClass: ModelClass; + + $relatedQuery( + relationName: K, + trxOrKnex?: TransactionOrKnex, + ): RelatedQueryBuilder; + + $relatedQuery( + relationName: string, + trxOrKnex?: TransactionOrKnex, + ): QueryBuilderType; + + $query(trxOrKnex?: TransactionOrKnex): SingleQueryBuilder>; + + $id(id: any): void; + $id(): any; + + $fetchGraph( + expression: RelationExpression, + options?: FetchGraphOptions, + ): SingleQueryBuilder>; + + $formatDatabaseJson(json: Pojo): Pojo; + $parseDatabaseJson(json: Pojo): Pojo; + + $formatJson(json: Pojo): Pojo; + $parseJson(json: Pojo, opt?: ModelOptions): Pojo; + + $beforeValidate(jsonSchema: JSONSchema, json: Pojo, opt: ModelOptions): JSONSchema; + $validate(json?: Pojo, opt?: ModelOptions): Pojo; // may throw ValidationError if validation fails + $afterValidate(json: Pojo, opt: ModelOptions): void; // may throw ValidationError if validation fails + + $beforeInsert(queryContext: QueryContext): Promise | void; + $afterInsert(queryContext: QueryContext): Promise | void; + $afterUpdate(opt: ModelOptions, queryContext: QueryContext): Promise | void; + $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise | void; + $afterFind(queryContext: QueryContext): Promise | void; + $beforeDelete(queryContext: QueryContext): Promise | void; + $afterDelete(queryContext: QueryContext): Promise | void; + + $toDatabaseJson(): Pojo; + $toJson(opt?: ToJsonOptions): ModelObject; + toJSON(opt?: ToJsonOptions): ModelObject; + + $setJson(json: object, opt?: ModelOptions): this; + $setDatabaseJson(json: object): this; + + $setRelated( + relation: String | Relation, + related: RM | RM[] | null | undefined, + ): this; + + $appendRelated( + relation: String | Relation, + related: RM | RM[] | null | undefined, + ): this; + + $set(obj: Pojo): this; + $clone(opt?: CloneOptions): this; + $traverse(filterConstructor: typeof Model, traverser: TraverserFunction): this; + $traverse(traverser: TraverserFunction): this; + $traverseAsync(filterConstructor: typeof Model, traverser: TraverserFunction): Promise; + $traverseAsync(traverser: TraverserFunction): Promise; + $omitFromJson(keys: string | string[] | { [key: string]: boolean }): this; + $omitFromDatabaseJson(keys: string | string[] | { [key: string]: boolean }): this; + + $knex(): Knex; + $transaction(): Knex; + + QueryBuilderType: QueryBuilder; + } + + /** + * Overloading is required here until the following issues (at least) are resolved: + * + * - https://github.com/microsoft/TypeScript/issues/1360 + * - https://github.com/Microsoft/TypeScript/issues/5453 + * + * @tutorial https://vincit.github.io/objection.js/guide/transactions.html#creating-a-transaction + */ + export interface transaction { + start(knexOrModel: Knex | AnyModelConstructor): Promise; + + ( + modelClass1: MC1, + callback: (boundModelClass: MC1, trx?: Transaction) => Promise, + ): Promise; + + ( + modelClass1: MC1, + modelClass2: MC2, + callback: ( + boundModelClass1: MC1, + boundModelClass2: MC2, + trx?: Transaction, + ) => Promise, + ): Promise; + + < + MC1 extends AnyModelConstructor, + MC2 extends AnyModelConstructor, + MC3 extends AnyModelConstructor, + ReturnValue, + >( + modelClass1: MC1, + modelClass2: MC2, + modelClass3: MC3, + callback: ( + boundModelClass1: MC1, + boundModelClass2: MC2, + boundModelClass3: MC3, + trx?: Transaction, + ) => Promise, + ): Promise; + + < + MC1 extends AnyModelConstructor, + MC2 extends AnyModelConstructor, + MC3 extends AnyModelConstructor, + MC4 extends AnyModelConstructor, + ReturnValue, + >( + modelClass1: MC1, + modelClass2: MC2, + modelClass3: MC3, + modelClass4: MC4, + callback: ( + boundModelClass1: MC1, + boundModelClass2: MC2, + boundModelClass3: MC3, + boundModelClass4: MC4, + trx?: Transaction, + ) => Promise, + ): Promise; + + < + MC1 extends AnyModelConstructor, + MC2 extends AnyModelConstructor, + MC3 extends AnyModelConstructor, + MC4 extends AnyModelConstructor, + MC5 extends AnyModelConstructor, + ReturnValue, + >( + modelClass1: MC1, + modelClass2: MC2, + modelClass3: MC3, + modelClass4: MC4, + modelClass5: MC5, + callback: ( + boundModelClass1: MC1, + boundModelClass2: MC2, + boundModelClass3: MC3, + boundModelClass4: MC4, + boundModelClass5: MC5, + trx?: Transaction, + ) => Promise, + ): Promise; + + ( + knex: Knex, + callback: (trx: Transaction) => Promise, + ): Promise; + } + + interface initialize { + (knex: Knex, modelClasses: AnyModelConstructor[]): Promise; + (modelClasses: AnyModelConstructor[]): Promise; + } + + /** + * JSON Schema 7 + * Draft 07 + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01 + * + * These definitions were written by + * + * Boris Cherny https://github.com/bcherny, + * Cyrille Tuzi https://github.com/cyrilletuzi, + * Lucian Buzzo https://github.com/lucianbuzzo, + * Roland Groza https://github.com/rolandjitsu. + * + * https://www.npmjs.com/package/@types/json-schema + */ + + /** + * Primitive type + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1 + */ + export type JSONSchemaTypeName = + | 'string' + | 'number' + | 'integer' + | 'boolean' + | 'object' + | 'array' + | 'null' + | string; + + export type JSONSchemaType = JSONSchemaArray[] | boolean | number | null | object | string; + + // Workaround for infinite type recursion + // https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 + export interface JSONSchemaArray extends Array {} + + /** + * Meta schema + * + * Recommended values: + * - 'http://json-schema.org/schema#' + * - 'http://json-schema.org/hyper-schema#' + * - 'http://json-schema.org/draft-07/schema#' + * - 'http://json-schema.org/draft-07/hyper-schema#' + * + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-5 + */ + export type JSONSchemaVersion = string; + + /** + * JSON Schema v7 + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01 + */ + export type JSONSchemaDefinition = JSONSchema | boolean; + export interface JSONSchema { + $id?: string; + $ref?: string; + $schema?: JSONSchemaVersion; + $comment?: string; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1 + */ + type?: JSONSchemaTypeName | JSONSchemaTypeName[]; + enum?: JSONSchemaType[]; + const?: JSONSchemaType; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.2 + */ + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.3 + */ + maxLength?: number; + minLength?: number; + pattern?: string; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4 + */ + items?: JSONSchemaDefinition | JSONSchemaDefinition[]; + additionalItems?: JSONSchemaDefinition; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + contains?: JSONSchema; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.5 + */ + maxProperties?: number; + minProperties?: number; + required?: string[]; + properties?: { + [key: string]: JSONSchemaDefinition; + }; + patternProperties?: { + [key: string]: JSONSchemaDefinition; + }; + additionalProperties?: JSONSchemaDefinition; + dependencies?: { + [key: string]: JSONSchemaDefinition | string[]; + }; + propertyNames?: JSONSchemaDefinition; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.6 + */ + if?: JSONSchemaDefinition; + then?: JSONSchemaDefinition; + else?: JSONSchemaDefinition; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.7 + */ + allOf?: JSONSchemaDefinition[]; + anyOf?: JSONSchemaDefinition[]; + oneOf?: JSONSchemaDefinition[]; + not?: JSONSchemaDefinition; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7 + */ + format?: string; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-8 + */ + contentMediaType?: string; + contentEncoding?: string; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9 + */ + definitions?: { + [key: string]: JSONSchemaDefinition; + }; + + /** + * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-10 + */ + title?: string; + description?: string; + default?: JSONSchemaType; + readOnly?: boolean; + writeOnly?: boolean; + examples?: JSONSchemaType; + } +}