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.

879 lines 34.1 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Schema = void 0; const zod_1 = require("zod"); const Builder_1 = require("./Builder"); const Package_1 = require("./Package"); ; const AlterTable_1 = require("./Contracts/AlterTable"); class Schema { $db = new Builder_1.Builder(); table = async (table, schemas) => { try { let columns = []; for (const key in schemas) { const data = schemas[key]; const { type, attributes } = this.detectSchema(data); if (type == null || attributes == null) continue; columns = [ ...columns, `\`${key}\` ${type} ${attributes != null && attributes.length ? `${attributes.join(" ")}` : ""}`, ]; } const sql = [ `${this.$db["$constants"]("CREATE_TABLE_NOT_EXISTS")}`, `${table} (${columns?.join(",")})`, `${this.$db["$constants"]("ENGINE")}`, ].join(" "); await this.$db.rawQuery(sql); console.log(`Migrats : '${table}' created successfully`); return; } catch (err) { console.log(err.message?.replace(/ER_TABLE_EXISTS_ERROR:/g, "")); } }; static table = async (table, schemas) => { return new this().table(table, schemas); }; createTable = (database, table, schema) => { const query = this.$db["_queryBuilder"](); return query.createTable({ database, table, schema }); }; static createTable = (database, table, schema) => { return new this().createTable(database, table, schema); }; detectSchema(schema) { try { return { type: schema?.type ?? schema?._type ?? null, attributes: schema?.attributes ?? schema?._attributes ?? null, }; } catch (e) { return { type: null, attributes: null, }; } } static detectSchema(schema) { return new this().detectSchema(schema); } /** * * The 'Sync' method is used to check for create or update table or columns with your schema in your model. * * The schema can define with method 'useSchema' * @param {string} pathFolders directory to models * @property {boolean} options.force - forec always check all columns if not exists will be created * @property {boolean} options.log - show log execution with sql statements * @property {boolean} options.foreign - check when has a foreign keys will be created * @property {boolean} options.changed - check when column is changed attribute will be change attribute * @return {Promise<void>} * @example * * - node_modules * - app * - Models * - User.ts * - Post.ts * * // file User.ts * class User extends Model { * constructor(){ * super() * this.hasMany({ name : 'posts' , model : Post }) * this.useSchema ({ * id : new Blueprint().int().notNull().primary().autoIncrement(), * uuid : new Blueprint().varchar(50).null(), * email : new Blueprint().int().notNull().unique(), * name : new Blueprint().varchar(255).null(), * created_at : new Blueprint().timestamp().null(), * updated_at : new Blueprint().timestamp().null(), * deleted_at : new Blueprint().timestamp().null() * }) * } * } * * // file Post.ts * class Post extends Model { * constructor(){ * super() * this.hasMany({ name : 'comments' , model : Comment }) * this.belongsTo({ name : 'user' , model : User }) * this.useSchema ({ * id : new Blueprint().int().notNull().primary().autoIncrement(), * uuid : new Blueprint().varchar(50).null(), * user_id : new Blueprint().int().notNull().foreign({ references : 'id' , on : User , onDelete : 'CASCADE' , onUpdate : 'CASCADE' }), * title : new Blueprint().varchar(255).null(), * created_at : new Blueprint().timestamp().null(), * updated_at : new Blueprint().timestamp().null(), * deleted_at : new Blueprint().timestamp().null() * }) * } * } * * * await new Schema().sync(`app/Models` , { force : true , log = true, foreign = true , changed = true }) */ async sync(pathFolders, { force = false, log = false, foreign = false, changed = false, index = false, } = {}) { const directories = Package_1.Package.fs.readdirSync(pathFolders, { withFileTypes: true, }); const files = await Promise.all(directories.map((directory) => { const newDir = Package_1.Package.path.resolve(String(pathFolders), directory.name); if (directory.isDirectory() && directory.name.toLocaleLowerCase().includes("migrations")) return null; return directory.isDirectory() ? Schema.sync(newDir, { force, log, changed }) : newDir; })); const pathModels = [].concat(...files).filter((d) => d != null || d === ""); await new Promise((r) => setTimeout(r, 2000)); const models = await Promise.all(pathModels .map((pathModel) => this._import(pathModel)) .filter((d) => d != null)); if (!models.length) return; await this.syncExecute({ models, force, log, foreign, changed, index }); return; } /** * * The 'Sync' method is used to check for create or update table or columns with your schema in your model. * * The schema can define with method 'useSchema' * @param {string} pathFolders directory to models * @type {object} options * @property {boolean} options.force - forec always check all columns if not exists will be created * @property {boolean} options.log - show log execution with sql statements * @property {boolean} options.foreign - check when has a foreign keys will be created * @property {boolean} options.changed - check when column is changed attribute will be change attribute * @property {boolean} options.index - add columns to index * @return {Promise<void>} * @example * * - node_modules * - app * - Models * - User.ts * - Post.ts * * // file User.ts * class User extends Model { * constructor(){ * super() * this.hasMany({ name : 'posts' , model : Post }) * this.useSchema ({ * id : new Blueprint().int().notNull().primary().autoIncrement(), * uuid : new Blueprint().varchar(50).null(), * email : new Blueprint().int().notNull().unique(), * name : new Blueprint().varchar(255).null(), * created_at : new Blueprint().timestamp().null(), * updated_at : new Blueprint().timestamp().null(), * deleted_at : new Blueprint().timestamp().null() * }) * } * } * * // file Post.ts * class Post extends Model { * constructor(){ * super() * this.hasMany({ name : 'comments' , model : Comment }) * this.belongsTo({ name : 'user' , model : User }) * this.useSchema ({ * id : new Blueprint().int().notNull().primary().autoIncrement(), * uuid : new Blueprint().varchar(50).null(), * user_id : new Blueprint().int().notNull().foreign({ references : 'id' , on : User , onDelete : 'CASCADE' , onUpdate : 'CASCADE' }), * title : new Blueprint().varchar(255).null(), * created_at : new Blueprint().timestamp().null(), * updated_at : new Blueprint().timestamp().null(), * deleted_at : new Blueprint().timestamp().null() * }) * } * } * * * await Schema.sync(`app/Models` , { force : true }) */ static async sync(pathFolders, { force = false, log = false, foreign = false, changed = false, index = false, } = {}) { return new this().sync(pathFolders, { force, log, foreign, changed, index, }); } /** * The 'validator' method is used Create runtime validator schema from Model definition. * * Generates request validation schema compatible with supported adapters. * * * @param {Model} model Model class used to generate validator schema. * @type {object} options * @param {string[]} options.omit * @param {string[]} options.optional * * * @example * * import { Schema } from 'tspace-mysql' * import { User } from './User' * import { Elysia } from 'elysia' * * new Elysia() * .post('/', ({ body }) => { * return { body } * }, { * body: Schema * .validator(User) * .create({ * omit: ["id", "created_at", "updated_at", "deleted_at"], * optional: ["uuid"] * }) * }) * .put('/', ({ body }) => { * return { body } * }, { * body: Schema * .validator(User) * .update({ * required: ["id","email"], * omit: ["uuid"] * }) * }) * .listen(8000) */ validator(model) { return { /** * The 'create' method is used Create runtime validator schema from Model definition. * * Generates request validation schema compatible with supported adapters. * * @type {object} options * @param {string[]} options.omit * @param {string[]} options.optional * * * @example * * import { Schema } from 'tspace-mysql' * import { User } from './User' * import { Elysia } from 'elysia' * * new Elysia() * .post('/', ({ body }) => { * return { body } * }, { * body: Schema * .validator(User) * .create({ * omit: ["id", "created_at", "updated_at", "deleted_at"], * optional: ["uuid"] * }) * .listen(8000) */ create: (options) => { return this._createValidator(model, options); }, /** * The 'update' method is used Create runtime validator schema from Model definition. * * Generates request validation schema compatible with supported adapters. * * @type {object} options * @param {string[]} options.required * @param {string[]} options.omit * @example * * import { Schema } from 'tspace-mysql' * import { User } from './User' * import { Elysia } from 'elysia' * * new Elysia() * .post('/', ({ body }) => { * return { body } * }, { * body: Schema * .validator(User) * .update({ * required: ["id","email"], * omit: ["uuid"] * }) * }) * .listen(8000) */ update: (options) => { return this._updateValidator(model, options); } }; } /** * The 'validator' method is used Create runtime validator schema from Model definition. * * Generates request validation schema compatible with supported adapters. * * * @param {Model} model Model class used to generate validator schema. * @type {object} options * @param {string[]} options.omit * @param {string[]} options.optional * * * @example * * import { Schema } from 'tspace-mysql' * import { User } from './User' * import { Elysia } from 'elysia' * * new Elysia() * .post('/', ({ body }) => { * return { body } * }, { * body: Schema.createValidator(User, { * omit: ["id", "created_at", "updated_at", "deleted_at"], * optional: ["uuid"] * }) * }) * .listen(8000) */ static validator(model) { return new this().validator(model); } alterTable(model) { return new AlterTable_1.AlterTable(model); } static alterTable(model) { return new this().alterTable(model); } _createValidator(model, options) { const schema = new model().getSchemaModel(); if (schema == null) { throw new Error(`The '${new model().constructor.name}' is missing schema definition.`); } const shape = {}; const definition = this._definitionSchema(schema); for (const key in definition) { if (options?.omit?.includes(key)) continue; const value = definition[key]; let schemaValidator; if (Array.isArray(value)) { schemaValidator = zod_1.z.enum(value); } else if (typeof value === "string" && value.includes("|null")) { const base = value.replace("|null", ""); //@ts-ignore schemaValidator = zod_1.z.nullable(zod_1.z[base]()); } else { //@ts-ignore schemaValidator = zod_1.z[value](); } if (options?.optional?.includes(key)) { schemaValidator = zod_1.z.optional(schemaValidator); } // @ts-ignore shape[key] = schemaValidator; } return zod_1.z.object(shape); } _updateValidator(model, options) { const schema = new model().getSchemaModel(); if (schema == null) { throw new Error(`The '${new model().constructor.name}' is missing schema definition.`); } const shape = {}; const definition = this._definitionSchema(schema); for (const key in definition) { const value = definition[key]; let schemaValidator; if (!options?.required?.includes(key)) { const v = Array.isArray(value) ? "enum" : value?.replace("|null", ""); //@ts-ignore schemaValidator = Array.isArray(value) ? zod_1.z.enum(value) : zod_1.z[v](); schemaValidator = zod_1.z.nullable(schemaValidator); schemaValidator = zod_1.z.optional(schemaValidator); // @ts-ignore shape[key] = schemaValidator; continue; } if (Array.isArray(value)) { schemaValidator = zod_1.z.enum(value); } else if (typeof value === "string" && value.includes("|null")) { const base = value.replace("|null", ""); //@ts-ignore schemaValidator = zod_1.z.nullable(zod_1.z[base]()); } else { //@ts-ignore schemaValidator = zod_1.z[value](); } // @ts-ignore shape[key] = schemaValidator; } return zod_1.z.object(shape); } _definitionSchema(schema) { const typeMap = new Map([ [Number, "number"], [String, "string"], [Boolean, "boolean"], [Date, "date"], [Array, "array"], [Object, "object"], ]); let definition = {}; for (const key in schema) { const type = schema[key].valueType; const isNull = schema[key].isNull; const isEnum = schema[key].isEnum; definition[key] = typeMap.get(type) ?? "unknown"; if (isNull) { definition[key] = definition[key] + '|null'; } if (isEnum) { definition[key] = schema[key].enums; } } return definition; } async _import(pathModel) { try { const loadModel = await Promise.resolve(`${pathModel}`).then(s => __importStar(require(s))).catch((_) => { }); const model = new loadModel.default(); return model; } catch (err) { console.log(`Check your 'Model' from path : '${pathModel}' is not instance of Model, Please export default class with extends Model.`); return null; } } async syncExecute({ models, force, log, foreign, changed, index, }) { const query = this.$db["_queryBuilder"](); const rawTables = await this.$db .debug(log) .rawQuery(query.getTables(this.$db.database())); const tables = rawTables.map((c) => Object.values(c)[0]); for (const model of models) { if (model == null) continue; let rawSchemaModel = model.getSchemaModel(); if (!rawSchemaModel) continue; const schemaModel = Object .entries(rawSchemaModel) .reduce((prev, [column, blueprint]) => { if (!blueprint.isVirtual) { prev[model['_valuePattern'](column)] = blueprint; } return prev; }, {}); await this._syncTable({ schemaModel, model, log, tables }); if (force) { await this._syncMissingColumn({ schemaModel, model, log, }); } if (changed) { await this._syncChangeColumn({ schemaModel, model, log, }); } if (index) { await this._syncIndex({ schemaModel, model, log, }); } if (foreign) { await this._syncForeignKey({ schemaModel, model, log, }); } // ---- After sync will calling some function in registry ---- const onSyncTable = model["$state"].get("ON_SYNC_TABLE"); if (onSyncTable) { await onSyncTable(); } } return; } async _syncForeignKey({ schemaModel, model, log, }) { for (const key in schemaModel) { if (schemaModel[key]?.foreignKey == null) continue; const foreign = schemaModel[key].foreignKey; if (foreign.on == null) continue; const onReference = typeof foreign.on === "string" ? foreign.on : new foreign.on(); const table = typeof onReference === "string" ? onReference : onReference.getTableName(); const generateConstraintName = ({ modelTable, key, foreignTable, foreignKey, }) => { const MAX_LENGTH = 64; const baseName = [ "fk", `${modelTable}(${key})`, `${foreignTable}(${foreignKey})`, ].join("_"); if (baseName.length <= MAX_LENGTH) { return `\`${baseName}\``; } const hash = Buffer.from(baseName).toString("base64").slice(0, 8); const shortParts = [ "fk", `${modelTable.slice(0, 16)}(${key.slice(0, 16)})`, `${foreignTable.slice(0, 16)}(${foreignKey.slice(0, 16)})`, hash, ]; const shortName = shortParts.join("_").slice(0, MAX_LENGTH); return `\`${shortName}\``; }; const constraintName = generateConstraintName({ modelTable: model.getTableName(), key, foreignTable: table, foreignKey: foreign.references, }).replace(/`/g, ""); const query = model["_queryBuilder"](); try { const FK = await this.$db.debug(log) .hasFK({ table: model.getTableName(), constraint: constraintName, }); if (FK) continue; await this.$db.debug(log).rawQuery(query.addFK({ table: model.getTableName(), tableRef: table, key, constraint: constraintName, foreign: { references: foreign.references, onDelete: foreign.onDelete, onUpdate: foreign.onUpdate, }, })); } catch (e) { const schemaModelOn = await onReference.getSchemaModel(); if (!schemaModelOn) continue; await this.$db .debug(log) .rawQuery(this.createTable(this.$db.database(), `\`${table}\``, schemaModelOn)) .catch((err) => { console.log(`\x1b[31mERROR: Failed to create table '${table}' caused by '${err.message}'\x1b[0m`); }); await this.$db .debug(log) .rawQuery(query.addFK({ table: model.getTableName(), tableRef: table, key, constraint: constraintName, foreign: { references: foreign.references, onDelete: foreign.onDelete, onUpdate: foreign.onUpdate, }, })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to create foreign key on table '${table}' with constraint ${constraintName} caused by '${err.message}'\x1b[0m`); }); } } } async _syncIndex({ schemaModel, model, log, }) { for (const key in schemaModel) { const name = schemaModel[key]?.indexKey; if (name == null) continue; const table = model.getTableName(); const index = name == "" ? `idx_${table}(${key})` : name; const query = model["_queryBuilder"](); try { const INDEX = await this.$db.debug(log) .hasIndex({ table: table, name: index, }); if (INDEX) continue; await this.$db.debug(log).rawQuery(query.addIndex({ table: table, name: index, columns: [key], })); } catch (err) { await this.$db .debug(log) .rawQuery(query.createTable({ database: this.$db.database(), table: table, schema: schemaModel })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to create the table '${table}' caused by '${err.message}'\x1b[0m`); }); await this.$db .debug(log) .rawQuery(query.addIndex({ table: model.getTableName(), name: index, columns: [key], })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to craete index key '${index}' with name ${key} caused by '${err.message}'\x1b[0m`); }); } } for (const key in schemaModel) { const compositeIndex = schemaModel[key]?.compositeIndexKey; if (compositeIndex == null) continue; const table = model.getTableName(); const comebindKey = [key, ...compositeIndex.columns]; const index = compositeIndex.name == "" ? `idx_${table}(${comebindKey})` : compositeIndex.name; const query = model["_queryBuilder"](); try { const INDEX = await this.$db.debug(log) .hasIndex({ table: table, name: index, }); if (INDEX) continue; await this.$db.debug(log).rawQuery(query.addIndex({ table: table, name: index, columns: comebindKey, })); } catch (err) { await this.$db .debug(log) .rawQuery(query.createTable({ database: this.$db.database(), table: table, schema: schemaModel })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to create the table '${table}' caused by '${err.message}'\x1b[0m`); }); const compositeIndex = schemaModel[key]?.compositeIndexKey; if (compositeIndex == null) continue; const comebindKey = [key, ...compositeIndex.columns]; await this.$db .debug(log) .rawQuery(query.addIndex({ table: model.getTableName(), name: index, columns: comebindKey, })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to craete index key '${index}' with name ${comebindKey} caused by '${err.message}'\x1b[0m`); }); } } } async _syncChangeColumn({ schemaModel, model, log, }) { const schemaTable = await model.getSchema(); const query = model["_queryBuilder"](); const isChangeType = ({ typeTable, typeSchema, }) => { const mappings = { integer: ["int", "integer"], boolean: ["boolean", "tinyint(1)"], smallint: ["smallint", "tinyint(1)"], "tinyint(1)": ["boolean"], json: ["json"], text: ["text", /^longtext$/i], "timestamp without time zone": ["timestamp", "datetime"], }; if (typeTable.startsWith("character varying")) { const typeTableFormated = typeTable.replace("character varying", "varchar"); if (typeTableFormated === typeSchema) return false; return true; } if (typeTable in mappings) { return !mappings[typeTable].some((pattern) => { if (typeof pattern === "string") { return typeSchema.toLowerCase() === pattern.toLowerCase(); } if (pattern instanceof RegExp) { return pattern.test(typeSchema.toLowerCase()); } return false; }); } if (typeTable === typeSchema) { return false; } return true; }; const isChangeNullable = ({ nullable, isNull }) => { if (nullable === 'YES' && isNull) { return false; } if (nullable === 'NO' && !isNull) { return false; } return true; }; const isChangeDefault = ({ defaultTable, defaultSchema }) => { if (defaultTable == defaultSchema) { return false; } return true; }; const wasChangedColumns = Object.entries(schemaModel) .map(([key, value]) => { const find = schemaTable.find((t) => t.Field === key); if (find == null) return null; const typeTable = String(find.Type).toLowerCase(); const typeSchema = String(value.type).toLowerCase(); const isChangedType = isChangeType({ typeTable, typeSchema }); const isChangedNullable = isChangeNullable({ nullable: find.Nullable, isNull: value.isNull }); const isChangedDefault = isChangeDefault({ defaultTable: find.Default, defaultSchema: value.defaultValue }); return [ isChangedType, isChangedNullable, isChangedDefault ].some(v => v) ? key : null; }) .filter((d) => d != null); if (!wasChangedColumns.length) return; for (const column of wasChangedColumns) { if (column == null) continue; const { type, attributes } = this.detectSchema(schemaModel[column]); if (type == null || attributes == null) continue; await this.$db .debug(log) .rawQuery(query.changeColumn({ table: model.getTableName(), type, column, attributes, })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to change the column '${column}' caused by '${err.message}'\x1b[0m`); }); } } async _syncMissingColumn({ schemaModel, model, log, }) { const schemaTable = await model.getSchema(); const query = model["_queryBuilder"](); const schemaTableKeys = schemaTable.map((k) => k.Field); const schemaModelKeys = Object.keys(schemaModel); const missingColumns = schemaModelKeys.filter((schemaModelKey) => !schemaTableKeys.includes(schemaModelKey)); if (!missingColumns.length) return; const entries = Object.entries(schemaModel); for (const column of missingColumns) { const indexWithColumn = entries.findIndex(([key]) => key === column); const findAfterIndex = indexWithColumn ? entries[indexWithColumn - 1][0] : null; const { type, attributes } = this.detectSchema(schemaModel[column]); if (type == null || findAfterIndex == null || attributes == null) { continue; } await this.$db .debug(log) .rawQuery(query.addColumn({ table: model.getTableName(), type, column, attributes, after: findAfterIndex, })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to add the column '${column}' caused by '${err.message}'\x1b[0m`); }); } } async _syncTable({ schemaModel, model, log, tables }) { const onCreatedTable = model["$state"].get("ON_CREATED_TABLE"); const talbeIsExists = tables.some((table) => table === model.getTableName()); if (!talbeIsExists) { const sql = this.createTable(this.$db.database(), `\`${model.getTableName()}\``, schemaModel); await model.debug(log).rawQuery(sql); if (onCreatedTable) { await onCreatedTable(); } } } } exports.Schema = Schema; exports.default = Schema; //# sourceMappingURL=Schema.js.map