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