UNPKG

@goatlab/fluent

Version:

Readable query Interface & API generator for TS and Node

393 lines (392 loc) 16.8 kB
"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;