UNPKG

@decaf-ts/core

Version:

Core persistence module for the decaf framework

435 lines 16.7 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var _a, _b, _c, _d; import { Model } from "@decaf-ts/decorator-validation"; import { Condition } from "./Condition.js"; import { prefixMethod } from "@decaf-ts/db-decorators"; import { final, toCamelCase } from "@decaf-ts/logging"; import { Adapter, PersistenceKeys, UnsupportedError, } from "./../persistence/index.js"; import { QueryError } from "./errors.js"; import { ContextualLoggedClass, } from "./../utils/index.js"; import { Context } from "./../persistence/Context.js"; import { QueryClause } from "./types.js"; import { GroupOperator, Operator, PreparedStatementKeys } from "./constants.js"; import { OrderDirection } from "./../repository/constants.js"; import { Repository } from "./../repository/Repository.js"; /** * @description Base class for database query statements * @summary Provides a foundation for building and executing database queries * * This abstract class implements the query builder pattern for constructing * database queries. It supports various query operations like select, from, * where, orderBy, groupBy, limit, and offset. It also provides methods for * executing queries and handling pagination. * * @template Q - The query type specific to the database adapter * @template M - The model type this statement operates on * @template R - The return type of the query * @param {Adapter<any, Q, any, any>} adapter - The database adapter to use for executing queries * @class Statement * @example * // Create a statement to query users * const statement = new SQLStatement(adapter); * const users = await statement * .select() * .from(User) * .where(Condition.attribute("status").eq("active")) * .orderBy(["createdAt", "DESC"]) * .limit(10) * .execute(); * * // Use pagination * const paginator = await statement * .select() * .from(User) * .paginate(20); // 20 users per page * * @mermaid * sequenceDiagram * participant Client * participant Statement * participant Adapter * participant Database * * Client->>Statement: select() * Client->>Statement: from(Model) * Client->>Statement: where(condition) * Client->>Statement: orderBy([field, direction]) * Client->>Statement: limit(value) * Client->>Statement: execute() * Statement->>Statement: build() * Statement->>Adapter: raw(query) * Adapter->>Database: execute query * Database-->>Adapter: return results * Adapter-->>Statement: return processed results * Statement-->>Client: return final results */ export class Statement extends ContextualLoggedClass { constructor(adapter, overrides) { super(); this.adapter = adapter; this.overrides = overrides; [this.execute, this.paginate].forEach((m) => { prefixMethod(this, m, async (...args) => { let execArgs = args; if ((!execArgs.length || !(execArgs[execArgs.length - 1] instanceof Context)) && this.fromSelector) { const ctx = await this.adapter.context(PersistenceKeys.QUERY, this.overrides || {}, this.fromSelector); execArgs = [...execArgs, ctx]; } const { ctx, ctxArgs } = Adapter.logCtx(execArgs, m.name); const forceSimple = ctx.get("forcePrepareSimpleQueries"); const forceComplex = ctx.get("forcePrepareComplexQueries"); if ((forceSimple && this.isSimpleQuery()) || forceComplex) await this.prepare(ctx); return ctxArgs; }, m.name); }); } get log() { return this.adapter.log.for(Statement); } select(selector) { Object.defineProperty(this, "selectSelector", { value: selector, writable: false, }); return this; } distinct(selector) { this.distinctSelector = selector; return this; } max(selector) { this.maxSelector = selector; return this; } min(selector) { this.minSelector = selector; return this; } count(selector) { this.countSelector = selector; return this; } from(selector) { this.fromSelector = (typeof selector === "string" ? Model.get(selector) : selector); if (!this.fromSelector) throw new QueryError(`Could not find selector model: ${selector}`); return this; } where(condition) { this.whereCondition = condition; return this; } orderBy(selector) { this.orderBySelector = selector; return this; } groupBy(selector) { this.groupBySelector = selector; return this; } limit(value) { this.limitSelector = value; return this; } offset(value) { this.offsetSelector = value; return this; } async execute(...args) { try { if (this.prepared) return this.executePrepared(...args); const query = this.build(); return (await this.raw(query, ...args)); } catch (e) { throw new QueryError(e); } } async executePrepared(...argz) { const repo = Repository.forModel(this.fromSelector, this.adapter.alias); const { method, args, params } = this.prepared; return repo.statement(method, ...args, params, ...argz); } async raw(rawInput, ...args) { const { ctx, ctxArgs } = this.logCtx(args, this.raw); const allowRawStatements = ctx.get("allowRawStatements"); if (!allowRawStatements) throw new UnsupportedError("Raw statements are not allowed in the current configuration"); const results = await this.adapter.raw(rawInput, true, ...ctxArgs); if (!this.selectSelector) { return results; } const pkAttr = Model.pk(this.fromSelector); const processor = function recordProcessor(r) { const id = r[pkAttr]; return this.adapter.revert(r, this.fromSelector, id, undefined, ctx); }.bind(this); if (Array.isArray(results)) return results.map(processor); return processor(results); } prepareCondition(condition, ctx) { // @ts-expect-error accessing protected properties // eslint-disable-next-line prefer-const let { attr1, operator, comparison } = condition; const result = {}; switch (operator) { case GroupOperator.AND: case GroupOperator.OR: { let side1 = attr1, side2 = comparison; if (typeof attr1 !== "string") { const condition1 = this.prepareCondition(attr1, ctx); side1 = condition1.method; result.args = [...(result.args || []), ...(condition1.args || [])]; } if (comparison instanceof Condition) { const condition2 = this.prepareCondition(comparison, ctx); side2 = condition2.method; result.args = [...(result.args || []), ...(condition2.args || [])]; } result.method = `${side1} ${operator.toLowerCase()} ${side2}`; break; } case Operator.EQUAL: result.method = attr1; result.args = [...(result.args || []), comparison]; break; case Operator.DIFFERENT: result.method = `${attr1} diff`; result.args = [...(result.args || []), comparison]; break; case Operator.REGEXP: result.method = `${attr1} matches`; result.args = [...(result.args || []), comparison]; break; case Operator.BIGGER: result.method = `${attr1} bigger`; result.args = [...(result.args || []), comparison]; break; case Operator.BIGGER_EQ: result.method = `${attr1} bigger than equal`; break; case Operator.SMALLER: result.method = `${attr1} less`; result.args = [...(result.args || []), comparison]; break; case Operator.SMALLER_EQ: result.method = `${attr1} less than equal`; result.args = [...(result.args || []), comparison]; break; case Operator.IN: result.method = `${attr1} in`; result.args = [...(result.args || []), comparison]; break; default: throw new QueryError(`Unsupported operator ${operator}`); } return result; } // eslint-disable-next-line @typescript-eslint/no-unused-vars squash(ctx) { if (this.selectSelector && this.selectSelector.length) return undefined; if (this.groupBySelector) return undefined; if (this.countSelector) return undefined; if (this.maxSelector) return undefined; if (this.minSelector) return undefined; let attrFromWhere; if (this.whereCondition) { // if (this.orderBySelector) return undefined; if (this.whereCondition["comparison"] instanceof Condition) return undefined; attrFromWhere = this.whereCondition["attr1"]; } const order = this.orderBySelector ? this.orderBySelector : attrFromWhere ? [attrFromWhere, OrderDirection.DSC] : [Model.pk(this.fromSelector), OrderDirection.DSC]; const [attrFromOrderBy, sort] = order; const params = { direction: sort, }; if (this.limitSelector) params.limit = this.limitSelector; if (this.offsetSelector) params.offset = this.offsetSelector; const squashed = { // listBy class: this.fromSelector, method: PreparedStatementKeys.LIST_BY, args: [attrFromOrderBy], params: params, }; if (attrFromWhere) { // findBy squashed.method = PreparedStatementKeys.FIND_BY; squashed.args = [ attrFromWhere, this.whereCondition["comparison"], ]; squashed.params = params; } return squashed; } async prepare(ctx) { ctx = ctx || (await this.adapter.context(PersistenceKeys.QUERY, this.overrides || {}, this.fromSelector)); if (this.isSimpleQuery() && ctx.get("forcePrepareSimpleQueries")) { const squashed = this.squash(ctx); if (squashed) { this.prepared = squashed; return this; } } const args = []; const params = {}; const prepared = { class: this.fromSelector, args, params, }; const method = [QueryClause.FIND_BY]; if (this.whereCondition) { const parsed = this.prepareCondition(this.whereCondition, ctx); method.push(parsed.method); if (parsed.args && parsed.args.length) args.push(...parsed.args); } if (this.selectSelector) method.push(QueryClause.SELECT, this.selectSelector.join(` ${QueryClause.AND.toLowerCase()} `)); if (this.orderBySelector) { method.push(QueryClause.ORDER_BY, this.orderBySelector[0]); params.direction = this.orderBySelector[1]; } if (this.groupBySelector) method.push(QueryClause.GROUP_BY, this.groupBySelector); if (this.limitSelector) params.limit = this.limitSelector; if (this.offsetSelector) { params.skip = this.offsetSelector; } prepared.method = toCamelCase(method.join(" ")); prepared.params = params; this.prepared = prepared; return this; } isSimpleQuery() { return !((this.selectSelector && this.selectSelector.length) || this.groupBySelector || this.countSelector || this.maxSelector || this.minSelector); } /** * @description Creates a paginator for the query * @summary Builds the query and wraps it in a RamPaginator to enable pagination of results. * This allows retrieving large result sets in smaller chunks. * @param {number} size - The page size (number of results per page) * @return {Promise<Paginator<M, R, RawRamQuery<M>>>} A promise that resolves to a paginator for the query */ async paginate(size, ...args) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const ctx = args.pop(); // handled by prefix. kept for example for overrides try { return this.adapter.Paginator(this.prepared || this.build(), size, this.fromSelector); } catch (e) { throw new QueryError(e); } } toString() { return `${this.adapter.flavour} statement`; } } __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Array]), __metadata("design:returntype", Object) ], Statement.prototype, "select", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [typeof (_a = typeof S !== "undefined" && S) === "function" ? _a : Object]), __metadata("design:returntype", Object) ], Statement.prototype, "distinct", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [typeof (_b = typeof S !== "undefined" && S) === "function" ? _b : Object]), __metadata("design:returntype", Object) ], Statement.prototype, "max", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [typeof (_c = typeof S !== "undefined" && S) === "function" ? _c : Object]), __metadata("design:returntype", Object) ], Statement.prototype, "min", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [typeof (_d = typeof S !== "undefined" && S) === "function" ? _d : Object]), __metadata("design:returntype", Object) ], Statement.prototype, "count", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Object) ], Statement.prototype, "from", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Condition]), __metadata("design:returntype", Object) ], Statement.prototype, "where", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Array]), __metadata("design:returntype", Object) ], Statement.prototype, "orderBy", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Object) ], Statement.prototype, "groupBy", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Number]), __metadata("design:returntype", Object) ], Statement.prototype, "limit", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [Number]), __metadata("design:returntype", Object) ], Statement.prototype, "offset", null); __decorate([ final(), __metadata("design:type", Function), __metadata("design:paramtypes", [void 0]), __metadata("design:returntype", Promise) ], Statement.prototype, "execute", null); //# sourceMappingURL=Statement.js.map