UNPKG

@topgroup/diginext

Version:

A BUILD SERVER & CLI to deploy apps to any Kubernetes clusters.

463 lines (462 loc) 24.3 kB
"use strict"; 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;