@cheetah.js/orm
Version:
A simple ORM for Cheetah.js
440 lines (439 loc) • 18 kB
JavaScript
"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;