@sqb/connect
Version:
Multi-dialect database connection framework written with TypeScript
294 lines (293 loc) • 12.8 kB
JavaScript
import { And, In, Param, Select } from '@sqb/builder';
import { AssociationNode } from '../model/association-node.js';
import { EmbeddedFieldMetadata } from '../model/embedded-field-metadata.js';
import { EntityMetadata } from '../model/entity-metadata.js';
import { isAssociationField, isColumnField, isEmbeddedField, } from '../util/orm.helper.js';
import { parseFieldsProjection, } from '../util/parse-fields-projection.js';
import { joinAssociationGetLast, prepareFilter, } from './command.helper.js';
import { RowConverter } from './row-converter.js';
const SORT_ORDER_PATTERN = /^([-+])?(.*)$/;
export class FindCommand {
constructor(selectEntity, outputEntity) {
this.maxEagerFetch = 100000;
this.maxSubQueries = 5;
this.mainAlias = 'T';
this.resultAlias = 'T';
this._joins = [];
this._selectColumns = {};
this._filter = And();
this.mainEntity = selectEntity;
this.resultEntity = outputEntity;
this.converter = new RowConverter(outputEntity.ctor);
}
static async create(source, opts = {}) {
let command;
let listingEntity;
if (source instanceof AssociationNode) {
const node = source;
listingEntity = await node.resolveTarget();
const resultEntity = await node.getLast().resolveTarget();
command = new FindCommand(listingEntity, resultEntity);
if (node.conditions)
await command.filter(node.conditions);
if (node.next) {
const join = await joinAssociationGetLast(command._joins, node.next, command.mainAlias);
command.resultAlias = join.joinAlias;
}
}
else {
listingEntity = source;
command = new FindCommand(listingEntity, listingEntity);
}
if (!listingEntity.tableName)
throw new Error(`${listingEntity.ctor.name} is not decorated with decorator`);
if (typeof opts.maxSubQueries === 'number')
command.maxSubQueries = opts.maxSubQueries;
if (typeof opts.maxEagerFetch === 'number')
command.maxEagerFetch = opts.maxEagerFetch;
return command;
}
static async execute(args) {
const command = await FindCommand.create(args.entity, {
maxSubQueries: args.maxSubQueries,
maxEagerFetch: args.maxEagerFetch,
});
await command.addFields({
projection: args.projection,
sort: args.sort,
});
if (args.filter)
await command.filter(args.filter);
if (args.sort)
await command.sort(args.sort);
return await command.execute(args);
}
async addFields(opts = {}) {
const tableAlias = opts.tableAlias || this.resultAlias;
const entity = opts.entity || this._getEntityFromAlias(tableAlias);
const converter = opts.converter || this.converter;
const projection = typeof opts.projection === 'string' || Array.isArray(opts.projection)
? parseFieldsProjection(opts.projection)
: opts.projection;
const defaultFields = !projection || !Object.values(projection).find(p => !p.sign);
const sortFields = opts.sort && opts.sort.length
? opts.sort.map(x => x.toLowerCase())
: undefined;
const prefix = opts.prefix || '';
const suffix = opts.suffix || '';
for (const key of Object.keys(entity.fields)) {
const col = EntityMetadata.getField(entity, key);
if (!col || col.hidden)
continue;
const colNameLower = col.name.toLowerCase();
const p = projection?.[colNameLower];
if (
/** Ignore if field is omitted */
p?.sign === '-' ||
/** Ignore if default fields and field is not in projection */
(!defaultFields && !p) ||
/** Ignore if default fields enabled and fields is exclusive */
(defaultFields && col.exclusive && !p)) {
continue;
}
// Add field to select list if field is a column
if (isColumnField(col)) {
const fieldAlias = this._selectColumn(tableAlias, col, prefix, suffix);
// Add column to converter
converter.addValueProperty({
name: col.name,
fieldAlias,
dataType: col.dataType,
parse: col.parse,
});
continue;
}
if (isEmbeddedField(col)) {
const typ = await EmbeddedFieldMetadata.resolveType(col);
const subConverter = converter.addObjectProperty({
name: col.name,
type: typ.ctor,
}).converter;
await this.addFields({
tableAlias,
converter: subConverter,
entity: typ,
prefix: col.fieldNamePrefix,
suffix: col.fieldNameSuffix,
projection: p?.projection,
sort: extractSubFields(colNameLower, sortFields),
});
continue;
}
if (isAssociationField(col)) {
// OtO relation
if (!col.association.returnsMany()) {
const joinInfo = await joinAssociationGetLast(this._joins, col.association, tableAlias);
const subConverter = converter.addObjectProperty({
name: col.name,
type: joinInfo.targetEntity.ctor,
}).converter;
// Add join fields to select columns list
await this.addFields({
tableAlias: joinInfo.joinAlias,
converter: subConverter,
entity: joinInfo.targetEntity,
projection: p?.projection,
sort: extractSubFields(colNameLower, sortFields),
});
continue;
}
// One-2-Many Eager relation
if (this.maxSubQueries > 0) {
const targetCol = await col.association.resolveTargetProperty();
const sourceCol = await col.association.resolveSourceProperty();
// We need to know key value to filter sub query.
// So add key field into select columns
const parentField = this._selectColumn(tableAlias, sourceCol);
const findCommand = await FindCommand.create(col.association, {
maxSubQueries: this.maxSubQueries - 1,
maxEagerFetch: this.maxEagerFetch,
});
findCommand.converter.parent = this.converter;
await findCommand.filter(In(targetCol.name, Param(parentField)));
const sort = sortFields && extractSubFields(colNameLower, sortFields);
await findCommand.addFields({
projection: p?.projection,
sort,
});
if (sort)
await findCommand.sort(sort);
const keyField = findCommand._selectColumn(findCommand.mainAlias, targetCol);
const resultType = await col.association.getLast().resolveTarget();
converter.addNestedProperty({
name: col.name,
type: resultType.ctor,
findCommand,
parentField,
keyField,
sort: extractSubFields(colNameLower, sortFields),
});
}
}
}
}
_selectColumn(tableAlias, el, prefix, suffix) {
const fieldName = (prefix || '').toLowerCase() +
el.fieldName.toUpperCase() +
(suffix || '').toLowerCase();
const fieldAlias = (tableAlias + '_' + fieldName).substring(0, 30);
this._selectColumns[fieldAlias] = {
field: el,
statement: tableAlias + '.' + fieldName + ' as ' + fieldAlias,
};
return fieldAlias;
}
async filter(filter) {
await prepareFilter(this.mainEntity, filter, this._filter);
}
async sort(sortFields) {
const out = [];
for (const item of sortFields) {
const m = item.match(SORT_ORDER_PATTERN);
if (!m)
throw new Error(`"${item}" is not a valid order expression`);
let elName = m[2];
let prefix = '';
let suffix = '';
let _entityDef = this.resultEntity;
let tableAlias = this.resultAlias;
if (elName.includes('.')) {
const a = elName.split('.');
while (a.length > 1) {
const col = EntityMetadata.getField(_entityDef, a.shift() || '');
if (isEmbeddedField(col)) {
_entityDef = await EmbeddedFieldMetadata.resolveType(col);
if (col.fieldNamePrefix)
prefix += col.fieldNamePrefix;
if (col.fieldNameSuffix)
suffix = col.fieldNameSuffix + suffix;
}
else if (isAssociationField(col)) {
if (col.association.returnsMany()) {
elName = '';
break;
}
const joinInfo = await joinAssociationGetLast(this._joins, col.association, tableAlias);
tableAlias = joinInfo.joinAlias;
_entityDef = joinInfo.targetEntity;
}
else
throw new Error(`Invalid column (${elName}) declared in sort property`);
}
if (!elName)
continue;
elName = a.shift() || '';
}
const col = EntityMetadata.getField(_entityDef, elName);
if (!col)
throw new Error(`Unknown field (${elName}) declared in sort property`);
if (!isColumnField(col))
throw new Error(`Can not sort by "${elName}", because it is not a data column`);
const dir = m[1] || '+';
out.push((dir || '') + tableAlias + '.' + prefix + col.fieldName + suffix);
}
this._sort = out;
}
async execute(args) {
// Generate select query
const columnSqls = Object.keys(this._selectColumns).map(x => this._selectColumns[x].statement);
if (!columnSqls.length)
columnSqls.push('1');
const query = Select(...columnSqls).from(this.mainEntity.tableName + ' as ' + this.mainAlias);
if (args.distinct)
query.distinct();
query.where(...this._filter._items);
if (this._sort) {
query.orderBy(...this._sort);
}
if (args.offset)
query.offset(args.offset);
// joins must be added last
if (this._joins) {
for (const j of this._joins) {
query.join(j.join);
}
}
// Execute query
const resp = await args.connection.execute(query, {
params: args?.params,
fetchRows: args?.limit,
objectRows: false,
cursor: false,
prettyPrint: args.prettyPrint,
});
// Create objects
if (resp.rows && resp.fields) {
return this.converter.transform(args.connection, resp.fields, resp.rows, args.onTransformRow);
}
return [];
}
_getEntityFromAlias(tableAlias) {
if (tableAlias === this.mainAlias)
return this.mainEntity;
if (tableAlias === this.resultAlias)
return this.resultEntity;
if (this._joins) {
const join = this._joins.find(j => j.joinAlias === tableAlias);
if (join)
return join.targetEntity;
}
throw new Error(`Unknown table alias "${tableAlias}"`);
}
}
function extractSubFields(colNameLower, fields) {
if (!(fields && fields.length))
return;
if (fields) {
return fields.reduce((trg, v) => {
if (v.startsWith(colNameLower + '.'))
trg.push(v.substring(colNameLower.length + 1).toLowerCase());
return trg;
}, []);
}
}