@topgroup/diginext
Version:
A BUILD SERVER & CLI to deploy apps to any Kubernetes clusters.
463 lines (462 loc) • 24.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseService = exports.DEFAULT_PAGE_SIZE = void 0;
/* eslint-disable prettier/prettier */
const index_1 = require("diginext-utils/dist/string/index");
const random_1 = require("diginext-utils/dist/string/random");
const log_1 = require("diginext-utils/dist/xconsole/log");
const lodash_1 = require("lodash");
const mongoose_1 = require("mongoose");
const entities_1 = require("../entities");
const mongodb_1 = require("../plugins/mongodb");
const parse_request_filter_1 = require("../plugins/parse-request-filter");
const slug_1 = require("../plugins/slug");
const traverse_1 = require("../plugins/traverse");
/**
* ![DANGEROUS]
* This pass phrase is ONLY being used to empty a database,
* and should not being used for production evironment.
*/
const EMPTY_PASS_PHRASE = "nguyhiemvcl";
exports.DEFAULT_PAGE_SIZE = 100;
class BaseService {
constructor(schema, ownership) {
const collection = schema.get("collection");
this.model = (0, mongoose_1.model)(collection, schema, collection);
this.ownership = ownership;
this.user = ownership === null || ownership === void 0 ? void 0 : ownership.owner;
this.workspace = ownership === null || ownership === void 0 ? void 0 : ownership.workspace;
}
async getActiveWorkspace(user) {
let workspace = user.activeWorkspace._id ? user.activeWorkspace : undefined;
console.log("getActiveWorkspace");
if (!workspace && mongodb_1.MongoDB.isValidObjectId(user.activeWorkspace)) {
const wsModel = (0, mongoose_1.model)("workspaces", entities_1.workspaceSchema, "workspaces");
workspace = await wsModel.findOne({ _id: user.activeWorkspace });
}
return workspace;
}
async getActiveRole(user) {
console.log("getActiveRole");
let role = user.activeRole._id ? user.activeRole : undefined;
if (!role && mongodb_1.MongoDB.isValidObjectId(user.activeRole)) {
const Model = (0, mongoose_1.model)("roles", entities_1.roleSchema, "roles");
role = await Model.findOne({ _id: user.activeRole });
}
return role;
}
async count(filter, options = {}) {
const parsedFilter = filter;
parsedFilter.$or = [{ deletedAt: null }, { deletedAt: { $exists: false } }];
if (options.isDebugging)
console.log(`BaseService > COUNT "${this.model.collection.name}" collection > parsedFilter :>>`, parsedFilter);
const total = await this.model.countDocuments(parsedFilter).exec();
if (options.isDebugging)
console.log(`BaseService > COUNT "${this.model.collection.name}" collection > total :>>`, total);
return total;
}
async create(data, options = {}) {
var _a, _b, _c, _d, _e;
try {
// generate slug (if needed)
const scope = this;
const slugRange = "zxcvbnmasdfghjklqwertyuiop1234567890";
async function generateUniqueSlug(input, attempt = 1) {
let slug = (0, slug_1.makeSlug)(input, { delimiter: "" });
let count = await scope.model.countDocuments({ slug }).exec();
if (count > 0)
slug = slug + "-" + (0, random_1.randomStringByLength)(attempt, slugRange).toLowerCase();
// check unique again
count = await scope.model.countDocuments({ slug }).exec();
if (count > 0)
return generateUniqueSlug(input, attempt + 1);
return slug;
}
if (data.slug) {
let count = await scope.count({ slug: data.slug });
if (count > 0)
data.slug = await generateUniqueSlug(data.slug, 1);
}
else {
data.slug = await generateUniqueSlug(data.name || "item", 1);
}
// generate metadata (for searching)
data.metadata = {};
const metadataExcludes = [
"_id",
"password",
"slug",
"token",
"access_token",
"secret",
"kubeConfig",
"serviceAccount",
"apiAccessToken",
"metadata",
"dx_key",
];
for (const [key, value] of Object.entries(data)) {
if (!metadataExcludes.includes(key) && !(0, mongodb_1.isValidObjectId)(value) && value)
data.metadata[key] = (0, index_1.clearUnicodeCharacters)(value.toString());
}
// assign item ownership:
if ((_a = this.req) === null || _a === void 0 ? void 0 : _a.user) {
const { user } = this.req;
const userId = user === null || user === void 0 ? void 0 : user._id;
data.owner = userId;
data.ownerSlug = user === null || user === void 0 ? void 0 : user.slug;
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > activeWorkspace :>> `, user.activeWorkspace);
if (this.model.collection.name !== "workspaces" && user.activeWorkspace) {
const workspace = await this.getActiveWorkspace(user);
if (workspace) {
data.workspace = workspace._id;
data.workspaceSlug = workspace.slug;
}
}
}
if (this.ownership) {
data.owner = (_b = this.ownership.owner) === null || _b === void 0 ? void 0 : _b._id;
data.ownerSlug = (_c = this.ownership.owner) === null || _c === void 0 ? void 0 : _c.slug;
if (this.model.collection.name !== "workspaces") {
data.workspace = (_d = this.ownership.workspace) === null || _d === void 0 ? void 0 : _d._id;
data.workspaceSlug = (_e = this.ownership.workspace) === null || _e === void 0 ? void 0 : _e.slug;
}
}
/**
* Preprocess data before create:
* - Convert all valid "ObjectId" string to ObjectId()
* - Convert "undefined" or "null" to null
*/
data = (0, lodash_1.cloneDeepWith)(data, function (val) {
if ((0, mongodb_1.isValidObjectId)(val))
return mongodb_1.MongoDB.toObjectId(val);
if (val === "undefined" || val === "null")
return null;
});
// set created/updated date:
data.createdAt = data.updatedAt = new Date();
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > create > data :>> `, data);
const createdDoc = new this.model(data);
let newItem = await createdDoc.save();
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > create > newItem :>> `, newItem);
// strip unneccessary fields
delete newItem.__v;
newItem.id = newItem._id;
// convert all {ObjectId} to {string}:
return (0, traverse_1.replaceObjectIdsToStrings)(newItem);
}
catch (e) {
(0, log_1.logError)(`[BASE_SERVICE] "${this.model.collection.name}" > Unable to create:`, e.stack);
return;
}
}
async find(filter = {}, options = {}, pagination) {
// if (options.isDebugging) console.log(`BaseService > "${this.model.collection.name}" > find :>> filter:`, filter);
// where
let _filter = (0, parse_request_filter_1.parseRequestFilter)(filter);
const where = { ..._filter };
if (!(options === null || options === void 0 ? void 0 : options.deleted))
where.deletedAt = { $exists: false };
if (options.isDebugging) {
console.log(`BaseService > "${this.model.collection.name}" > find > where :>>`);
console.dir(JSON.parse(JSON.stringify(where)), { depth: 10 });
}
const pipelines = [
{
$match: where,
},
];
// populate
if ((options === null || options === void 0 ? void 0 : options.populate) && (options === null || options === void 0 ? void 0 : options.populate.length) > 0) {
options === null || options === void 0 ? void 0 : options.populate.forEach((field) => {
var _a;
const collectionPath = this.model.schema.paths[field];
if (!collectionPath)
return;
const lookupCollection = (_a = collectionPath.options) === null || _a === void 0 ? void 0 : _a.ref;
if (!lookupCollection)
return;
const isPopulatedFieldArray = Array.isArray(collectionPath.options.type);
// use $lookup to find relation field
pipelines.push({
$lookup: {
from: lookupCollection,
localField: field,
foreignField: "_id",
as: field,
},
});
// if there are many results, return an array, if there are only 1 result, return an object
pipelines.push({
$addFields: {
[field]: {
$cond: isPopulatedFieldArray
? [{ $isArray: `$${field}` }, `$${field}`, { $ifNull: [`$${field}`, null] }]
: {
if: {
$and: [{ $isArray: `$${field}` }, { $eq: [{ $size: `$${field}` }, 1] }],
},
then: { $arrayElemAt: [`$${field}`, 0] },
else: {
$cond: {
if: {
$and: [{ $isArray: `$${field}` }, { $ne: [{ $size: `$${field}` }, 1] }],
},
then: `$${field}`,
else: null,
},
},
},
},
},
});
});
}
// sort
if (options === null || options === void 0 ? void 0 : options.order) {
pipelines.push({ $sort: options === null || options === void 0 ? void 0 : options.order });
}
// select
if ((options === null || options === void 0 ? void 0 : options.select) && options.select.length > 0) {
const $project = {};
options.select.forEach((field) => {
let shouldInclude = 1;
if (field.startsWith("-")) {
field = field.substring(1);
shouldInclude = 0;
}
$project[field] = shouldInclude;
});
pipelines.push({ $project });
}
// exclude metadata on query result
pipelines.push({ $project: { metadata: 0 } });
// skip & limit (take)
if (options === null || options === void 0 ? void 0 : options.skip)
pipelines.push({ $skip: options.skip });
if (options === null || options === void 0 ? void 0 : options.limit)
pipelines.push({ $limit: options.limit });
let [results, totalItems] = await Promise.all([this.model.aggregate(pipelines).exec(), this.model.countDocuments(where).exec()]);
// console.log(`"${this.model.collection.name}" > results >>`, results);
if (pagination && this.req && this.req.get) {
if (typeof pagination.page_size === "undefined")
pagination.page_size = exports.DEFAULT_PAGE_SIZE;
pagination.total_items = totalItems || results.length;
pagination.total_pages = pagination.page_size ? Math.ceil(totalItems / pagination.page_size) : 1;
const prevPage = pagination.current_page - 1 <= 0 ? 1 : pagination.current_page - 1;
const nextPage = pagination.current_page + 1 > pagination.total_pages && pagination.total_pages != 0
? pagination.total_pages
: pagination.current_page + 1;
pagination.prev_page =
pagination.current_page != prevPage
? `${this.req.protocol}://${this.req.get("host")}${this.req.baseUrl}${this.req.path}` +
"?" +
new URLSearchParams({ ...this.req.query, page: prevPage.toString(), size: pagination.page_size.toString() }).toString()
: null;
pagination.next_page =
pagination.current_page != nextPage
? `${this.req.protocol}://${this.req.get("host")}${this.req.baseUrl}${this.req.path}` +
"?" +
new URLSearchParams({ ...this.req.query, page: nextPage.toString(), size: pagination.page_size.toString() }).toString()
: null;
}
// convert all {ObjectId} to {string}:
results = (0, traverse_1.replaceObjectIdsToStrings)(results.map((item) => {
delete item.__v;
item.id = item._id;
return item;
}));
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > find > json results >>`, results);
// console.log("isArray(results) :>> ", isArray(results));
return results;
}
async findOne(filter, options = {}) {
const [result] = await this.find(filter, { ...options, limit: 1 });
// if (!result) throw new Error("No data found.");
return result;
}
/**
* Looking for unique "field" path of the documents in a collection
* @param path - Document path (field) to be groupped
*/
async distinct(path, filter = {}, options = {}, pagination) {
// where
let _filter = (0, parse_request_filter_1.parseRequestFilter)(filter);
const where = { ..._filter };
if (!(options === null || options === void 0 ? void 0 : options.deleted))
where.deletedAt = { $exists: false };
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > find > where :>>`, where);
const pipelines = [
{
$match: where,
},
];
// populate
if ((options === null || options === void 0 ? void 0 : options.populate) && (options === null || options === void 0 ? void 0 : options.populate.length) > 0) {
options === null || options === void 0 ? void 0 : options.populate.forEach((collection) => {
const lookupCollection = this.model.schema.paths[collection].options.ref;
const isPopulatedFieldArray = Array.isArray(this.model.schema.paths[collection].options.type);
// use $lookup to find relation field
pipelines.push({
$lookup: {
from: lookupCollection,
localField: collection,
foreignField: "_id",
as: collection,
},
});
// if there are many results, return an array, if there are only 1 result, return an object
pipelines.push({
$addFields: {
[collection]: {
$cond: isPopulatedFieldArray
? [{ $isArray: `$${collection}` }, `$${collection}`, { $ifNull: [`$${collection}`, null] }]
: {
if: {
$and: [{ $isArray: `$${collection}` }, { $eq: [{ $size: `$${collection}` }, 1] }],
},
then: { $arrayElemAt: [`$${collection}`, 0] },
else: {
$cond: {
if: {
$and: [{ $isArray: `$${collection}` }, { $ne: [{ $size: `$${collection}` }, 1] }],
},
then: `$${collection}`,
else: null,
},
},
},
},
},
});
});
}
// sort
if (options === null || options === void 0 ? void 0 : options.order) {
pipelines.push({ $sort: options === null || options === void 0 ? void 0 : options.order });
}
// distinct
pipelines.push({ $project: { [`${path}`]: { $toString: `$${path}` } } });
pipelines.push({ $group: { _id: `$${path}` } });
pipelines.push({ $project: { _id: 0, [`${path}`]: `$_id` } });
// skip & limit (take)
if (options === null || options === void 0 ? void 0 : options.skip)
pipelines.push({ $skip: options.skip });
if (options === null || options === void 0 ? void 0 : options.limit)
pipelines.push({ $limit: options.limit });
let [results, totalItems] = await Promise.all([this.model.aggregate(pipelines).exec(), this.model.countDocuments(where).exec()]);
// console.log(`"${this.model.collection.name}" > results >>`, results);
if (pagination && this.req) {
pagination.total_items = totalItems || results.length;
pagination.total_pages = pagination.page_size ? Math.ceil(totalItems / pagination.page_size) : 1;
const prevPage = pagination.current_page - 1 <= 0 ? 1 : pagination.current_page - 1;
const nextPage = pagination.current_page + 1 > pagination.total_pages && pagination.total_pages != 0
? pagination.total_pages
: pagination.current_page + 1;
pagination.prev_page =
pagination.current_page != prevPage
? `${this.req.protocol}://${this.req.get("host")}${this.req.baseUrl}${this.req.path}` +
"?" +
new URLSearchParams({ ...this.req.query, page: prevPage.toString(), size: pagination.page_size.toString() }).toString()
: null;
pagination.next_page =
pagination.current_page != nextPage
? `${this.req.protocol}://${this.req.get("host")}${this.req.baseUrl}${this.req.path}` +
"?" +
new URLSearchParams({ ...this.req.query, page: nextPage.toString(), size: pagination.page_size.toString() }).toString()
: null;
}
// convert all {ObjectId} to {string}:
results = (0, traverse_1.replaceObjectIdsToStrings)(results.map((item) => {
delete item.__v;
item.id = item._id;
return item;
}));
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > distinct > json results >>`, results);
// console.log("isArray(results) :>> ", isArray(results));
return results;
}
async update(filter, data, options = {}) {
var _a, _b, _c, _d;
const updateFilter = { ...filter };
if (!(options === null || options === void 0 ? void 0 : options.deleted))
updateFilter.$or = [{ deletedAt: null }, { deletedAt: { $exists: false } }];
/**
* Preprocess data before update:
* - Convert all valid "ObjectId" string to ObjectId()
* - Convert "undefined" or "null" to null
*/
const convertedData = (0, lodash_1.cloneDeepWith)(data, function (val) {
if ((0, mongodb_1.isValidObjectId)(val))
return mongodb_1.MongoDB.toObjectId(val);
if (val === "undefined" || val === "null")
return null;
});
// set updated date
if (convertedData.$set) {
convertedData.$set.updatedAt = new Date();
if ((_a = this.ownership) === null || _a === void 0 ? void 0 : _a.owner)
convertedData.$set.updatedBy = (_b = this.ownership) === null || _b === void 0 ? void 0 : _b.owner._id;
}
else {
convertedData.updatedAt = new Date();
if ((_c = this.ownership) === null || _c === void 0 ? void 0 : _c.owner)
convertedData.updatedBy = (_d = this.ownership) === null || _d === void 0 ? void 0 : _d.owner._id;
}
// Notes: keep the square brackets in [updateData] -> it's the pipelines for update query
const updateData = (options === null || options === void 0 ? void 0 : options.raw) ? convertedData : [{ $set: convertedData }];
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > update > updateFilter :>> `, updateFilter);
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > update > updateData :>> `, updateData);
const affectedIds = (await this.find(updateFilter, { ...options, select: ["_id"] })).map((item) => item._id);
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > update > affectedIds :>> `, affectedIds);
try {
const updateRes = await this.model.updateMany(updateFilter, updateData).exec();
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > update > updateRes :>> `, updateRes);
// response > results
const affectedItems = await this.find({ _id: { $in: affectedIds } }, options);
if (options === null || options === void 0 ? void 0 : options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > update > affectedItems :>> `, affectedItems);
return updateRes.modifiedCount > 0 ? affectedItems : [];
}
catch (e) {
console.error(`[BASE_SERVICE] "${this.model.collection.name}" > Unable to update data:`, e.stack);
}
}
async updateOne(filter, data, options = {}) {
const results = await this.update(filter, data, { ...options, limit: 1 });
return results && results.length > 0 ? results[0] : undefined;
}
async softDelete(filter, options = {}) {
var _a;
const data = { deletedAt: new Date(), deletedBy: (_a = this.ownership) === null || _a === void 0 ? void 0 : _a.owner._id };
const deletedItems = await this.update(filter, data, { deleted: true });
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > softDelete > deletedItems :>> `, deletedItems, deletedItems.length);
return { ok: deletedItems.length > 0, affected: deletedItems.length };
}
async delete(filter, options = {}) {
const deleteFilter = filter;
const deleteRes = await this.model.deleteMany(deleteFilter).exec();
if (options.isDebugging)
console.log(`BaseService > "${this.model.collection.name}" > delete > deleteRes :>> `, deleteRes);
return { ok: deleteRes.deletedCount > 0, affected: deleteRes.deletedCount };
}
async empty(filter) {
if ((filter === null || filter === void 0 ? void 0 : filter.pass) != EMPTY_PASS_PHRASE)
return { ok: 0, n: 0, error: "[DANGER] You need a password to process this, buddy!" };
const deleteRes = await this.model.deleteMany({}).exec();
return { ...deleteRes, error: null };
}
}
exports.default = BaseService;
exports.BaseService = BaseService;