respond-framework
Version:
create as fast you think
546 lines (541 loc) • 15.3 kB
JavaScript
"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
};