@cheetah.js/orm
Version:
A simple ORM for Cheetah.js
243 lines (242 loc) • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SqlJoinManager = void 0;
class SqlJoinManager {
constructor(entityStorage, statements, entity, model, driver, logger, conditionBuilder, columnManager, modelTransformer, getOriginalColumnsCallback, getAliasCallback) {
this.entityStorage = entityStorage;
this.statements = statements;
this.entity = entity;
this.model = model;
this.driver = driver;
this.logger = logger;
this.conditionBuilder = conditionBuilder;
this.columnManager = columnManager;
this.modelTransformer = modelTransformer;
this.getOriginalColumnsCallback = getOriginalColumnsCallback;
this.getAliasCallback = getAliasCallback;
}
addJoinForRelationshipPath(relationshipPath) {
const relationshipNames = relationshipPath.split('.');
let currentEntity = this.entity;
let currentAlias = this.statements.alias;
for (const relationshipName of relationshipNames) {
const relationship = currentEntity.relations.find((rel) => rel.propertyKey === relationshipName);
if (!relationship) {
throw new Error(`Relationship "${relationshipName}" not found in entity "${currentEntity.tableName}"`);
}
const statement = this.statements.strategy === 'joined'
? this.statements.join
: this.statements.selectJoin;
const nameAliasProperty = this.statements.strategy === 'joined' ? 'joinAlias' : 'alias';
const existingJoin = statement?.find((j) => j.joinProperty === relationshipName && j.originAlias === currentAlias);
if (existingJoin) {
currentAlias = existingJoin[nameAliasProperty];
currentEntity = this.entityStorage.get(relationship.entity());
continue;
}
this.applyJoin(relationship, {}, currentAlias);
const newStatement = this.statements.strategy === 'joined'
? this.statements.join
: this.statements.selectJoin;
currentAlias = newStatement[newStatement.length - 1][nameAliasProperty];
currentEntity = this.entityStorage.get(relationship.entity());
}
}
applyJoin(relationShip, value, alias) {
const { tableName, schema } = this.getTableName();
const { tableName: joinTableName, schema: joinSchema, hooks: joinHooks, } = this.entityStorage.get(relationShip.entity()) || {
tableName: relationShip.entity().name.toLowerCase(),
schema: 'public',
};
const originPrimaryKey = this.getPrimaryKey();
const joinAlias = this.getAliasCallback(joinTableName);
const joinWhere = this.conditionBuilder.build(value, joinAlias, relationShip.entity());
const on = this.buildJoinOn(relationShip, alias, joinAlias, originPrimaryKey);
if (this.statements.strategy === 'joined') {
this.addJoinedJoin(relationShip, joinTableName, joinSchema, joinAlias, joinWhere, on, alias, schema, tableName, joinHooks);
}
else {
this.addSelectJoin(relationShip, joinTableName, joinSchema, joinAlias, joinWhere, originPrimaryKey, alias, joinHooks);
}
return joinWhere;
}
async handleSelectJoin(entities, models) {
if (!this.statements.selectJoin || this.statements.selectJoin.length === 0) {
return;
}
for (const join of this.statements.selectJoin.reverse()) {
await this.processSelectJoin(join, entities, models);
}
return models;
}
getPathForSelectJoin(selectJoin) {
const path = this.getPathForSelectJoinRecursive(selectJoin);
return path.reverse();
}
buildJoinOn(relationShip, alias, joinAlias, originPrimaryKey) {
let on = '';
switch (relationShip.relation) {
case "one-to-many":
on = `${joinAlias}."${this.getFkKey(relationShip)}" = ${alias}."${originPrimaryKey}"`;
break;
case "many-to-one":
on = `${alias}."${relationShip.columnName}" = ${joinAlias}."${this.getFkKey(relationShip)}"`;
break;
}
return on;
}
addJoinedJoin(relationShip, joinTableName, joinSchema, joinAlias, joinWhere, on, alias, schema, tableName, joinHooks) {
this.statements.join = this.statements.join || [];
this.statements.join.push({
joinAlias: joinAlias,
joinTable: joinTableName,
joinSchema: joinSchema || 'public',
joinWhere: joinWhere,
joinProperty: relationShip.propertyKey,
originAlias: alias,
originSchema: schema,
originTable: tableName,
propertyKey: relationShip.propertyKey,
joinEntity: relationShip.entity(),
type: 'LEFT',
on,
originalEntity: relationShip.originalEntity,
hooks: joinHooks,
});
}
addSelectJoin(relationShip, joinTableName, joinSchema, joinAlias, joinWhere, originPrimaryKey, alias, joinHooks) {
this.statements.selectJoin = this.statements.selectJoin || [];
this.statements.selectJoin.push({
statement: 'select',
columns: this.getOriginalColumnsCallback().filter(column => column.startsWith(`${relationShip.propertyKey}`)).map(column => column.split('.')[1]) || [],
table: `"${joinSchema || 'public'}"."${joinTableName}"`,
alias: joinAlias,
where: joinWhere,
joinProperty: relationShip.propertyKey,
fkKey: this.getFkKey(relationShip),
primaryKey: originPrimaryKey,
originAlias: alias,
originProperty: relationShip.propertyKey,
joinEntity: relationShip.entity(),
originEntity: relationShip.originalEntity,
hooks: joinHooks,
});
}
async processSelectJoin(join, entities, models) {
let ids = this.getIds(join, entities, models);
if (Array.isArray(ids)) {
ids = ids.map((id) => this.formatValue(id)).join(', ');
}
this.updateJoinWhere(join, ids);
this.updateJoinColumns(join);
const child = await this.driver.executeStatement(join);
this.logger.debug(`SQL: ${child.sql} [${Date.now() - child.startTime}ms]`);
this.attachJoinResults(join, child, models);
}
getIds(join, entities, models) {
let ids = entities[`${join.originAlias}_${join.primaryKey}`];
if (typeof ids === 'undefined') {
const selectJoined = this.statements.selectJoin.find(j => j.joinEntity === join.originEntity);
if (!selectJoined) {
return undefined;
}
ids = this.findIdRecursively(models, selectJoined, join);
}
return ids;
}
updateJoinWhere(join, ids) {
if (join.where) {
join.where = `${join.where} AND ${join.alias}."${join.fkKey}" IN (${ids})`;
}
else {
join.where = `${join.alias}."${join.fkKey}" IN (${ids})`;
}
}
updateJoinColumns(join) {
if (join.columns && join.columns.length > 0) {
join.columns = join.columns.map((column) => `${join.alias}."${column}" as "${join.alias}_${column}"`);
}
else {
join.columns = this.columnManager.getColumnsForEntity(join.joinEntity, join.alias);
}
}
attachJoinResults(join, child, models) {
const property = this.entityStorage.get(this.model).relations.find((rel) => rel.propertyKey === join.joinProperty);
const values = child.query.rows.map((row) => this.modelTransformer.transform(join.joinEntity, join, row));
const path = this.getPathForSelectJoin(join);
this.setValueByPath(models, path, property?.type === Array ? [...values] : values[0]);
}
setValueByPath(obj, path, value) {
let currentObj = obj;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
currentObj[key] = currentObj[key] || {};
currentObj = currentObj[key];
}
currentObj[path[path.length - 1]] = value;
}
getPathForSelectJoinRecursive(selectJoin) {
const originJoin = this.statements.selectJoin.find(j => j.joinEntity === selectJoin.originEntity);
let pathInJoin = [];
if (!originJoin) {
return [selectJoin.joinProperty];
}
if (originJoin.originEntity !== this.statements.originEntity) {
pathInJoin = this.getPathForSelectJoinRecursive(originJoin);
}
return [selectJoin.joinProperty, ...pathInJoin];
}
findIdRecursively(models, selectJoined, join) {
let ids = models[selectJoined.originProperty][join.primaryKey];
if (typeof ids === 'undefined') {
const nextSelectJoined = this.statements.selectJoin.find(j => j.joinEntity === selectJoined.originEntity);
if (nextSelectJoined) {
ids = this.findIdRecursively(models, nextSelectJoined, join);
}
}
return ids;
}
getFkKey(relationShip) {
if (typeof relationShip.fkKey === 'undefined') {
return 'id';
}
if (typeof relationShip.fkKey === 'string') {
return relationShip.fkKey;
}
const match = /\.(?<propriedade>[\w]+)/.exec(relationShip.fkKey.toString());
const propertyKey = match ? match.groups.propriedade : '';
const entity = this.entityStorage.get(relationShip.entity());
if (!entity) {
throw new Error(`Entity not found in storage for relationship. ` +
`Make sure the entity ${relationShip.entity().name} is decorated with `);
}
const property = Object.entries(entity.properties).find(([key, _value]) => key === propertyKey)?.[1];
if (property) {
return property.options.columnName;
}
const relation = entity.relations.find(rel => rel.propertyKey === propertyKey);
if (relation && relation.columnName) {
return relation.columnName;
}
throw new Error(`Property or relation "${propertyKey}" not found in entity "${entity.tableName}". ` +
`Available properties: ${Object.keys(entity.properties).join(', ')}. ` +
`Available relations: ${entity.relations.map(r => r.propertyKey).join(', ')}`);
}
getTableName() {
const tableName = this.entity.tableName || this.model.name.toLowerCase();
const schema = this.entity.schema || 'public';
return { tableName, schema };
}
getPrimaryKey() {
for (const prop in this.entity.properties) {
if (this.entity.properties[prop].options.isPrimary) {
return prop;
}
}
return 'id';
}
formatValue(value) {
return (typeof value === 'string') ? `'${value}'` : value;
}
}
exports.SqlJoinManager = SqlJoinManager;