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
JavaScript
"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