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,419 lines 238 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"); const Repository_1 = __importDefault(require("./Repository")); 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 'query' method is used to return instance * @static * @example * const user = await User.query().where('id',1).findOne(); * console.log(user); */ static query() { return new this(); } /** * * The 'find' method is used to retrieve a single record from a database table by its primary key. * * It allows you to retrieve a single record from a database table that meets the specified criteria. * @type {?object} options * @property {?object} options.select * @property {?object} options.except * @property {?object[]} options.orderBy * @property {?string[]} options.groupBy * @property {?string} options.having * @property {?number} options.limit * @property {?number} options.offset * @property {?object} options.where * @property {?string[]} options.whereRaw * @property {?object} options.whereQuery * @property {?{condition,callback}} options.when * @property {?{localKey , referenceKey}[]} options.join * @property {?{localKey , referenceKey}[]} options.rightJoin * @property {?{localKey , referenceKey}[]} options.leftJoin * @property {?string[]} options.relations * @property {string[]} options.relationExists * @property {?{condition,callback}} options.relationQuery * @property {?boolean} options.debug * @returns {promise<object>[]} * * @example * import { User } from '../Models/User' * * const users = await User.find({ * select : { id: true, name: true }, * where : { * id: 1 * } * }) * */ static async find(primaryKey, options = {}) { return await (0, Repository_1.default)(this).find(primaryKey, options); } /** * * The 'findOne' method is used to retrieve the get record that matches the query conditions. * * It allows you to retrieve a single record from a database table that meets the specified criteria. * @type {?object} options * @property {?object} options.select * @property {?object} options.except * @property {?object[]} options.orderBy * @property {?string[]} options.groupBy * @property {?string} options.having * @property {?number} options.limit * @property {?number} options.offset * @property {?object} options.where * @property {?string[]} options.whereRaw * @property {?object} options.whereQuery * @property {?{condition,callback}} options.when * @property {?{localKey , referenceKey}[]} options.join * @property {?{localKey , referenceKey}[]} options.rightJoin * @property {?{localKey , referenceKey}[]} options.leftJoin * @property {?string[]} options.relations * @property {string[]} options.relationExists * @property {?{condition,callback}} options.relationQuery * @property {?boolean} options.debug * @returns {promise<object>[]} * * @example * import { User } from '../Models/User' * * const users = await User.findOne({ * select : { id: true, name: true }, * where : { * id: 1 * } * }) * */ static async findOne(options = {}) { return await (0, Repository_1.default)(this).findOne(options); } /** * * The 'findMany' method is used to retrieve the get record that matches the query conditions. * * It allows you to retrieve a single record from a database table that meets the specified criteria. * @type {?object} options * @property {?object} options.select * @property {?object} options.except * @property {?object[]} options.orderBy * @property {?string[]} options.groupBy * @property {?string} options.having * @property {?number} options.limit * @property {?number} options.offset * @property {?object} options.where * @property {?string[]} options.whereRaw * @property {?object} options.whereQuery * @property {?{condition,callback}} options.when * @property {?{localKey , referenceKey}[]} options.join * @property {?{localKey , referenceKey}[]} options.rightJoin * @property {?{localKey , referenceKey}[]} options.leftJoin * @property {?string[]} options.relations * @property {string[]} options.relationExists * @property {?{condition,callback}} options.relationQuery * @property {?boolean} options.debug * @returns {promise<object>[]} * * @example * import { User } from '../Models/User' * * const users = await User.findMany({ * select : { id: true, name: true }, * where : { * id: 1 * } * }) * */ static async findMany(options = {}) { return await (0, Repository_1.default)(this).findMany(options); } /** * * The 'paginate' method is used to perform pagination on a set of database query results obtained through the Query Builder. * * It allows you to split a large set of query results into smaller, more manageable pages, * making it easier to display data in a web application and improve user experience. * @type {?object} options * @property {?object} options.select * @property {?object} options.except * @property {?object[]} options.orderBy * @property {?string[]} options.groupBy * @property {?string} options.having * @property {?number} options.limit * @property {?number} options.offset * @property {?object} options.where * @property {?string[]} options.whereRaw * @property {?object} options.whereQuery * @property {?{condition,callback}} options.when * @property {?{localKey , referenceKey}[]} options.join * @property {?{localKey , referenceKey}[]} options.rightJoin * @property {?{localKey , referenceKey}[]} options.leftJoin * @property {?string[]} options.relations * @property {string[]} options.relationExists * @property {?{condition,callback}} options.relationQuery * @property {?boolean} options.debug * @property {?number} options.page * @returns {promise<{ meta , data[]}>} * * @example * import { User } from '../Models/User' * * const users = await User.paginate({ * limit:15, * page: 1, * select : { id: true, name: true }, * where : { * id: 1 * } * }) */ static async paginate(options = {}) { return await (0, Repository_1.default)(this).paginate(options); } /** * The 'exists' method is used to determine if any records exist in the database table that match the query conditions. * * It returns a boolean value indicating whether there are any matching records. * @type {?object} options * @property {?object} options.select * @property {?object} options.except * @property {?object[]} options.orderBy * @property {?string[]} options.groupBy * @property {?string} options.having * @property {?number} options.limit * @property {?number} options.offset * @property {?object} options.where * @property {?string[]} options.whereRaw * @property {?object} options.whereQuery * @property {?{condition,callback}} options.when * @property {?{localKey , referenceKey}[]} options.join * @property {?{localKey , referenceKey}[]} options.rightJoin * @property {?{localKey , referenceKey}[]} options.leftJoin * @property {?boolean} options.debug * @property {?number} options.page * * @example * import { User } from '../Models/User' * * const users = await User.exists({ * where : { * id: 1 * } * }) * */ static async exists(options) { return await (0, Repository_1.default)(this).exists(options); } /** * The 'toQuery' method is used to retrieve the raw SQL query that would be executed by a query builder instance without actually executing it. * * This method is particularly useful for debugging and understanding the SQL queries generated by your application. * @type {?object} options * @property {?object} options.select * @property {?object} options.except * @property {?object[]} options.orderBy * @property {?string[]} options.groupBy * @property {?string} options.having * @property {?number} options.limit * @property {?number} options.offset * @property {?object} options.where * @property {?string[]} options.whereRaw * @property {?object} options.whereQuery * @property {?{condition,callback}} options.when * @property {?{localKey , referenceKey}[]} options.join * @property {?{localKey , referenceKey}[]} options.rightJoin * @property {?{localKey , referenceKey}[]} options.leftJoin * @property {?boolean} options.debug * @property {?number} options.page * * @example * import { User } from '../Models/User' * * const users = await User.exists({ * where : { * id: 1 * } * }) * */ static toQuery(options) { return (0, Repository_1.default)(this).toString(options); } /** * The 'create' method is used to insert a new record into a database table associated. * * It simplifies the process of creating and inserting records. * @type {object} options * @property {object} options.data * @property {?boolean} options.debug * @property {?transaction} options.transaction * @return {promise<T.Result<M>>} */ static async create(options) { return (0, Repository_1.default)(this).create(options); } /** * The 'createMany' method is used to insert a new records into a database table associated. * * It simplifies the process of creating and inserting records with an array. * @type {object} options * @property {object[]} options.data * @property {?boolean} options.debug * @property {?transaction} options.transaction * @return {promise<TS[]>} */ static async createMany(options) { return (0, Repository_1.default)(this).createMany(options); } /** * * The 'createOrUpdate' method allows you to update an existing record in a database table if it exists or create a new record if it does not exist. * * This method is particularly useful when you want to update a record based on certain conditions and, * if the record matching those conditions doesn't exist, create a new one with the provided data. * @type {object} options * @property {object} options.data * @property {object} options.where * @property {?boolean} options.debug * @return {promise<NR extends true ? undefined : T.Result<M>[]>} */ static async createOrUpdate(options) { return (0, Repository_1.default)(this).createOrUpdate(options); } /** * The 'createNotExists' method to insert data into a database table while ignoring any duplicate key constraint violations. * * This method is particularly useful when you want to insert records into a table and ensure that duplicates are not inserted, * but without raising an error or exception if duplicates are encountered. * * @type {object} options * @property {object} options.data * @property {object} options.where * @property {?boolean} options.debug * @property {?transaction} options.transaction * @return {promise<T | null>} */ static async createNotExists(options) { return (0, Repository_1.default)(this).createNotExists(options); } /** * * The 'createOrSelect' method to insert data into a database table while select any duplicate key constraint violations. * * This method is particularly useful when you want to insert records into a table and ensure that duplicates are not inserted, * but if exists should be returns a result. * @type {object} options * @property {object} options.data * @property {object} options.where * @property {?boolean} options.debug * @return {promise<T.Result<M>>} */ static async createOrSelect(options) { return (0, Repository_1.default)(this).createOrSelect(options); } /** * The 'update' method is used to update existing records in a database table that are associated. * * It simplifies the process of updating records by allowing you to specify the values to be updated using a single call. * * It allows you to remove one record that match certain criteria. * @type {object} options * @property {object} options.data * @property {object} options.where * @property {?boolean} options.debug * @property {?transaction} options.transaction * @return {promise< NR extends true ? undefined : T.Result<M> | null>} */ static async update(options) { return (0, Repository_1.default)(this).update(options); } /** * The 'updateMany' method is used to update existing records in a database table that are associated. * * It simplifies the process of updating records by allowing you to specify the values to be updated using a single call. * * It allows you to remove more records that match certain criteria. * @type {object} options * @property {object} options.data * @property {object} options.where * @property {?boolean} options.debug * @property {?transaction} options.transaction * @return {promise<T.Result<M>[]>} */ static async updateMany(options) { return (0, Repository_1.default)(this).updateMany(options); } /** * The 'cache' method is used get the functions from the Cache * @returns {TCacheModel} cache */ static get cache() { const getCacheKey = (key) => { const db = new this().database(); const table = new this().getTableName(); return `${db}:${table}:${key}`; }; return { provider: () => Cache_1.Cache.provider(), driver: (driver) => Cache_1.Cache.driver(driver), all: async () => { return await Cache_1.Cache.all(); }, clear: async () => { return await Cache_1.Cache.clear(); }, get: async (key, options) => { const cacheKey = options?.namespace ? getCacheKey(key) : key; return await Cache_1.Cache.get(cacheKey); }, exists: async (key, options) => { const cacheKey = options?.namespace ? getCacheKey(key) : key; return await Cache_1.Cache.exists(cacheKey); }, set: async (key, value, ms, options) => { const cacheKey = options?.namespace ? getCacheKey(key) : key; return await Cache_1.Cache.set(cacheKey, value, ms); }, delete: async (key, options) => { const cacheKey = options?.namespace ? getCacheKey(key) : key; return await Cache_1.Cache.delete(cacheKey); }, }; } /** * 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"), ...where ]); } 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").snake_case, this.$constants("PATTERN").camelCase, ]; if (!allowPattern.includes(pattern)) { throw this._assertError(`The 'tspace-mysql' support only pattern '${this.$constants("PATTERN").snake_case}', '${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").snake_case); 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 * @property {boolean} namespace whether to use namespace for cache key, default is false, namespace is `${database}:${table}:${key}` * @returns {this} this */ cache({ key, expires, namespace }) { const getCacheKey = (key) => { const db = this.database(); const table = this.getTableName(); return `${db}:${table}:${key}`; }; const cacheKey = namespace ? getCacheKey(key) : key; this.$state.set("CACHE", { key: cacheKey, 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); exc