@goatlab/fluent
Version:
Readable query Interface & API generator for TS and Node
380 lines • 13.1 kB
JavaScript
;
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