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.

474 lines 20.2 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Schema = void 0; const Builder_1 = require("./Builder"); const tools_1 = require("../tools"); 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, "")); } }; createTable = (table, schemas) => { 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.join(" ")}`]; } return [ `${this.$db["$constants"]("CREATE_TABLE_NOT_EXISTS")}`, `${table} (${columns.join(", ")})`, `${this.$db["$constants"]("ENGINE")}`, ].join(" "); }; 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) { try { return { type: schema?.type ?? schema?._type ?? null, attributes: schema?.attributes ?? schema?._attributes ?? null, }; } catch (e) { return { type: null, attributes: null, }; } } /** * * 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 = tools_1.Tool.fs.readdirSync(pathFolders, { withFileTypes: true, }); const files = await Promise.all(directories.map((directory) => { const newDir = tools_1.Tool.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, }); } 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`); return null; } } async syncExecute({ models, force, log, foreign, changed, index, }) { const checkTables = await this.$db.rawQuery(this.$db["$constants"]("SHOW_TABLES")); const existsTables = checkTables.map((c) => Object.values(c)[0]); for (const model of models) { if (model == null) continue; const schemaModel = model.getSchemaModel(); if (!schemaModel) continue; const checkTableIsExists = existsTables.some((table) => table === model.getTableName()); if (!checkTableIsExists) { const sql = this.createTable(`\`${model.getTableName()}\``, schemaModel); await model.debug(log).rawQuery(sql); const beforeCreatingTheTable = model["$state"].get("BEFORE_CREATING_TABLE"); if (beforeCreatingTheTable != null) await beforeCreatingTheTable(); } if (foreign) { await this._syncForeignKey({ schemaModel, model, log, }); } if (index) { await this._syncIndex({ schemaModel, model, log, }); } if (!force) continue; const schemaTable = await model.getSchema(); const schemaTableKeys = schemaTable.map((k) => k.Field); const schemaModelKeys = Object.keys(schemaModel); const wasChangedColumns = changed ? 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 changed = !typeSchema.startsWith(typeTable); if (changed) { if (typeSchema === 'boolean' && typeTable === 'tinyint(1)') { return null; } } return changed ? key : null; }) .filter((d) => d != null) : []; if (wasChangedColumns.length) { for (const column of wasChangedColumns) { if (column == null) continue; const { type, attributes } = this.detectSchema(schemaModel[column]); if (type == null || attributes == null) continue; const sql = [ this.$db["$constants"]("ALTER_TABLE"), `\`${model.getTableName()}\``, this.$db["$constants"]("CHANGE"), `\`${column}\``, `\`${column}\` ${type} ${attributes != null && attributes.length ? `${attributes .filter((v) => !['PRIMARY KEY'] .includes(v)).join(" ")}` : ""}`, ].join(" "); await this.$db.debug(log).rawQuery(sql); } } const missingColumns = schemaModelKeys.filter((schemaModelKey) => !schemaTableKeys.includes(schemaModelKey)); if (!missingColumns.length) continue; 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; const sql = [ this.$db["$constants"]("ALTER_TABLE"), `\`${model.getTableName()}\``, this.$db["$constants"]("ADD"), `\`${column}\` ${type} ${attributes != null && attributes.length ? `${attributes.join(" ")}` : ""}`, this.$db["$constants"]("AFTER"), `\`${findAfterIndex}\``, ].join(" "); await this.$db.debug(log).rawQuery(sql); } await this._syncForeignKey({ schemaModel, model, log, }); } 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 }); const constants = this.$db["$constants"]; const sql = [ constants("ALTER_TABLE"), `\`${model.getTableName()}\``, constants("ADD_CONSTRAINT"), constraintName, `${constants("FOREIGN_KEY")}(\`${key}\`)`, `${constants("REFERENCES")} \`${table}\`(\`${foreign.references}\`)`, `${constants("ON_DELETE")} ${foreign.onDelete}`, `${constants("ON_UPDATE")} ${foreign.onUpdate}`, ].join(" "); try { await this.$db.debug(log).rawQuery(sql); } catch (e) { if (typeof onReference === "string") continue; const message = String(e.message); if (message.includes("Duplicate foreign key constraint") || message.includes('Duplicate key on write or update')) { continue; } const schemaModelOn = await onReference.getSchemaModel(); if (!schemaModelOn) continue; const tableSql = this.createTable(`\`${table}\``, schemaModelOn); await this.$db .debug(log) .rawQuery(tableSql) .catch((e) => console.log(e)); await this.$db .debug(log) .rawQuery(sql) .catch((e) => console.log(e)); continue; } } } 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 sql = [ `${this.$db["$constants"]("CREATE_INDEX")}`, `${index}`, `${this.$db["$constants"]("ON")}`, `${table}(\`${key}\`)`, ].join(" "); try { await this.$db.debug(log).rawQuery(sql); } catch (err) { if (String(err.message).includes("Duplicate key name")) continue; const tableSql = this.createTable(`\`${table}\``, schemaModel); await this.$db .debug(log) .rawQuery(tableSql) .catch((e) => console.log(e)); await this.$db .debug(log) .rawQuery(sql) .catch((e) => console.log(e)); continue; } } } } exports.Schema = Schema; exports.default = Schema; //# sourceMappingURL=Schema.js.map