UNPKG

@goatlab/fluent

Version:

Readable query Interface & API generator for TS and Node

380 lines 13.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseConnector = void 0; const js_utils_1 = require("@goatlab/js-utils"); const clearEmpties_1 = require("./TypeOrmConnector/util/clearEmpties"); class BaseConnector { outputKeys; relatedQuery; chunk = null; pullSize = null; paginator = undefined; rawQuery = undefined; modelRelations; isMongoDB; constructor() { this.chunk = null; this.pullSize = null; this.paginator = undefined; this.rawQuery = undefined; this.outputKeys = []; } /** * * @param data */ async insertMany(_data) { throw new Error('get() method not implemented'); } /** * * @param id * @param data */ async updateById(_id, _data) { throw new Error('get() method not implemented'); } /** * * @param query */ async findMany(_query) { throw new Error('findMany() method not implemented'); } /** * Executes the findMany() method and * returns it's first result * * @return {Object} First result */ async findFirst(query) { const data = await this.findMany({ ...query, limit: 1 }); if (!data[0]) { return null; } return data[0]; } async requireById(id, q) { const found = await this.findByIds([id], { select: q?.select, include: q?.include, limit: 1 }); for (let i = 0; i < found.length; i++) { const d = found[i]; if (this.isMongoDB) { // Handle both _id and id cases if (d._id) { d.id = d._id.toString(); delete d._id; } else if (d.id && typeof d.id !== 'string') { d.id = d.id.toString(); } } (0, clearEmpties_1.clearEmpties)(js_utils_1.Objects.deleteNulls(d)); } if (!found[0]) { throw new Error(`Object ${id} not found`); } // No need to validate, as findMany already validates return found[0]; } async requireFirst(query) { const found = await this.findMany({ ...query, limit: 1 }); for (let i = 0; i < found.length; i++) { const d = found[i]; if (this.isMongoDB) { // Handle both _id and id cases if (d._id) { d.id = d._id.toString(); delete d._id; } else if (d.id && typeof d.id !== 'string') { d.id = d.id.toString(); } } (0, clearEmpties_1.clearEmpties)(js_utils_1.Objects.deleteNulls(d)); } if (!found[0]) { const stringQuery = query ? JSON.stringify(query) : ''; throw new Error(`No objects found matching: ${stringQuery}`); } // The object is already validated by findMany return found[0]; } async findByIds(ids, q) { const query = { where: { id: { in: ids } }, limit: q?.limit, select: q?.select, include: q?.include }; const data = await this.findMany(query); // The object should already be validated by FindMany return data; } async findById(id, q) { const result = await this.findByIds([id], { ...q, limit: 1 }); if (!result[0]) { return null; } return result[0]; } /** * * Gets the data in the current query and * transforms it into a collection * @returns {Collection} Fluent Collection */ async collect(query) { const data = await this.findMany(query); if (!Array.isArray(data)) { return new js_utils_1.Collection([data]); } return new js_utils_1.Collection(data); } /** * Gets all values for a given KEY * @returns {Array} * @param path */ async pluck(path, query) { const data = await this.findMany(query); const paths = Object.keys(js_utils_1.Objects.flatten(path)); const result = []; const pathStr = String(paths[0]); for (let i = 0; i < data.length; i++) { const extracted = js_utils_1.Objects.getFromPath(data[i], pathStr, undefined); if (typeof extracted.value !== 'undefined') { result.push(extracted.value); } } return result; } /** * Sets the relatedQuery param, to be used by the * different LOAD methods * @param r */ setRelatedQuery(r) { this.relatedQuery = r; } /** * Associate One-to-Many relationship. * Associate an object to the parent. * @param data */ async associate(data) { if (!this.relatedQuery?.entity || !this.relatedQuery.key) { throw new Error('Associate can only be called as a related model'); } // Get ids of the Loaded query "One" side of relation const parentData = await this.relatedQuery.repository.findMany({ ...this.relatedQuery.query, // We just need the IDs to make the relations select: { id: true } }); // "Many" side of relation foreignKey const foreignKeyName = this.relatedQuery.repository.modelRelations[this.relatedQuery.key] .inverseSidePropertyPath; if (!foreignKeyName) { throw new Error('The relationship was not properly defined. Please check that your Repository and Model relations have the same keys'); } const relatedData = parentData.map(r => ({ [foreignKeyName]: r.id, ...data })); const existingIds = (0, clearEmpties_1.clearEmpties)(relatedData.map(r => r.id)); const existingData = existingIds.length ? await this.findByIds(relatedData.map(r => r.id)) : []; const updateQueries = []; const insertQueries = []; for (const related of relatedData) { const exists = existingData.find((d) => { // We need to manually define the id field const p = d; return p.id === related.id; }); if (exists) { updateQueries.push(this.updateById(exists.id, { ...exists, [foreignKeyName]: related[foreignKeyName] })); } else { insertQueries.push(related); } } const updateResult = await Promise.all(updateQueries); const insertedResult = await this.insertMany(insertQueries); return [...updateResult, ...insertedResult]; } /** * Attach an object with Many-to-Many relation * @param id */ // TODO: properly type the pivot object async attach(id, pivot) { if (!this.relatedQuery?.entity || !this.relatedQuery.key) { throw new Error('Associate can only be called as a related model'); } const parentData = await this.relatedQuery.repository.findMany({ ...this.relatedQuery.query, // We just need the IDs to make the relations select: { id: true } }); const foreignKeyName = this.relatedQuery.repository.modelRelations[this.relatedQuery.key] .joinColumns[0].propertyPath; const inverseKeyName = this.relatedQuery.repository.modelRelations[this.relatedQuery.key] .inverseJoinColumns[0].propertyPath; if (!foreignKeyName || !inverseKeyName) { throw new Error(`The relationship was not properly defined. Please check that your Repository and Model relations have the same keys: Searching for: ${this.relatedQuery.key}`); } // TODO: insert data to the pivot table const relatedData = parentData.map(d => ({ [foreignKeyName]: d.id, [inverseKeyName]: id, ...pivot })); return this.relatedQuery.pivot.insertMany(relatedData); } /** * One-to-Many relationship * To be used in the "parent" entity (One) */ hasMany(r) { // Handle both constructor and factory function patterns const newRepo = typeof r.repository === 'function' && r.repository.prototype && r.repository.prototype.constructor === r.repository ? new r.repository() : r.repository(); const calleeName = new Error('dummy').stack?.split('\n')[2] || '' // " at functionName ( ..." => "functionName" .replace(/^\s+at\s+(.+?)\s.+/g, '$1') .split('.')[1]; if (this.relatedQuery) { newRepo.setRelatedQuery({ ...this.relatedQuery, key: calleeName }); } return newRepo; } /** * Inverse One-to-Many relationship * To be used in the "children" entity (Many) */ belongsTo(r) { return this.hasMany(r); } /** * One-to-One model relationship */ // TODO implement hasOne hasOne() { throw new Error('Method not implemented'); } /** * Many-to-Many relationship * To be used in both of the Related models (excluding pivot) */ belongsToMany(r) { // Handle both constructor and factory function patterns const newRepo = typeof r.repository === 'function' && r.repository.prototype && r.repository.prototype.constructor === r.repository ? new r.repository() : r.repository(); // Hacky way to get the name of the callee function const relationName = new Error('dummy').stack?.split('\n')[2] || '' // " at functionName ( ..." => "functionName" .replace(/^\s+at\s+(.+?)\s.+/g, '$1') .split('.')[1]; // Handle both constructor and factory function patterns for pivot const pivot = typeof r.pivot === 'function' && r.pivot.prototype && r.pivot.prototype.constructor === r.pivot ? new r.pivot() : r.pivot(); pivot.setRelatedQuery({ ...this.relatedQuery, key: relationName }); if (this.relatedQuery) { newRepo.setRelatedQuery({ ...this.relatedQuery, key: relationName, pivot }); } else { newRepo.setRelatedQuery({ key: relationName, pivot }); } // this.relationQuery.relations[relationName] return newRepo; } /** * */ // TODO implement hasManyThrough hasManyThrough() { throw new Error('Method not implemented'); } /** * Maps the given Data to show only those fields * explicitly detailed on the Select function * * @param {Array} data Data from local or remote DB * @returns {Array} Formatted data with the selected columns */ jsApplySelect(select, data) { const Data = Array.isArray(data) ? [...data] : [data]; if (!select) { return data; } const selectedAttributes = Object.keys(js_utils_1.Objects.flatten(select)); const iterationArray = this.outputKeys.length === 0 && selectedAttributes.length > 0 ? selectedAttributes : [...this.outputKeys]; const compareArray = this.outputKeys.length === 0 && selectedAttributes.length > 0 ? [...this.outputKeys] : selectedAttributes; return Data.map(element => { const newElement = {}; iterationArray.forEach(attribute => { if (compareArray.length > 0 && !compareArray.includes(attribute)) { return undefined; } const extract = js_utils_1.Objects.getFromPath(element, attribute, undefined); const value = js_utils_1.Objects.get(() => extract.value, undefined); if (typeof value !== 'undefined' && value !== null) { if (typeof value === 'object' && Object.hasOwn(value, 'data') && Object.hasOwn(value.data, 'name')) { newElement[extract.label] = value.data.name; } else { // Skip ObjectId conversion for now to fix build // TODO: Implement proper ObjectId handling newElement[extract.label] = value; } } }); return js_utils_1.Objects.nest(newElement); }); } } exports.BaseConnector = BaseConnector; //# sourceMappingURL=BaseConnector.js.map