UNPKG

respond-framework

Version:
546 lines (541 loc) 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _mongodb = require("mongodb"); var _constants = require("../helpers/constants.js"); var _indexMock = require("./index.mock.js"); var _createJoin = require("./aggregates/createJoin.js"); var _toFromObjectIds = require("./helpers/toFromObjectIds.js"); var _createAggregateStages = require("./aggregates/createAggregateStages.js"); var _createQuerySelector = require("./helpers/createQuerySelector.js"); var _safeMethods = require("./safeMethods.js"); var _pick = require("./utils/pick.js"); var _createQueryKey = require("./helpers/createQueryKey.js"); var _default = exports.default = _constants.isDev ? _indexMock.default : { async findOne(selector = {}, { project, sort = { updatedAt: -1 } } = {}) { selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); const doc = await this.mongo().findOne(selector, { projection: (0, _toFromObjectIds.toProject)(project), sort }); return doc && this.super.create(doc); }, async findMany(selector, { project, sort = { updatedAt: -1, _id: 1 }, limit = this.config.listLimit ?? 10, skip = 0 } = {}) { selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); const models = await this.mongo().find(selector).sort(sort).skip(skip * limit).limit(limit).project((0, _toFromObjectIds.toProject)(project)).toArray(); return models.map(m => this.super.create(m)); }, async insertOne({ id, ...doc }, { project } = {}) { doc._id = new _mongodb.ObjectId(id); doc.createdAt = doc.updatedAt = new Date(doc.createdAt || new Date()); doc = (0, _toFromObjectIds.toObjectIds)(doc); await this.mongo().insertOne(doc); return project ? (0, _pick.pickAndCreate)(doc, project, this) : this.super.create(doc); }, async updateOne(selector, newDoc, { project } = {}) { const id = typeof selector === 'string' ? selector : selector.id; const { createdAt: _, updatedAt: __, ...doc } = newDoc || selector; // accept signature: updateOne(doc) selector = (0, _toFromObjectIds.toObjectIdsSelector)(id ? { id } : selector); const result = await this.mongo().findOneAndUpdate(selector, { $set: (0, _toFromObjectIds.toObjectIds)(doc), $currentDate: { updatedAt: true } }, { projection: (0, _toFromObjectIds.toProject)(project), returnDocument: 'after' }); return result.value ? this.super.create(result.value) : id ? this.super.insertOne({ id, ...doc }, { project }) : undefined; // honor id: client-created id or deleted doc }, async upsert(selector = {}, newDoc, { insertDoc, project } = {}) { const id = typeof selector === 'string' ? selector : selector.id; const { createdAt: _, updatedAt: __, ...doc } = newDoc || selector; // upsert accepts this signature: upsert(doc) selector = (0, _toFromObjectIds.toObjectIdsSelector)(id ? { id } : selector); insertDoc = (0, _toFromObjectIds.toObjectIdsSelector)(insertDoc); const result = await this.mongo().findOneAndUpdate(selector, { $set: (0, _toFromObjectIds.toObjectIds)(doc), $setOnInsert: { ...selector, ...insertDoc, createdAt: new Date() }, $currentDate: { updatedAt: true } }, { upsert: true, projection: (0, _toFromObjectIds.toProject)(project), returnDocument: 'after' }); return result.value && this.super.create(result.value); }, async findAll(selector, options) { return this.super.findMany(selector, { ...options, limit: 0 }); }, async findLike(key, term, { selector, ...options } = {}) { term = term.replace(/\\*$/g, ''); // backslashes cant exist at end of regex const value = new RegExp(`^${term}`, 'i'); return this.super.findMany({ ...selector, [key]: value }, options); }, async findManyPaginated(selector, options = {}) { const skip = options.skip ? parseInt(options.skip) : 0; const [models, count] = await Promise.all([this.super.findMany(selector, { ...options, skip }), this.count(selector)]); return { [this._namePlural]: models, count, skip }; }, async search(query, { path = ['firstName', 'lastName'], selector, project, limit = this.config.listLimit ?? 10, skip = 0 } = {}) { selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); if (this.config.useLocalDb) { return this.super.findMany({ $text: { $search: query }, ...selector }, { project, skip, limit, sort: { updatedAt: 1 } }); } if (!query) return []; // mongo fails on empty query const models = await this.mongo().aggregate([{ $search: { index: 'nameSearch', text: { query, path, fuzzy: { maxEdits: 2, prefixLength: 1, maxExpansions: 100 } } } }, ...(selector ? [{ $match: selector }] : []), { $skip: skip * limit }, { $limit: limit }, ...(project ? [{ $project: (0, _toFromObjectIds.toProject)(project) }] : [])]).toArray(); return models.map(m => this.super.create(m)); }, async searchGeo({ lng, lat }, { selector, project, limit = this.config.listLimit ?? 10, skip = 0 } = {}) { selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); if (this.config.useLocalDb) { return this.super.findMany(selector, { project, limit, skip, sort: { updatedAt: 1 } }); // updateAt: 1, sorts in opposite of default direction to indicate something happened } const models = await this.mongo().aggregate([{ $geoNear: { near: { type: 'Point', coordinates: [lng, lat] }, spherical: true, distanceField: 'distance', key: 'location' } }, ...(selector ? [{ $match: selector }] : []), { $skip: skip * limit }, { $limit: limit }, ...(project ? [{ $project: (0, _toFromObjectIds.toProject)(project) }] : [])]).toArray(); return models.map(m => this.super.create(m)); }, async joinOne(id, name, { project, projectJoin, sort = { updatedAt: -1, _id: 1 } } = {}) { const coll = this.db[name]; const parentName = this._name; const fk = parentName + 'Id'; const selector = (0, _toFromObjectIds.toObjectIdsSelector)({ [fk]: id }); project = (0, _toFromObjectIds.toProject)(project); projectJoin = (0, _toFromObjectIds.toProject)(projectJoin); let [parent, children] = await Promise.all([this.super.findOne(id, project), coll.super.findOne(selector, { project: projectJoin, sort })]); return { [parentName]: parent, [name]: children }; }, async joinMany(id, name, { project, projectJoin, sort = { updatedAt: -1, _id: 1 }, limit = this.config.listLimit ?? 10, skip = 0 } = {}) { const coll = this.db[name]; const parentName = this._name; const fk = parentName + 'Id'; const selector = (0, _toFromObjectIds.toObjectIdsSelector)({ [fk]: id }); project = (0, _toFromObjectIds.toProject)(project); projectJoin = (0, _toFromObjectIds.toProject)(projectJoin); let [parent, children] = await Promise.all([this.super.findOne(id, project), coll.super.findMany(selector, { project: projectJoin, sort, limit, skip })]); return { [parentName]: parent, [coll._namePlural]: children }; }, async join(name, { foreignKey: fk, selector, selectorJoin, project, projectJoin, sort, sortJoin, limit = this.config.listLimit ?? 10, limitJoin = this.config.listLimit ?? 10, skip = 0, innerJoin } = {}) { sort ??= { updatedAt: -1, id: -1 }; sortJoin ??= { updatedAt: -1, id: -1 }; fk ??= this._name + 'Id'; const localField = '_id'; const coll = this.db[name]; project = (0, _toFromObjectIds.toProject)(project); selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); sort = (0, _toFromObjectIds.toObjectIdsSelector)(sort); projectJoin = (0, _toFromObjectIds.toProject)(projectJoin); selectorJoin = (0, _toFromObjectIds.toObjectIdsSelector)(selectorJoin); sortJoin = (0, _toFromObjectIds.toObjectIdsSelector)(sortJoin); const stages = [...(selector ? [{ $match: selector }] : []), { $sort: sort }, { $skip: skip * limit }, { $limit: limit }, ...(0, _createJoin.default)({ collection: this._namePlural, from: name, foreignField: fk, localField, project, projectJoin, selector: selectorJoin, sort: sortJoin, limit: limitJoin, inner: innerJoin })]; const results = await this.mongo().aggregate(stages).next(); // result of createJoin is first element in array const outer = this._namePlural; const inner = coll._namePlural; return { [outer]: results[outer].map(m => this.super.create(m)), [inner]: results[name].map(m => coll.super.create(m)) }; }, async queryPaginated(query, { project } = {}) { const { sortKey = 'updatedAt', sortValue = -1, limit = this.config.listLimit ?? 10, skip = 0, location, ...sel } = query; const selector = this.createQuerySelector(sel); // advanced filtering suitable for an admin panel const sort = { [sortKey]: sortValue, _id: sortValue }; const [count, models] = await Promise.all([this.count(selector), location ? this.super.searchGeo(location, { selector, project, limit, skip }) : this.super.findMany(selector, { project, sort, limit, skip })]); const total = Math.ceil(count / limit); const hasNext = skip + 1 < total; const next = hasNext ? skip + 1 : null; return { query, count, total, next, index: skip, [this._namePlural]: models, page: models.map(m => m.id), key: this.createQueryKey(query) }; }, async aggregatePaginated(query, { project } = {}) { const { sortKey = 'updatedAt', sortValue = -1, limit = this.config.listLimit ?? 10, skip = 0, startDate, endDate, location, ...sel } = query; const selector = this.createQuerySelector((0, _toFromObjectIds.toObjectIdsSelector)(sel)); // clear unused params, transform regex strings, date handling const sort = { [sortKey]: sortValue, _id: sortValue, location }; const stages = this.aggregateStages?.map(s => ({ ...s, startDate, endDate })); const { count, [this._namePlural]: models } = await this.super.aggregate({ selector, stages, project, sort, limit, skip }); const total = Math.ceil(count / limit); const hasNext = skip + 1 < total; const next = hasNext ? skip + 1 : null; return { query, count, total, next, index: skip, [this._namePlural]: models, page: models.map(m => m.id), key: this.createQueryKey(query) }; }, async aggregate({ selector, stages: specs = [], // stages in userland are respond-specific "specs" which abstracts the underlying implementation project, sort = { updatedAt: -1, _id: 1 }, limit = this.config.listLimit ?? 10, skip = 0 } = {}) { const stages = (0, _createAggregateStages.default)(specs, { collection: this, selector, project, sort, limit, skip }); const countPromise = this.mongo().aggregate((0, _createAggregateStages.createStagesCount)(stages)).toArray(); const docsPromise = this.mongo().aggregate(stages).toArray(); const [counts, docs] = await Promise.all([countPromise, docsPromise]); const count = counts[0]?.count ?? 0; const models = docs.map(m => this.super.create(m)); return { count, [this._namePlural]: models }; }, async count(selector) { return this.mongo().count((0, _toFromObjectIds.toObjectIdsSelector)(selector)); }, async totalPages(selector, limit = this.config.listLimit ?? 10) { const count = await this.mongo().count((0, _toFromObjectIds.toObjectIdsSelector)(selector)); return Math.ceil(count / limit); }, async insertMany(docs) { return this.mongo().insertMany(docs); }, async updateMany(selector, $set) { return this.mongo().updateMany((0, _toFromObjectIds.toObjectIdsSelector)(selector), { $set: (0, _toFromObjectIds.toObjectIds)($set) }); }, async deleteMany(selector) { selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); await this.mongo().deleteMany(selector); }, async deleteOne(selector) { selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); return this.mongo().deleteOne(selector); }, async incrementOne(selector, $inc) { selector = (0, _toFromObjectIds.toObjectIdsSelector)(selector); return this.mongo().updateOne(selector, { $inc, $set: { updatedAt: new Date() } }); }, create(doc) { if (!doc) return new this.Model(); const { _id, id = _id, ...rest } = (0, _toFromObjectIds.fromObjectIds)(doc); // mongo ObjectId objects converted to strings for ez client consumption const revived = this.db.revive({ ...rest, id }); return new this.Model(revived); }, make(doc) { const revived = this.db.revive(doc); return new this.Model(revived, false); }, save(model) { return this.upsert(model); }, mongo() { if (this._mongoCollection) return this._mongoCollection; const str = typeof this.config.dbConnectionString === 'function' ? this.config.dbConnectionString() : this.config.dbConnectionString; const dbName = this.config.dbName ?? str.slice(str.lastIndexOf('/') + 1).split('?')[0] ?? 'app'; const client = new _mongodb.MongoClient(str); const db = client.db(dbName); return this._mongoCollection = db.collection(this._name); }, insertSeed() { console.warn(`respond: db.${this._name}.insertSeed is intended for development use only`); }, get super() { if (this._super) return this._super; const proto = Object.getPrototypeOf(this); return this._super = new Proxy({}, { get: (_, k) => proto[k].bind(this) }); }, clone(doc) { const model = Object.defineProperties(Object.create(this.parent), Object.getOwnPropertyDescriptors(this)); return doc ? Object.assign(model, doc) : model; }, createQuerySelector: _createQuerySelector.default, createQueryKey: _createQueryKey.default, ..._safeMethods.default };