UNPKG

@cheetah.js/orm

Version:
440 lines (439 loc) 18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqlBuilder = void 0; const entities_1 = require("./domain/entities"); const orm_1 = require("./orm"); const value_processor_1 = require("./utils/value-processor"); const sql_condition_builder_1 = require("./query/sql-condition-builder"); const model_transformer_1 = require("./query/model-transformer"); const sql_column_manager_1 = require("./query/sql-column-manager"); const sql_join_manager_1 = require("./query/sql-join-manager"); class SqlBuilder { constructor(model) { this.statements = {}; this.aliases = new Set(); this.updatedColumns = []; this.originalColumns = []; const orm = orm_1.Orm.getInstance(); this.driver = orm.driverInstance; this.logger = orm.logger; this.entityStorage = entities_1.EntityStorage.getInstance(); this.getEntity(model); this.statements.hooks = this.entity.hooks; this.modelTransformer = new model_transformer_1.ModelTransformer(this.entityStorage); this.columnManager = new sql_column_manager_1.SqlColumnManager(this.entityStorage, this.statements, this.entity); const applyJoinWrapper = (relationship, value, alias) => { return this.joinManager.applyJoin(relationship, value, alias); }; this.conditionBuilder = new sql_condition_builder_1.SqlConditionBuilder(this.entityStorage, applyJoinWrapper, this.statements); this.joinManager = new sql_join_manager_1.SqlJoinManager(this.entityStorage, this.statements, this.entity, this.model, this.driver, this.logger, this.conditionBuilder, this.columnManager, this.modelTransformer, () => this.originalColumns, this.getAlias.bind(this)); } select(columns) { const tableName = this.entity.tableName || this.model.name.toLowerCase(); const schema = this.entity.schema || 'public'; this.statements.statement = 'select'; this.statements.columns = columns; this.originalColumns = columns || []; this.statements.alias = this.getAlias(tableName); this.statements.table = `"${schema}"."${tableName}"`; return this; } setStrategy(strategy = 'joined') { this.statements.strategy = strategy; return this; } setInstance(instance) { this.statements.instance = instance; return this; } insert(values) { const { tableName, schema } = this.getTableName(); const processedValues = value_processor_1.ValueProcessor.processForInsert(values, this.entity); this.statements.statement = 'insert'; this.statements.instance = value_processor_1.ValueProcessor.createInstance(processedValues, this.model, 'insert'); this.statements.alias = this.getAlias(tableName); this.statements.table = `"${schema}"."${tableName}"`; this.statements.values = this.withUpdatedValues(this.withDefaultValues(processedValues, this.entity), this.entity); this.reflectToValues(); return this; } update(values) { const { tableName, schema } = this.getTableName(); const processedValues = value_processor_1.ValueProcessor.processForUpdate(values, this.entity); this.statements.statement = 'update'; this.statements.alias = this.getAlias(tableName); this.statements.table = `${schema}.${tableName}`; this.statements.values = this.withUpdatedValues(processedValues, this.entity); this.statements.instance = value_processor_1.ValueProcessor.createInstance(processedValues, this.model, 'update'); return this; } delete() { const { tableName, schema } = this.getTableName(); this.statements.statement = 'delete'; this.statements.alias = this.getAlias(tableName); this.statements.table = `${schema}.${tableName}`; return this; } where(where) { if (!where || Object.keys(where).length === 0) { return this; } const newWhere = {}; for (const key in where) { if (where[key] instanceof Object) { newWhere[key] = where[key]; continue; } newWhere[value_processor_1.ValueProcessor.getColumnName(key, this.entity)] = where[key]; } where = newWhere; this.statements.where = this.conditionBuilder.build(where, this.statements.alias, this.model); return this; } orderBy(orderBy) { if (!orderBy) { return this; } this.statements.orderBy = this.objectToStringMap(orderBy); return this; } limit(limit) { this.statements.limit = limit; return this; } offset(offset) { this.statements.offset = offset; return this; } load(load) { load?.forEach(relationshipPath => { this.joinManager.addJoinForRelationshipPath(relationshipPath); }); if (this.statements.join) { this.statements.join = this.statements.join?.reverse(); } if (this.statements.selectJoin) { this.statements.selectJoin = this.statements.selectJoin?.reverse(); } return this; } count() { const { tableName, schema } = this.getTableName(); this.statements.statement = 'count'; this.statements.alias = this.getAlias(tableName); this.statements.table = `"${schema}"."${tableName}"`; return this; } getPrimaryKeyColumnName(entity) { // Lógica para obter o nome da coluna de chave primária da entidade // Aqui você pode substituir por sua própria lógica, dependendo da estrutura do seu projeto // Por exemplo, se a chave primária for sempre 'id', você pode retornar 'id'. // Se a lógica for mais complexa, você pode adicionar um método na classe Options para obter a chave primária. return 'id'; } async execute() { if (!this.statements.columns) { this.statements.columns = this.columnManager.generateColumns(this.model, this.updatedColumns); } else { this.statements.columns = [...this.columnManager.processUserColumns(this.statements.columns), ...this.updatedColumns]; } this.statements.join = this.statements.join?.reverse(); this.beforeHooks(); const result = await this.driver.executeStatement(this.statements); this.logExecution(result); return result; } beforeHooks() { if (this.statements.statement === 'update') { this.callHook('beforeUpdate', this.statements.instance); return; } if (this.statements.statement === 'insert') { this.callHook('beforeCreate'); return; } } afterHooks(model) { if (this.statements.statement === 'update') { this.callHook('afterUpdate', this.statements.instance); return; } if (this.statements.statement === 'insert') { this.callHook('afterCreate', model); return; } } async executeAndReturnFirst() { const hasOneToManyJoinedJoin = this.hasOneToManyJoinedJoin(); if (!hasOneToManyJoinedJoin) { this.statements.limit = 1; } const result = await this.execute(); if (result.query.rows.length === 0) { return undefined; } if (hasOneToManyJoinedJoin) { return this.processOneToManyJoinedResult(result.query.rows); } const entities = result.query.rows[0]; const model = await this.modelTransformer.transform(this.model, this.statements, entities); this.afterHooks(model); await this.joinManager.handleSelectJoin(entities, model); return model; } async executeAndReturnFirstOrFail() { const hasOneToManyJoinedJoin = this.hasOneToManyJoinedJoin(); if (!hasOneToManyJoinedJoin) { this.statements.limit = 1; } const result = await this.execute(); if (result.query.rows.length === 0) { throw new Error('Result not found'); } if (hasOneToManyJoinedJoin) { const model = await this.processOneToManyJoinedResult(result.query.rows); if (!model) { throw new Error('Result not found'); } return model; } const entities = result.query.rows[0]; const model = await this.modelTransformer.transform(this.model, this.statements, entities); this.afterHooks(model); await this.joinManager.handleSelectJoin(entities, model); return model; } async executeAndReturnAll() { const result = await this.execute(); if (result.query.rows.length === 0) { return []; } const rows = result.query.rows; const results = []; for (const row of rows) { const models = this.modelTransformer.transform(this.model, this.statements, row); this.afterHooks(models); await this.joinManager.handleSelectJoin(row, models); results.push(models); } return results; } hasOneToManyJoinedJoin() { if (!this.statements.join || this.statements.join.length === 0) { return false; } if (this.statements.strategy !== 'joined') { return false; } return this.statements.join.some(join => { const relationship = this.entity.relations.find(rel => rel.propertyKey === join.joinProperty); return relationship?.relation === 'one-to-many'; }); } async processOneToManyJoinedResult(rows) { const primaryKey = this.getPrimaryKeyName(); const alias = this.statements.alias; const primaryKeyColumn = `${alias}_${primaryKey}`; const firstRowPrimaryKeyValue = rows[0][primaryKeyColumn]; const relatedRows = rows.filter(row => row[primaryKeyColumn] === firstRowPrimaryKeyValue); const model = this.modelTransformer.transform(this.model, this.statements, relatedRows[0]); this.afterHooks(model); this.attachOneToManyRelations(model, relatedRows); return model; } attachOneToManyRelations(model, rows) { if (!this.statements.join) { return; } for (const join of this.statements.join) { const relationship = this.entity.relations.find(rel => rel.propertyKey === join.joinProperty); if (relationship?.relation === 'one-to-many') { const joinedModels = rows.map(row => this.modelTransformer.transform(join.joinEntity, { alias: join.joinAlias }, row)); const uniqueModels = this.removeDuplicatesByPrimaryKey(joinedModels, join.joinEntity); model[join.joinProperty] = uniqueModels; } } } removeDuplicatesByPrimaryKey(models, entityClass) { const entity = this.entityStorage.get(entityClass); if (!entity) { return models; } const primaryKey = this.getPrimaryKeyNameForEntity(entity); const seen = new Set(); const unique = []; for (const model of models) { const id = model[primaryKey]; if (id && !seen.has(id)) { seen.add(id); unique.push(model); } } return unique; } getPrimaryKeyName() { return this.getPrimaryKeyNameForEntity(this.entity); } getPrimaryKeyNameForEntity(entity) { for (const prop in entity.properties) { if (entity.properties[prop].options.isPrimary) { return prop; } } return 'id'; } async executeCount() { const result = await this.execute(); if (result.query.rows.length === 0) { return 0; } return parseInt(result.query.rows[0].count); } logExecution(result) { this.logger.debug(`SQL: ${result.sql} [${Date.now() - result.startTime}ms]`); } async inTransaction(callback) { return await this.driver.transaction(async (tx) => { // @ts-ignore return await callback(this); }); } objectToStringMap(obj, parentKey = '') { return Object.keys(obj) .filter(key => obj.hasOwnProperty(key)) .flatMap(key => this.mapObjectKey(obj, key, parentKey)); } mapObjectKey(obj, key, parentKey) { const fullKey = parentKey ? `${parentKey}.${key}` : key; if (this.isNestedObject(obj[key])) { return this.objectToStringMap(obj[key], fullKey); } if (parentKey) { return [`${this.columnManager.discoverAlias(fullKey, true)} ${obj[key]}`]; } const columnName = value_processor_1.ValueProcessor.getColumnName(key, this.entity); return [`${this.columnManager.discoverAlias(columnName, true)} ${obj[key]}`]; } isNestedObject(value) { return typeof value === 'object' && value !== null; } getTableName() { const tableName = this.entity.tableName || this.model.name.toLowerCase(); const schema = this.entity.schema || 'public'; return { tableName, schema }; } t(value) { return (typeof value === 'string') ? `'${value}'` : value; } // private conditionLogicalOperatorToSql<T extends typeof BaseEntity>(conditions: Condition<T>[], operator: 'AND' | 'OR'): string { // const sqlParts = conditions.map(cond => this.conditionToSql(cond)); // return this.addLogicalOperatorToSql(sqlParts, operator); // } getEntity(model) { const entity = this.entityStorage.get(model); this.model = model; if (!entity) { throw new Error('Entity not found'); } this.entity = entity; } /** * Retrieves an alias for a given table name. * * @param {string} tableName - The name of the table. * @private * @returns {string} - The alias for the table name. */ getAlias(tableName) { const baseAlias = tableName.split('').shift() || ''; const uniqueAlias = this.generateUniqueAlias(baseAlias); this.aliases.add(uniqueAlias); return uniqueAlias; } generateUniqueAlias(baseAlias) { let counter = 1; let candidate = `${baseAlias}${counter}`; while (this.aliases.has(candidate)) { counter++; candidate = `${baseAlias}${counter}`; } return candidate; } withDefaultValues(values, entityOptions) { this.applyDefaultProperties(values, entityOptions); this.applyOnInsertProperties(values, entityOptions); return values; } applyDefaultProperties(values, entityOptions) { const defaultProperties = Object.entries(entityOptions.properties).filter(([_, value]) => value.options.default); for (const [key, property] of defaultProperties) { this.setDefaultValue(values, key, property); } } setDefaultValue(values, key, property) { const columnName = property.options.columnName; if (typeof values[columnName] !== 'undefined') return; values[columnName] = typeof property.options.default === 'function' ? property.options.default() : property.options.default; } applyOnInsertProperties(values, entityOptions) { const properties = Object.entries(entityOptions.properties).filter(([_, value]) => value.options.onInsert); properties.forEach(([key, property]) => this.applyOnInsert(values, key, property)); } applyOnInsert(values, key, property) { const columnName = property.options.columnName; values[columnName] = property.options.onInsert(); this.updatedColumns.push(`${this.statements.alias}."${columnName}" as "${this.statements.alias}_${columnName}"`); } withUpdatedValues(values, entityOptions) { const properties = Object.entries(entityOptions.properties).filter(([_, value]) => value.options.onUpdate); properties.forEach(([key, property]) => this.applyOnUpdate(values, property)); return values; } applyOnUpdate(values, property) { const columnName = property.options.columnName; values[columnName] = property.options.onUpdate(); this.updatedColumns.push(`${this.statements.alias}."${columnName}" as "${this.statements.alias}_${columnName}"`); } callHook(type, model) { const hooks = this.statements.hooks?.filter(hook => hook.type === type) || []; const instance = model || this.statements.instance; hooks.forEach(hook => this.executeHook(hook, instance, !model)); } executeHook(hook, instance, shouldReflect) { instance[hook.propertyName](); if (shouldReflect) this.reflectToValues(); } reflectToValues() { for (const key in this.statements.instance) { if (this.shouldSkipKey(key)) continue; this.reflectKey(key); } } shouldSkipKey(key) { return key.startsWith('$') || key.startsWith('_'); } reflectKey(key) { if (this.entity.properties[key]) { this.reflectProperty(key); return; } this.reflectRelation(key); } reflectProperty(key) { const columnName = this.entity.properties[key].options.columnName; this.statements.values[columnName] = this.statements.instance[key]; } reflectRelation(key) { const rel = this.entity.relations.find(rel => rel.propertyKey === key); if (rel) { this.statements.values[rel.columnName] = this.statements.instance[key]; } } } exports.SqlBuilder = SqlBuilder;