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.

574 lines 23.4 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 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 = (database, table, schema) => { const query = this.$db["_queryBuilder"](); return query.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) { 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 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 || !force) 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; }, {}); if (force) { await this._syncTable({ schemaModel, model, log, tables }); 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.createFK({ 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.createFK({ 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, index: index, }); if (INDEX) continue; await this.$db.debug(log).rawQuery(query.createIndex({ table: table, index: index, key: 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.createIndex({ table: model.getTableName(), index: index, key: key, })) .catch((err) => { console.log(`\x1b[31mERROR: Failed to craete index key '${index}' with name ${key} caused by '${err.message}'\x1b[0m`); }); } } } async _syncChangeColumn({ schemaModel, model, log, }) { const schemaTable = await model.getSchema(); const query = model["_queryBuilder"](); const isTypeMatch = ({ 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"], }; // Enum in postgres too hard for maping // if have value add in Blueprint.enum, The sync column is not supported if (/^character varying(\(\d+\))?$/i.test(typeTable) && /^enum\(.+\)$/i.test(typeSchema)) { return true; } if (typeTable.startsWith("character varying")) { const typeTableFormated = typeTable.replace("character varying", "varchar"); if (typeTableFormated === typeSchema) return true; return false; } 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 true; } return false; }; 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 sameType = isTypeMatch({ typeTable, typeSchema }); return sameType ? null : key; }) .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; 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