UNPKG

tspace-mysql

Version:

Tspace MySQL is a promise-based ORM for Node.js, designed with modern TypeScript and providing type safety for schema databases.

1,452 lines 220 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Model = void 0; require("reflect-metadata"); const pluralize_1 = __importDefault(require("pluralize")); const DB_1 = require("./DB"); const Schema_1 = require("./Schema"); const AbstractModel_1 = require("./Abstracts/AbstractModel"); const RelationManager_1 = require("./RelationManager"); const StateManager_1 = require("./StateManager"); const Cache_1 = require("./Cache"); const JoinModel_1 = require("./JoinModel"); const constants_1 = require("../constants"); const Decorator_1 = require("./Decorator"); let globalSettings = { softDelete: false, debug: false, uuid: false, timestamp: false, logger: { selected: false, inserted: false, updated: false, deleted: false, }, }; /** * * The 'Model' class is a representation of a database table * @generic {Type} TS * @generic {Type} TR * @example * import { Model, Blueprint, type T } from 'tspace-mysql' * * const schema = { * id : new Blueprint().int().primary().autoIncrement(), * uuid : new Blueprint().varchar(50).null(), * email : new Blueprint().varchar(50).null(), * name : new Blueprint().varchar(255).null(), * } * * type TS = T.Schema<typeof Schema> * * class User extends Model<TS> { * boot() { * this.useSchema(schema) * } * } * * const users = await new User().findMany() * console.log(users) */ class Model extends AbstractModel_1.AbstractModel { constructor() { super(); /** * * @Get initialize for model */ this._initialModel(); /** * * @boot Setup for model * same as using constructor */ this.boot(); } /** * The 'global' method is used setting global variables in models. * @static * @param {GlobalSetting} settings * @example * Model.global({ * softDelete : true, * uuid : true, * timestamp : true, * debug : true * logger : { * selected : true, * inserted : true, * updated : true, * deleted : true * }, * }) * @returns {void} void */ static global(settings) { globalSettings = Object.assign({}, globalSettings, settings); return; } /** * The 'table' method is used get table name. * @static * @returns {string} name of the table */ static get table() { return new this().getTableName(); } /** * The 'formatPattern' method is used to change the format of the pattern. * @param {object} data { data , pattern } * @property {Record | string} data * @property {string} parttern * @returns {Record | string} T */ static formatPattern({ data, pattern, }) { const utils = new this().$utils; if (pattern === "snake_case") { if (typeof data === "string") { return data.replace(/([A-Z])/g, (str) => `_${str.toLocaleLowerCase()}`); } return utils.snakeCase({ ...data }); } if (pattern === "camelCase") { if (typeof data === "string") { return data.replace(/(.(_|-|\s)+.)/g, (str) => str[0] + str[str.length - 1].toUpperCase()); } return utils.camelCase({ ...data }); } return data; } /** * The 'instance' method is used get instance. * @override * @static * @returns {Model} instance of the Model */ static get instance() { return new this(); } /** * The 'cache' method is used get the functions from the Cache * @returns {TCache} cache */ static get cache() { return Cache_1.Cache; } /** * The 'boot' method is a special method that you can define within a model. * @example * class User extends Model { * boot() { * this.useUUID() * this.usePrimaryKey('id') * this.useTimestamp() * this.useSoftDelete() * } * } * @returns {void} void */ boot() { } /** * The 'globalScope' method is a feature that allows you to apply query constraints to all queries for a given model. * * Suported only methods -> select , except , where , orderBy, GroupBy , limit and offset * @example * class User extends Model { * boot() { * super() * this.globalScope(query => { * return query.where('id' , '>' , 10) * }) * } * } * @returns {void} void */ globalScope(callback) { const model = new Model().copyModel(this); const repository = callback(model); if (repository instanceof Promise) throw new Error('"globalScope" is not supported a Promise'); if (!(repository instanceof Model)) throw new Error(`Unknown callback query: '${repository}'`); this.$state.set("GLOBAL_SCOPE_QUERY", () => { const select = repository?.$state.get("SELECT") || []; const except = repository?.$state.get("EXCEPTS") || []; const where = repository?.$state.get("WHERE") || []; const groupBy = repository?.$state.get("GROUP_BY") || []; const orderBy = repository?.$state.get("ORDER_BY") || []; const limit = repository?.$state.get("LIMIT") || null; const offset = repository?.$state.get("OFFSET") || null; if (select.length) { this.$state.set("SELECT", [...this.$state.get("SELECT"), ...select]); } if (except.length) { this.$state.set("EXCEPTS", [ ...this.$state.get("EXCEPTS"), ...except ]); } if (where.length) { this.$state.set("WHERE", [ ...this.$state.get("WHERE"), [ this.$state.get("WHERE").length ? `${this.$constants("AND")}` : "", ...where, ].join(" "), ]); } if (groupBy.length) { this.$state.set("GROUP_BY", [ ...this.$state.get("GROUP_BY"), ...groupBy, ]); } if (orderBy.length) { this.$state.set("ORDER_BY", [ ...this.$state.get("ORDER_BY"), ...orderBy, ]); } if (limit != null) { this.$state.set("LIMIT", this.$state.get("LIMIT") ?? limit); } if (offset != null) { this.$state.set("OFFSET", this.$state.get("OFFSET") ?? offset); } }); return this; } /** * The 'useGlobalScope' method is a feature that allows you to apply query constraints to all queries for a given model. * * Suported only methods -> select , except , where , orderBy, GroupBy , limit and offset * * @example * class User extends Model { * boot() { * super() * this.useGlobalScope(query => { * return query.where('id' , '>' , 10) * }) * } * } * @returns {void} void */ useGlobalScope(callback) { return this.globalScope(callback); } /** * The "useObserve" method is used to pattern refers to a way of handling model events using observer classes. * Model events are triggered when certain actions occur on models, * such as creating, updating, deleting, or saving a record. * * Observers are used to encapsulate the event-handling logic for these events, * keeping the logic separate from the model itself and promoting cleaner, more maintainable code. * @param {Function} observer * @returns this * @example * * class UserObserve { * public selected(results : unknown) { * console.log({ results , selected : true }) * } * * public created(results : unknown) { * console.log({ results , created : true }) * } * * public updated(results : unknown) { * console.log({ results ,updated : true }) * } * * public deleted(results : unknown) { * console.log({ results ,deleted : true }) * } * } * * class User extends Model { * boot() { * super() * this.useObserver(UserObserve) * } * } */ useObserver(observer) { this.$state.set("OBSERVER", observer); return this; } /** * The "useLogger" method is used to keeping query data and changed in models. * * @type {object} options * @property {boolean} options.selected - default is false * @property {boolean} options.inserted - default is true * @property {boolean} options.updated - default is true * @property {boolean} options.deleted - default is true * @example * class User extends Model { * boot() { * this.useLogger({ * selected : true, * inserted : true, * updated : true, * deleted : true, * }) * } * } * @returns {this} this */ useLogger({ selected = false, inserted = true, updated = true, deleted = true, } = {}) { this.$state.set("LOGGER", ![selected, inserted, updated, deleted].every((v) => v === false)); this.$state.set("LOGGER_OPTIONS", { selected, inserted, updated, deleted, }); return this; } /** * The "useSchema" method is used to define the schema. * * It's automatically create, called when not exists table or columns. * @param {object} schema using Blueprint for schema * @example * import { Blueprint } from 'tspace-mysql'; * class User extends Model { * boot() { * this.useSchema ({ * id : new Blueprint().int().notNull().primary().autoIncrement(), * uuid : new Blueprint().varchar(50).null(), * email : new Blueprint().varchar(50).null(), * name : new Blueprint().varchar(255).null(), * created_at : new Blueprint().timestamp().null(), * updated_at : new Blueprint().timestamp().null() * }) * } * } * @returns {this} this */ useSchema(schema) { this.$state.set("SCHEMA_TABLE", schema); return this; } /** * The "useTransform " method is used to define value transformers for model columns.. * * Each transformer defines how a value is converted: * - `to` : before persisting to the database * - `from` : after retrieving from the database * * Transformers can be synchronous or asynchronous. * * @param {object} transforms * @example * import { Blueprint } from 'tspace-mysql'; * class User extends Model { * boot() { * this.useTransform({ * name : { * to : async (v) => `${v}-> transform@before`, * from : async (v) => `${v}-> transform@after`, * }, * }) * } * } * @returns {this} this */ useTransform(transforms) { this.$state.set('TRANSFORMS', transforms); return this; } /** * The "usePrimaryKey" method is add primary keys for database tables. * * @param {string} primary * @returns {this} this * @example * class User extends Model { * boot() { * this.usePrimaryKey() * } * } */ usePrimaryKey(primary) { this.$state.set("PRIMARY_KEY", primary); return this; } /** * The "useUUID" method is a concept of using UUIDs (Universally Unique Identifiers) as column 'uuid' in table. * * It's automatically genarate when created a result. * @param {string?} column [column=uuid] make new name column for custom column replace uuid with this * @returns {this} this * @example * class User extends Model { * boot() { * this.useUUID() * } * } */ useUUID(column) { this.$state.set("UUID", true); if (column) this.$state.set("UUID_FORMAT", column); return this; } /** * The "useDebug" method is viewer raw-sql logs when excute the results. * @returns {this} this */ useDebug() { this.$state.set("DEBUG", true); return this; } /** * The "usePattern" method is used to assign pattern [snake_case , camelCase]. * @param {string} pattern * @returns {this} this * @example * class User extends Model { * boot() { * this.usePattern('camelCase') * } * } */ usePattern(pattern) { const allowPattern = [ this.$constants("PATTERN").snakeCase, this.$constants("PATTERN").camelCase, ]; if (!allowPattern.includes(pattern)) { throw this._assertError(`The 'tspace-mysql' support only pattern '${this.$constants("PATTERN").snakeCase}', '${this.$constants("PATTERN").camelCase}'`); } this.$state.set("PATTERN", pattern); this._makeTableName(); return this; } /** * The "useCamelCase" method is used to assign pattern camelCase. * @returns {this} this * @example * class User extends Model { * boot() { * this.useCamelCase() * } * } */ useCamelCase() { this.$state.set("PATTERN", this.$constants("PATTERN").camelCase); this._makeTableName(); return this; } /** * The "SnakeCase" method is used to assign pattern snake_case. * @returns {this} this * @example * class User extends Model { * boot() { * this.SnakeCase() * } * } */ useSnakeCase() { this.$state.set("PATTERN", this.$constants("PATTERN").snakeCase); this._makeTableName(); return this; } /** * The "useSoftDelete" refer to a feature that allows you to "soft delete" records from a database table instead of permanently deleting them. * * Soft deleting means that the records are not physically removed from the database but are instead marked as deleted by setting a timestamp in a dedicated column. * * This feature is particularly useful when you want to retain a record of deleted data and potentially recover it later, * or when you want to maintain referential integrity in your database * @param {string?} column default deleted_at * @returns {this} this * @example * class User extends Model { * boot() { * this.useSoftDelete('deletedAt') * } * } */ useSoftDelete(column) { this.$state.set("SOFT_DELETE", true); if (column) this.$state.set("SOFT_DELETE_FORMAT", column); return this; } /** * The "useTimestamp" method is used to assign a timestamp when creating a new record, * or updating a record. * @param {object} timestampFormat * @property {string} timestampFormat.createdAt - change new name column replace by default [created at] * @property {string} timestampFormat.updatedAt - change new name column replace by default updated at * @returns {this} this * @example * class User extends Model { * boot() { * this.useTimestamp({ * createdAt : 'createdAt', * updatedAt : 'updatedAt' * }) * } * } */ useTimestamp(timestampFormat) { this.$state.set("TIMESTAMP", true); if (timestampFormat) { this.$state.set("TIMESTAMP_FORMAT", { CREATED_AT: timestampFormat.createdAt, UPDATED_AT: timestampFormat.updatedAt, }); } return this; } /** * This "useTable" method is used to assign the name of the table. * @param {string} table table name in database * @returns {this} this * @example * class User extends Model { * boot() { * this.useTable('setTableNameIsUser') // => 'setTableNameIsUser' * } * } */ useTable(table) { this.$state.set("TABLE_NAME", `\`${table.replace(/\`/g, "")}\``); return this; } /** * This "useTableSingular" method is used to assign the name of the table with signgular pattern. * @returns {this} this * @example * class User extends Model { * boot() { * this.useTableSingular() // => 'user' * } * } */ useTableSingular() { const table = this._classToTableName(this.constructor?.name, { singular: true, }); this.$state.set("TABLE_NAME", `\`${this._valuePattern(table)}\``); return this; } /** * This "useTablePlural " method is used to assign the name of the table with pluarl pattern * @returns {this} this * @example * class User extends Model { * boot() { * this.useTablePlural() // => 'users' * } * } */ useTablePlural() { const table = this._classToTableName(this.constructor?.name); this.$state.set("TABLE_NAME", `\`${this._valuePattern(table)}\``); return this; } /** * This 'useValidationSchema' method is used to validate the schema when have some action create or update. * @param {Object<ValidateSchema>} schema types (String Number and Date) * @returns {this} this * @example * class User extends Model { * boot() { * this.useValidationSchema({ * id : Number, * uuid : Number, * name : { * type : String, * require : true * // json : true, * // enum : ["1","2","3"] * }, * email : { * type : String, * require : true, * length : 199, * match: /^[a-zA-Z0-9._]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, * unique : true, * fn : async (email : string) => /^[a-zA-Z0-9._]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email) * }, * createdAt : Date, * updatedAt : Date, * deletedAt : Date * }) * } * } */ useValidationSchema(schema) { if (schema == null) return this; this.$state.set("VALIDATE_SCHEMA", true); this.$state.set("VALIDATE_SCHEMA_DEFINED", schema); return this; } /** * This 'useValidateSchema' method is used to validate the schema when have some action create or update. * @param {Object<ValidateSchema>} schema types (String Number and Date) * @returns {this} this * @example * class User extends Model { * boot() { * this.useValidationSchema({ * id : Number, * uuid : string, * name : { * type : String, * require : true * }, * email : { * type : String, * require : true, * length : 199, * // json : true, * // enum : ["1","2","3"] * match: /^[a-zA-Z0-9._]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, * unique : true, * fn : async (email : string) => /^[a-zA-Z0-9._]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email) * }, * createdAt : Date, * updatedAt : Date, * deletedAt : Date * }) * } * } */ useValidateSchema(schema) { return this.useValidationSchema(schema); } /** * The "useHooks" method is used to assign hook function when execute returned results to callback function. * @param {Function[]} funs functions for callback result * @returns {this} this * @example * class User extends Model { * boot() { * this.useHook([(results) => console.log(results)]) * } * } */ useHooks(funs) { for (const func of funs) { if (typeof func !== "function") throw this._assertError(`this 'function' is not a function`); this.$state.set("HOOKS", [...this.$state.get("HOOKS"), func]); } return this; } /** * The "useMiddleware" method is used to register functions that run before the main handler executes. * * @param {Function} func functions for execute * @returns {this} this * @example * class User extends Model { * boot() { * this.useMiddleware(() => function ...) * } * } */ useMiddleware(func) { if (typeof func !== "function") throw new Error(`this '${func}' is not a function`); this.$state.set("MIDDLEWARES", [...this.$state.get("MIDDLEWARES"), func]); return this; } /** * * The 'useLifecycle' method is used to register lifecycle hooks for model events. * * Supported lifecycle types: * - "beforeInsert" * - "afterInsert" * - "beforeUpdate" * - "afterUpdate" * - "beforeRemove" * - "afterRemove" * * Each hook is executed in the order they are registered. * * @param {LifecycleType} type * The lifecycle event to register. Determines which hook * collection the function(s) will be stored in. * * @param {Function | Function[]} funcs * A function or array of functions to be executed when the * lifecycle event is triggered. All functions must be valid * JavaScript functions, otherwise an assertion error is thrown. * * @throws {Error} * Throws if any provided item in `funcs` is not a function. * * @returns {this} * Returns the current instance to enable method chaining. * * @example * // Register a single hook * this.useLifecycle("beforeInsert", () => { * console.log("Before insert hook"); * }); * * @example * // Register multiple hooks * this.useLifecycle("afterUpdate", [ * () => console.log("Update #1"), * () => console.log("Update #2"), * ]); * * @example * // Method chaining * this * .useLifecycle("beforeInsert", fnA) * .useLifecycle("afterInsert", fnB); */ useLifecycle(type, funcs) { const MAP = { beforeInsert: "LIFECYCLE_BEFORE_INSERTS", afterInsert: "LIFECYCLE_AFTER_INSERTS", beforeUpdate: "LIFECYCLE_BEFORE_UPDATES", afterUpdate: "LIFECYCLE_AFTER_UPDATES", beforeRemove: "LIFECYCLE_BEFORE_REMOVES", afterRemove: "LIFECYCLE_AFTER_REMOVES", }; const lists = Array.isArray(funcs) ? funcs : [funcs]; for (const func of lists) { if (typeof func !== "function") { throw this._assertError(`this 'function' is not a function`); } } const key = MAP[type]; const current = this.$state.get(key); this.$state.set(key, [...current, ...lists]); return this; } /** * The "whenCreatingTable" method is used exection function when creating the table. * @param {Function} fn functions for executing when creating the table * @returns {this} this * @example * class User extends Model { * boot() { * this.whenCreatingTable(async () => { * await new User() * .create({ * ...columns * }) * .save() * }) * } * } */ whenCreatingTable(fn) { if (!(fn instanceof Function)) throw this._assertError(`This '${fn}' is not a function.`); this.$state.set("ON_CREATED_TABLE", fn); return this; } /** * The "onCreatingTable" method is used exection function when creating the table. * @param {Function} fn functions for executing when creating the table * @returns {this} this * @example * class User extends Model { * boot() { * this.onCreatingTable(async () => { * await new User() * .create({ * ...columns * }) * .save() * }) * } * } */ onCreatedTable(fn) { if (!(fn instanceof Function)) { throw this._assertError(`This '${fn}' is not a function.`); } this.$state.set("ON_CREATED_TABLE", fn); return this; } /** * The "onSyncTable" method is used exection function when sync the table. * @param {Function} fn functions for executing when sync table * @returns {this} this * @example * class User extends Model { * boot() { * this.onSyncTable(async () => { * console.log('onSyncTable!!') * }) * } * } */ onSyncTable(fn) { if (!(fn instanceof Function)) { throw this._assertError(`This '${fn}' is not a function.`); } this.$state.set("ON_SYNC_TABLE", fn); return this; } /** * exceptColumns for method except * @override * @returns {promise<string>} string */ async exceptColumns() { const excepts = this.$state.get("EXCEPTS"); const hasDot = excepts.some((except) => /\./.test(except)); const names = excepts .map((except) => { if (/\./.test(except)) return except.split(".")[0]; return null; }) .filter(Boolean); const tableNames = names.length ? [...new Set(names)] : [this.$state.get("TABLE_NAME")]; const removeExcepts = []; const schemaColumns = this.getSchemaModel(); for (const tableName of tableNames) { const isHasSchema = [ schemaColumns != null, tableName?.replace(/`/g, "") === this.$state.get("TABLE_NAME")?.replace(/`/g, ""), ].every((d) => d); if (isHasSchema) { const columns = Object.keys(schemaColumns); const removeExcept = columns.filter((column) => { return excepts.every((except) => { if (/\./.test(except)) { const [table, _] = except.split("."); return except !== `${table}.${column}`; } return except !== column; }); }); removeExcepts.push(hasDot ? removeExcept.map((r) => `${tableName}.${r}`) : removeExcept); continue; } const columns = await this.getColumns(); const removeExcept = columns .map((v) => v.Field) .filter((column) => { return excepts.every((except) => { if (/\./.test(except)) { const [table, _] = except.split("."); return except !== `${table}.${column}`; } return except !== column; }); }); removeExcepts.push(hasDot ? removeExcept.map((r) => `\`${tableName}\`.${r}`) : removeExcept); } return removeExcepts.flat(); } /** * Build method for relation in model * @param {string} name name relation registry in your model * @param {Function} callback query callback * @returns {this} this */ buildMethodRelation(name, callback) { this.relations(name); const relation = this.$state .get("RELATIONS") .find((v) => v.name === name); if (relation == null) { throw this._assertError(`This Relation '${String(name)}' not be register in Model '${this.constructor?.name}'.`); } const relationHasExists = Object.values(this.$constants("RELATIONSHIP"))?.includes(relation.relation); if (!relationHasExists) { throw this._assertError(`Unknown relationship in '${this.$constants("RELATIONSHIP")}'.`); } if (callback == null) { relation.query = new relation.model(); return this; } relation.query = callback(new relation.model()); return this; } /** * The 'audit' method is used to sets the audit information for the tracking. * * @param {number} userId - The ID of the user performing the audit. * @param {Record<string, any>} [metadata] - Optional metadata to store with the audit. * @returns {this} this */ audit(userId, metadata) { if (metadata) this.$state.set("AUDIT_METADATA", metadata); this.$state.set("AUDIT", userId); return this; } meta(meta) { this.$state.set("META", meta); return this; } /** * The 'typeOfSchema' method is used get type of schema. * @returns {TS} type of schema */ typeOfSchema() { return {}; } /** * The 'typeOfRelation' method is used get type of relation. * @returns {TR} type of Relation */ typeOfRelation() { return {}; } /** * The 'cache' method is used get data from cache. * @param {Object} object * @property {string} key key of cache * @property {number} expires ms * @returns {this} this */ cache({ key, expires }) { this.$state.set("CACHE", { key, expires, }); return this; } /** * * @override * @param {string[]} ...columns * @returns {this} this */ select(...columns) { if (!columns.length) { this.$state.set("SELECT", ["*"]); return this; } let select = columns.map((c) => { const column = String(c); if (column.includes(this.$constants("RAW"))) { return column?.replace(this.$constants("RAW"), "").replace(/'/g, ""); } const blueprint = this._getBlueprintByColumn(column); if (blueprint?.isVirtual) { const sql = blueprint.sql?.select; if (sql == null) return this.bindColumn(column); if (sql.toLowerCase().includes(" as ")) { return sql; } return `${sql} ${this.$constants("AS")} ${column}`; } return this.bindColumn(column); }); select = [...this.$state.get("SELECT"), ...select]; if (this.$state.get("DISTINCT") && select.length) { select[0] = String(select[0]).includes(this.$constants("DISTINCT")) ? select[0] : `${this.$constants("DISTINCT")} ${select[0]}`; } this.$state.set("SELECT", select); return this; } addSelect(...columns) { let select = columns.map((c) => { const column = String(c); if (column.includes(this.$constants("RAW"))) { return column?.replace(this.$constants("RAW"), "").replace(/'/g, ""); } return this.bindColumn(column); }); this.$state.set("ADD_SELECT", [ ...select, ...this.$state.get("ADD_SELECT"), ]); return this; } /** * * @override * @param {...string} columns * @returns {this} this */ except(...columns) { if (!columns.length) return this; const exceptColumns = this.$state.get("EXCEPTS"); this.$state.set("EXCEPTS", [...columns, ...exceptColumns]); return this; } /** * * @override * @returns {this} this */ exceptTimestamp() { let excepts = []; if (this.$state.get("SOFT_DELETE")) { const deletedAt = this._valuePattern(this.$state.get("SOFT_DELETE_FORMAT")); excepts = [...excepts, deletedAt]; } const updatedAt = this._valuePattern(this.$state.get("TIMESTAMP_FORMAT").UPDATED_AT); const createdAt = this._valuePattern(this.$state.get("TIMESTAMP_FORMAT").CREATED_AT); excepts = [...excepts, createdAt, updatedAt]; const exceptColumns = this.$state.get("EXCEPTS"); this.$state.set("EXCEPTS", [...excepts, ...exceptColumns]); return this; } /** * * @override * @param {string} column * @param {string?} order by default order = 'asc' but you can used 'asc' or 'desc' * @returns {this} */ orderBy(column, order = "ASC") { const blueprint = this._getBlueprintByColumn(String(column)); if (blueprint?.isVirtual) { const sql = blueprint.sql?.where; if (sql) { this.$state.set("ORDER_BY", [ ...this.$state.get("ORDER_BY"), `${sql} ${order.toUpperCase()}`, ]); return this; } } const orderBy = [column] .map((c) => { if (/\./.test(c)) return this.bindColumn(c.replace(/'/g, "")); if (c.includes(this.$constants("RAW"))) return c?.replace(this.$constants("RAW"), ""); return this.bindColumn(c); }) .join(", "); this.$state.set("ORDER_BY", [ ...this.$state.get("ORDER_BY"), `${orderBy} ${order.toUpperCase()}`, ]); return this; } /** * * @override * @param {string?} columns [column=id] * @returns {this} */ latest(...columns) { let orderBy = "`id`"; if (columns?.length) { orderBy = columns .map((c) => { if (/\./.test(c)) return this.bindColumn(c.replace(/'/g, "")); if (c.includes(this.$constants("RAW"))) { return c?.replace(this.$constants("RAW"), ""); } const blueprint = this._getBlueprintByColumn(String(c)); if (blueprint?.isVirtual) { const sql = blueprint.sql?.orderBy; if (sql) return sql; } return this.bindColumn(c); }) .join(", "); } this.$state.set("ORDER_BY", [ ...this.$state.get("ORDER_BY"), `${orderBy} ${this.$constants("DESC")}`, ]); return this; } /** * * @override * @param {string?} columns [column=id] * @returns {this} */ oldest(...columns) { let orderBy = "`id`"; if (columns?.length) { orderBy = columns .map((c) => { if (/\./.test(c)) return this.bindColumn(c.replace(/'/g, "")); if (c.includes(this.$constants("RAW"))) return c?.replace(this.$constants("RAW"), ""); const blueprint = this._getBlueprintByColumn(String(c)); if (blueprint?.isVirtual) { const sql = blueprint.sql?.orderBy; if (sql) return sql; } return this.bindColumn(c); }) .join(", "); } this.$state.set("ORDER_BY", [ ...this.$state.get("ORDER_BY"), `${orderBy} ${this.$constants("ASC")}`, ]); return this; } /** * * @override * @param {string?} columns [column=id] * @returns {this} */ groupBy(...columns) { let groupBy = "id"; if (columns?.length) { groupBy = columns .map((c) => { if (/\./.test(c)) return this.bindColumn(c.replace(/'/g, "")); if (c.includes(this.$constants("RAW"))) return c?.replace(this.$constants("RAW"), ""); const blueprint = this._getBlueprintByColumn(String(c)); if (blueprint?.isVirtual) { const sql = blueprint.sql?.groupBy; if (sql) return sql; } return this.bindColumn(c); }) .join(", "); } this.$state.set("GROUP_BY", [...this.$state.get("GROUP_BY"), `${groupBy}`]); return this; } /** * @override * @param {string} column * @returns {string} return table.column */ bindColumn(column, pattern = true) { if (!/\./.test(column)) { if (column === "*") return "*"; const c = pattern ? this._valuePattern(column) : column; const alias = this.$state.get("ALIAS"); return [ alias == null || alias === "" ? `\`${this.getTableName().replace(/`/g, "")}\`` : `\`${alias.replace(/`/g, "")}\``, ".", `\`${c.replace(/`/g, "")}\``, ].join(""); } let [table, c] = column.split("."); c = pattern ? this._valuePattern(c) : c; if (c === "*") { return `\`${table.replace(/`/g, "")}\`.*`; } return `\`${table.replace(/`/g, "")}\`.\`${c.replace(/`/g, "")}\``; } /** * * @override * The 'makeSelectStatement' method is used to make select statement. * @returns {Promise<string>} string */ async makeSelectStatement() { const schemaModel = this.getSchemaModel(); const makeStatement = (columns) => { return [ `${this.$constants("SELECT")}`, `${columns.join(", ")}`, `${this.$constants("FROM")}`, `\`${this.getTableName()}\``, ].join(" "); }; if (schemaModel == null) { const schemaTable = await this.getSchema(); const columns = schemaTable.map((column) => `\`${this.getTableName()}\`.\`${column.Field}\``); return makeStatement(columns); } const columns = Object.keys(schemaModel).map((column) => `\`${this.getTableName()}\`.\`${column}\``); return makeStatement(columns); } /** * * @override * The 'makeInsertStatement' method is used to make insert table statement. * @returns {Promise<string>} string */ async makeInsertStatement() { const schemaModel = this.getSchemaModel(); const makeStatement = (columns) => { return [ `${this.$constants("INSERT")}`, `\`${this.getTableName()}\``, `(${columns.join(", ")})`, `${this.$constants("VALUES")}`, `(${Array(columns.length).fill("`?`").join(" , ")})`, ].join(" "); }; if (schemaModel == null) { const schemaTable = await this.getSchema(); const columns = schemaTable.map((column) => this.bindColumn(column.Field)); return makeStatement(columns); } const columns = Object.keys(schemaModel).map((column) => this.bindColumn(column)); return makeStatement(columns); } /** * * @override * The 'makeUpdateStatement' method is used to make update table statement. * @returns {Promise<string>} string */ async makeUpdateStatement() { const schemaModel = this.getSchemaModel(); const makeStatement = (columns) => { return [ `${this.$constants("UPDATE")}`, `\`${this.getTableName()}\``, `${this.$constants("SET")}`, `(${columns.join(", ")})`, `${this.$constants("WHERE")}`, `${this.bindColumn("id")} = '?'`, ].join(" "); }; if (schemaModel == null) { const schemaTable = await this.getSchema(); const columns = schemaTable.map((column) => `${this.bindColumn(column.Field)} = '?'`); return makeStatement(columns); } const columns = Object.keys(schemaModel).map((column) => `${this.bindColumn(column)} = '?'`); return makeStatement(columns); } /** * * @override * The 'makeDeleteStatement' method is used to make delete statement. * @returns {Promise<string>} string */ async makeDeleteStatement() { const makeStatement = () => { return [ `${this.$constants("DELETE")}`, `${this.$constants("FROM")}`, `\`${this.getTableName()}\``, `${this.$constants("WHERE")}`, `${this.bindColumn("id")} = \`?\``, ].join(" "); }; return makeStatement(); } /** * * @override * The 'makeCreateTableStatement' method is used to make create table statement. * @returns {Promise<string>} string */ async makeCreateTableStatement() { const schemaModel = this.getSchemaModel(); const makeStatement = (columns) => { return [ `${this.$constants("CREATE_TABLE_NOT_EXISTS")}`, `\`${this.getTableName()}\``, `(`, `\n ${columns?.join(",\n ")}`, `\n)`, `${this.$constants("ENGINE")}`, ].join(" "); }; if (schemaModel == null) { const columns = await this.showSchema(); return makeStatement(columns); } let columns = []; for (const key in schemaModel) { const data = schemaModel[key]; const { type, attributes } = Schema_1.Schema.detectSchema(data); if (type == null || attributes == null) continue; columns = [...columns, `${key} ${type} ${attributes?.join(" ")}`]; } return makeStatement(columns); } /** * * Clone instance of model * @param {Model} instance instance of model * @returns {this} this */ clone(instance) { const copy = Object.fromEntries(instance.$state.all()); this.$state.clone(copy); return this; } /** * * Copy an instance of model * @param {Model} instance instance of model * @param {Object} options keep data * @returns {Model} Model */ copyModel(instance, options) { if (!(instance instanceof Model)) { throw this._assertError("This instance is not an instanceof Model."); } const copy = Object.fromEntries(instance.$state.all()); const newInstance = new Model(); newInstance.$state.clone(copy); newInstance.$state.set("SAVE", null); newInstance.$state.set("DEBUG", false); newInstance.$state.set("LOGGER", false); newInstance.$state.set("AUDIT", null); newInstance.$state.set("AUDIT_METADATA", null); newInstance.$state.set("LIFECYCLE_BEFORE_INSERTS", []); newInstance.$state.set("LIFECYCLE_BEFORE_UPDATES", []); newInstance.$state.set("LIFECYCLE_BEFORE_REMOVES", []); newInstance.$state.set("LIFECYCLE_AFTER_INSERTS", []); newInstance.$state.set("LIFECYCLE_AFTER_UPDATES", []); newInstance.$state.set("LIFECYCLE_AFTER_REMOVES", []); if (options?.relations == null || !options.relations) newInstance.$state.set("RELATIONS", []); if (options?.insert == null || !options.insert) newInstance.$state.set("INSERT", null); if (options?.update == null || !options.update) newInstance.$state.set("UPDATE", null); if (options?.delete == null || !options.delete) newInstance.$state.set("DELETE", null); if (options?.select == null || !options.select) newInstance.$state.set("SELECT", []); if (options?.join == null || !options.join) newInstance.$state.set("JOIN", []); if (options?.where == null || !options.where) newInstance.$state.set("WHERE", []); if (options?.groupBy == null || !options.groupBy) newInstance.$state.set("GROUP_BY", []); if (options?.having == null || !options.having) newInstance.$state.set("HAVING", null); if (options?.orderBy == null || !options.orderBy) newInstance.$state.set("ORDER_BY", []); if (options?.limit == null || !options.limit) newInstance.$state.set("LIMIT", null); if (options?.offset == null || !options.offset) newInstance.$state.set("OFFSET", null); return newInstance; } /** * * execute the query using raw sql syntax * @override * @param {string} sql * @returns {this} this */ async _queryStatement(sql, { retry = false } = {}) { try { sql = this._queryBuilder({ onFormat: true }).format([sql]); const getResults = async (sql) => { if (this.$state.get("DEBUG")) { const startTime = +new Date(); const results = await this.$pool.query(sql); const endTime = +new Date(); this.$utils.consoleDebug(sql, retry); this.$state.set("QUERIES", [...this.$state.get("QUERIES"), sql]); this.$utils.consoleExec(startTime, endTime); return results; } return await this.$pool.query(sql); }; let result = null; if (!this.$state.get("AUDIT") && this.$state.get("LOGGER")) { const { Logger } = await Promise.resolve().then(() => __importStar(require("./Contracts/Logger"))); result = await Logger.tracking(this, { sql, fn: () => getResults(sql), }); } if (this.$state.get("AUDIT")) { const { Audit } = await Promise.resolve().then(() => __importStar(require("./Contracts/Audit"))); result = await Audit.tracking(this, { sql, fn: () => getResults(sql) }); } return result == null ? await getResults(sql) : result; } catch (error) { if (this.$state.get("DEBUG")) this.$utils.consoleDebug(sql, retry); if (this.$state.get("JOIN")?.length) throw error; const retryCount = Number(this.$state.get("RETRY")); await this._checkSchemaOrNextError(error, retryCount, error); this.$state.set("RETRY", retryCount + 1); return await this._queryStatement(sql, { retry: true }); } } /** * * execute the query using raw sql syntax actions for insert update and delete * @override * @param {string} sql * @returns {this} this */ async _actionStatement(sql, { retry = false } = {}) { try { sql = this._queryBuilder({ onFormat: true }).format([sql]); const getResults = async (sql) => { if (this.$state.get("DEBUG")) { const startTime = +new Date(); const results = await this.$pool.query(sql); const endTime = +new Date(); this.$utils.consoleDebug(sql, retry); this.$utils.consoleExec(startTime, endTime); this.$state.set("QUERIES", [...this.$state.get("QUERIES"), sql]); return results; } return await this.$pool.query(sql); }; let r