@goatlab/fluent
Version:
Readable query Interface & API generator for TS and Node
393 lines (392 loc) • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypeOrmConnector = void 0;
const tslib_1 = require("tslib");
const js_utils_1 = require("@goatlab/js-utils");
const BaseConnector_1 = require("../BaseConnector");
const outputKeys_1 = require("../outputKeys");
const generatorDatasource_1 = require("../generatorDatasource");
const getMongoWhere_1 = require("./queryBuilder/mongodb/getMongoWhere");
const getRelationsFromModelGenerator_1 = require("./util/getRelationsFromModelGenerator");
const getMongoFindAggregatedQuery_1 = require("./queryBuilder/mongodb/getMongoFindAggregatedQuery");
const extractInclude_1 = require("./util/extractInclude");
const extractOrderBy_1 = require("./util/extractOrderBy");
const getTypeOrmWhere_1 = require("./queryBuilder/sql/getTypeOrmWhere");
const getQueryBuilderWhere_1 = require("./queryBuilder/sql/getQueryBuilderWhere");
const clearEmpties_1 = require("./util/clearEmpties");
class TypeOrmConnector extends BaseConnector_1.BaseConnector {
constructor({ entity, dataSource, inputSchema, outputSchema }) {
super();
this.dataSource = dataSource;
this.inputSchema = inputSchema;
this.outputSchema =
outputSchema || inputSchema;
this.entity = entity;
}
initDB() {
this.repository = this.dataSource.getRepository(this.entity);
this.isMongoDB =
this.repository.metadata.connection.driver.options.type === 'mongodb';
if (this.isMongoDB) {
this.repository = this.dataSource.getMongoRepository(this.entity);
}
const relationShipBuilder = generatorDatasource_1.modelGeneratorDataSource.getRepository(this.entity);
const { relations } = (0, getRelationsFromModelGenerator_1.getRelationsFromModelGenerator)(relationShipBuilder);
this.modelRelations = relations;
this.outputKeys = (0, outputKeys_1.getOutputKeys)(relationShipBuilder) || [];
return 1;
}
async insert(data) {
this.initDB();
const validatedData = this.inputSchema.parse(data);
if (this.isMongoDB && validatedData['id']) {
validatedData['_id'] = js_utils_1.Ids.objectID(validatedData['id']);
delete validatedData['id'];
}
let datum = await this.repository.save(validatedData);
if (this.isMongoDB) {
datum['id'] = datum['id'].toString();
}
return this.outputSchema.parse((0, clearEmpties_1.clearEmpties)(js_utils_1.Objects.deleteNulls(datum)));
}
async insertMany(data) {
this.initDB();
const validatedData = this.inputSchema.array().parse(data);
const inserted = await this.repository.save(validatedData, {
chunk: data.length / 300
});
return this.outputSchema.array().parse(inserted.map(d => {
if (this.isMongoDB) {
d['id'] = d['id'].toString();
}
return (0, clearEmpties_1.clearEmpties)(js_utils_1.Objects.deleteNulls(d));
}));
}
async findMany(query) {
this.initDB();
const requiresCustomQuery = query?.include && Object.keys(query.include).length;
if (this.isMongoDB) {
const results = await this.customMongoRelatedFind(query);
return results;
}
if (requiresCustomQuery) {
const { queryBuilder: customQuery, selectedKeys } = this.customTypeOrmRelatedFind({
fluentQuery: query
});
customQuery.select(selectedKeys);
let [result, count] = await customQuery.getManyAndCount();
return result;
}
const generatedQuery = this.generateTypeOrmQuery(query);
let [found, count] = await this.repository.findAndCount(generatedQuery);
found.map(d => {
if (this.isMongoDB) {
d['id'] = d['_id'].toString();
}
(0, clearEmpties_1.clearEmpties)(js_utils_1.Objects.deleteNulls(d));
});
if (query?.paginated) {
const paginationInfo = {
total: count,
perPage: query.paginated.perPage,
currentPage: query.paginated.page,
nextPage: query.paginated.page + 1,
firstPage: 1,
lastPage: Math.ceil(count / query.paginated.perPage),
prevPage: query.paginated.page === 1 ? null : query.paginated.page - 1,
from: (query.paginated.page - 1) * query.paginated.perPage + 1,
to: query.paginated.perPage * query.paginated.page,
data: found
};
return paginationInfo;
}
if (query?.select) {
return found;
}
return this.outputSchema?.array().parse(found);
}
async updateById(id, data) {
this.initDB();
const dataToInsert = this.outputKeys.includes('updated')
? {
...data,
...{ updated: new Date() }
}
: data;
const validatedData = this.inputSchema.parse(dataToInsert);
await this.repository.update(id, validatedData);
return (await this.requireById(id));
}
async replaceById(id, data) {
this.initDB();
const idFieldName = this.isMongoDB ? '_id' : 'id';
const value = this.requireById(id);
const flatValue = js_utils_1.Objects.flatten(JSON.parse(JSON.stringify(value)));
Object.keys(flatValue).forEach(key => {
flatValue[key] = null;
});
const nullObject = js_utils_1.Objects.nest(flatValue);
const newValue = { ...nullObject, ...data };
delete newValue._id;
delete newValue.id;
delete newValue.created;
delete newValue.updated;
const dataToInsert = this.outputKeys.includes('updated')
? {
...data,
...{ updated: new Date() }
}
: data;
const validatedData = this.inputSchema.parse(dataToInsert);
await this.repository.update(id, validatedData);
return (await this.requireById(id));
}
async deleteById(id) {
this.initDB();
const parsedId = this.isMongoDB
? js_utils_1.Ids.objectID(id)
: id;
await this.repository.delete(parsedId);
return id;
}
async clear() {
this.initDB();
await this.repository.clear();
return true;
}
loadFirst(query) {
this.initDB();
const newInstance = this.clone();
newInstance.setRelatedQuery({
entity: this.entity,
repository: this,
query: {
...query,
limit: 1
}
});
return newInstance;
}
loadById(id) {
this.initDB();
const newInstance = this.clone();
newInstance.setRelatedQuery({
entity: this.entity,
repository: this,
query: {
where: {
id
}
}
});
return newInstance;
}
raw() {
this.initDB();
return this.repository;
}
mongoRaw() {
this.initDB();
return this.repository;
}
clone() {
this.initDB();
return new this.constructor();
}
generateTypeOrmQuery(query) {
let filter = {};
filter.where = this.isMongoDB
? (0, getMongoWhere_1.getMongoWhere)({
where: query?.where
})
: (0, getTypeOrmWhere_1.getTypeOrmWhere)({
where: query?.where
});
filter.take = query?.limit;
filter.skip = query?.offset;
if (query?.paginated) {
filter.take = query.paginated.perPage;
filter.skip = (query.paginated?.page - 1) * query?.paginated.perPage;
}
if (query?.select) {
const selectQuery = js_utils_1.Objects.flatten(query?.select || {});
filter.select = selectQuery;
}
if (query?.orderBy) {
filter.order = (0, extractOrderBy_1.extractOrderBy)(query.orderBy);
}
if (query?.include) {
filter.relations = (0, extractInclude_1.extractInclude)(query.include);
}
return filter;
}
customTypeOrmRelatedFind({ fluentQuery: query, queryBuilder, targetFluentRepository, alias, isLeftJoin }) {
const queryAlias = alias || queryBuilder?.alias || `${this.repository.metadata.tableName}`;
let customQuery = queryBuilder || this.raw().createQueryBuilder(queryAlias);
const self = targetFluentRepository || this;
if (!isLeftJoin) {
customQuery = (0, getQueryBuilderWhere_1.getQueryBuilderWhere)({
queryBuilder: customQuery,
queryAlias,
where: query?.where
});
}
const { queryBuilder: qb, selectedKeys } = this.getTypeOrmQueryBuilderSubqueries({
queryBuilder: customQuery,
selfReference: targetFluentRepository,
include: query?.include,
leftTableAlias: alias
});
customQuery = qb;
const extraKeys = this.getTypeOrmQueryBuilderSelect(queryAlias, self, query?.select);
const keySet = new Set([...selectedKeys, ...extraKeys]);
return {
queryBuilder: customQuery,
selectedKeys: Array.from(keySet)
};
}
getTypeOrmQueryBuilderSelect(queryAlias, self, select) {
const selected = js_utils_1.Objects.flatten(select || {});
const selectedKeys = [];
const iterableKeys = Object.keys(selected).length
? Object.keys(selected)
: self.outputKeys || [];
const baseNestedKeys = new Set();
for (const key of iterableKeys) {
const keyArray = key.split('.');
if (keyArray.length <= 1) {
continue;
}
const total = keyArray.length;
for (const [index, val] of keyArray.entries()) {
if (total === index + 1) {
continue;
}
let excludedField = '';
if (excludedField) {
excludedField = `${excludedField}.${excludedField}${val}`;
}
excludedField = `${excludedField}${val}`;
baseNestedKeys.add(excludedField);
}
}
for (const k of iterableKeys) {
const field = k.includes('.') ? js_utils_1.Strings.camel(`${k}`) : k;
const search = `${queryAlias}.${field}`;
let isNestedRelation = false;
for (const item of k.split('.')) {
if (!!self[item]) {
isNestedRelation = true;
break;
}
}
if (!!self[field] || !!self[queryAlias] || isNestedRelation) {
continue;
}
if (baseNestedKeys.has(field)) {
continue;
}
selectedKeys.push(search);
}
return selectedKeys;
}
getTypeOrmQueryBuilderSubqueries({ queryBuilder, selfReference, include, leftTableAlias }) {
const selectedKeys = [];
if (!include) {
return { queryBuilder, selectedKeys };
}
for (const relation of Object.keys(include)) {
const self = selfReference || this;
const dbRelation = self.modelRelations[relation];
const newSelf = self[relation]();
const fluentRelatedQuery = include[relation] === true ? {} : include[relation];
if (!dbRelation) {
throw new Error(`The relation ${relation} is not properly defined. Check your entity and repository`);
}
const selectedKeysArray = fluentRelatedQuery.select
? Object.keys(js_utils_1.Objects.flatten(fluentRelatedQuery.select))
: [];
if (dbRelation.isManyToOne) {
const leftSideTableName = leftTableAlias || queryBuilder.alias;
const leftSideForeignKey = `${leftSideTableName}.${dbRelation.joinColumns[0].propertyPath}`;
const rightSideTableName = `${leftSideTableName}_${relation}`;
const rightSidePrimaryKey = `${rightSideTableName}.id`;
const keys = new Set(selectedKeysArray.map(k => `${rightSideTableName}.${k}`));
selectedKeys.push(...Array.from(keys));
const shallowQuery = { ...fluentRelatedQuery };
delete shallowQuery['include'];
const { queryBuilder: leftJoinBuilder, selectedKeys: deepkeys } = this.customTypeOrmRelatedFind({
queryBuilder: this.raw().createQueryBuilder(rightSideTableName),
fluentQuery: shallowQuery,
targetFluentRepository: newSelf,
alias: rightSideTableName
});
selectedKeys.push(...deepkeys);
const joinQuery = leftJoinBuilder.getQuery().split('WHERE');
const customLeftJoin = joinQuery && joinQuery[1] ? joinQuery[1].trim() : '1=1';
const leftJoinParams = leftJoinBuilder.getParameters();
queryBuilder.leftJoinAndMapOne(`${leftSideTableName}.${relation}`, dbRelation.targetClass, rightSideTableName, `(${leftSideForeignKey} = ${rightSidePrimaryKey} AND ${customLeftJoin} )`, leftJoinParams);
const { queryBuilder: qb, selectedKeys: k } = this.customTypeOrmRelatedFind({
queryBuilder,
fluentQuery: fluentRelatedQuery,
targetFluentRepository: newSelf,
alias: rightSideTableName,
isLeftJoin: true
});
selectedKeys.push(...k);
queryBuilder = qb;
}
if (dbRelation.isOneToMany) {
const leftSideTableName = leftTableAlias || queryBuilder.alias;
const leftSidePrimaryKey = `${leftSideTableName}.id`;
const rightSideTableName = `${leftSideTableName}_${relation}`;
const rightSideForeignKey = `${rightSideTableName}.${dbRelation.inverseSidePropertyPath}`;
const keys = new Set(selectedKeysArray.map(k => `${rightSideTableName}.${k}`));
selectedKeys.push(...Array.from(keys));
const shallowQuery = { ...fluentRelatedQuery };
delete shallowQuery['include'];
const { queryBuilder: leftJoinBuilder, selectedKeys: deepKeys } = this.customTypeOrmRelatedFind({
queryBuilder: this.raw().createQueryBuilder(rightSideTableName),
fluentQuery: shallowQuery,
targetFluentRepository: newSelf,
alias: rightSideTableName
});
selectedKeys.push(...deepKeys);
const joinQuery = leftJoinBuilder.getQuery().split('WHERE');
const customLeftJoin = joinQuery && joinQuery[1] ? joinQuery[1].trim() : '1=1';
const leftJoinParams = leftJoinBuilder.getParameters();
queryBuilder.leftJoinAndMapMany(`${leftSideTableName}.${relation}`, dbRelation.targetClass, rightSideTableName, `(${leftSidePrimaryKey} = ${rightSideForeignKey} AND ${customLeftJoin} )`, leftJoinParams);
const { queryBuilder: q, selectedKeys: k } = this.customTypeOrmRelatedFind({
queryBuilder,
fluentQuery: fluentRelatedQuery,
targetFluentRepository: newSelf,
alias: rightSideTableName,
isLeftJoin: true
});
selectedKeys.push(...k);
queryBuilder = q;
}
}
return { queryBuilder, selectedKeys };
}
async customMongoRelatedFind(query) {
const aggregate = (0, getMongoFindAggregatedQuery_1.getMongoFindAggregatedQuery)({
query,
self: this
});
const raw = await this.mongoRaw().aggregate(aggregate).toArray();
if (query?.select) {
return this.outputSchema['deepPartial']()
.array()
.parse(raw);
}
return this.outputSchema?.array().parse(raw);
}
}
tslib_1.__decorate([
js_utils_1.Memo.syncMethod(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], TypeOrmConnector.prototype, "initDB", null);
exports.TypeOrmConnector = TypeOrmConnector;