DataModule.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. import async from "async";
  2. import config from "config";
  3. import mongoose, { Schema, Types as MongooseTypes } from "mongoose";
  4. import hash from "object-hash";
  5. import { createClient, RedisClientType } from "redis";
  6. import JobContext from "src/JobContext";
  7. import BaseModule from "../BaseModule";
  8. import ModuleManager from "../ModuleManager";
  9. import { UniqueMethods } from "../types/Modules";
  10. import { Collections, Types } from "../types/Collections";
  11. export default class DataModule extends BaseModule {
  12. collections?: Collections;
  13. redis?: RedisClientType;
  14. /**
  15. * Data Module
  16. *
  17. * @param moduleManager - Module manager class
  18. */
  19. public constructor(moduleManager: ModuleManager) {
  20. super(moduleManager, "data");
  21. }
  22. /**
  23. * startup - Startup data module
  24. */
  25. public override startup(): Promise<void> {
  26. return new Promise((resolve, reject) => {
  27. async.waterfall(
  28. [
  29. async () => super.startup(),
  30. async () => {
  31. const mongoUrl = config.get<string>("mongo.url");
  32. return mongoose.connect(mongoUrl);
  33. },
  34. async () => this.loadCollections(),
  35. async () => {
  36. if (this.collections) {
  37. await async.each(
  38. Object.values(this.collections),
  39. async collection =>
  40. collection.model.syncIndexes()
  41. );
  42. } else
  43. throw new Error("Collections have not been loaded");
  44. },
  45. async () => {
  46. const { url, password } = config.get<{
  47. url: string;
  48. password: string;
  49. }>("redis");
  50. this.redis = createClient({
  51. url,
  52. password
  53. });
  54. return this.redis.connect();
  55. },
  56. async () => {
  57. if (!this.redis)
  58. throw new Error("Redis connection not established");
  59. return this.redis.sendCommand([
  60. "CONFIG",
  61. "GET",
  62. "notify-keyspace-events"
  63. ]);
  64. },
  65. async (redisConfigResponse: string[]) => {
  66. if (
  67. !(
  68. Array.isArray(redisConfigResponse) &&
  69. redisConfigResponse[1] === "xE"
  70. )
  71. )
  72. throw new Error(
  73. `notify-keyspace-events is NOT configured correctly! It is set to: ${
  74. (Array.isArray(redisConfigResponse) &&
  75. redisConfigResponse[1]) ||
  76. "unknown"
  77. }`
  78. );
  79. },
  80. async () => super.started()
  81. ],
  82. err => {
  83. if (err) reject(err);
  84. else resolve();
  85. }
  86. );
  87. });
  88. }
  89. /**
  90. * shutdown - Shutdown data module
  91. */
  92. public override shutdown(): Promise<void> {
  93. return new Promise(resolve => {
  94. super
  95. .shutdown()
  96. .then(async () => {
  97. // TODO: Ensure the following shutdown correctly
  98. if (this.redis) await this.redis.quit();
  99. await mongoose.connection.close(false);
  100. })
  101. .finally(() => resolve());
  102. });
  103. }
  104. /**
  105. *
  106. * @param schema Our own schema format
  107. * @returns A Mongoose-compatible schema format
  108. */
  109. private convertSchemaToMongooseSchema(schema: any) {
  110. // Convert basic types from our own schema types to Mongoose schema types
  111. const typeToMongooseType = (type: Types) => {
  112. switch (type) {
  113. case Types.String:
  114. return String;
  115. case Types.Number:
  116. return Number;
  117. case Types.Date:
  118. return Date;
  119. case Types.Boolean:
  120. return Boolean;
  121. case Types.ObjectId:
  122. return MongooseTypes.ObjectId;
  123. default:
  124. return null;
  125. }
  126. };
  127. const schemaEntries = Object.entries(schema);
  128. const mongooseSchemaEntries = schemaEntries.map(([key, value]) => {
  129. let mongooseEntry = {};
  130. // Handle arrays
  131. if (value.type === Types.Array) {
  132. // Handle schemas in arrays
  133. if (value.item.type === Types.Schema)
  134. mongooseEntry = [
  135. this.convertSchemaToMongooseSchema(value.item.schema)
  136. ];
  137. // We don't support nested arrays
  138. else if (value.item.type === Types.Array)
  139. throw new Error("Nested arrays are not supported.");
  140. // Handle regular types in array
  141. else mongooseEntry.type = [typeToMongooseType(value.item.type)];
  142. }
  143. // Handle schemas
  144. else if (value.type === Types.Schema)
  145. mongooseEntry = this.convertSchemaToMongooseSchema(
  146. value.schema
  147. );
  148. // Handle regular types
  149. else mongooseEntry.type = typeToMongooseType(value.type);
  150. return [key, mongooseEntry];
  151. });
  152. const mongooseSchema = Object.fromEntries(mongooseSchemaEntries);
  153. return mongooseSchema;
  154. }
  155. /**
  156. * loadColllection - Import and load collection schema
  157. *
  158. * @param collectionName - Name of the collection
  159. * @returns Collection
  160. */
  161. private loadCollection<T extends keyof Collections>(
  162. collectionName: T
  163. ): Promise<Collections[T]> {
  164. return new Promise(resolve => {
  165. import(`../collections/${collectionName.toString()}`).then(
  166. ({ schema }: { schema: Collections[T]["schema"] }) => {
  167. // Here we create a Mongoose schema and model based on our schema
  168. const mongooseSchemaDocument =
  169. this.convertSchemaToMongooseSchema(schema.document);
  170. const mongoSchema = new Schema<
  171. Collections[T]["schema"]["document"]
  172. >(mongooseSchemaDocument, {
  173. timestamps: schema.timestamps
  174. });
  175. const model = mongoose.model(
  176. collectionName.toString(),
  177. mongoSchema
  178. );
  179. // @ts-ignore
  180. resolve({
  181. // @ts-ignore
  182. schema,
  183. // @ts-ignore
  184. model
  185. });
  186. }
  187. );
  188. });
  189. }
  190. /**
  191. * loadCollections - Load and initialize all collections
  192. *
  193. * @returns Promise
  194. */
  195. private loadCollections(): Promise<void> {
  196. return new Promise((resolve, reject) => {
  197. const fetchCollections = async () => ({
  198. abc: await this.loadCollection("abc")
  199. });
  200. fetchCollections()
  201. .then(collections => {
  202. this.collections = collections;
  203. resolve();
  204. })
  205. .catch(err => {
  206. reject(new Error(err));
  207. });
  208. });
  209. }
  210. /**
  211. * Returns the projection array/object that is one level deeper based on the property key
  212. *
  213. * @param projection The projection object/array
  214. * @param key The property key
  215. * @returns Array or Object
  216. */
  217. private getDeeperProjection(projection: any, key: string) {
  218. let deeperProjection;
  219. if (Array.isArray(projection))
  220. deeperProjection = projection
  221. .filter(property => property.startsWith(`${key}.`))
  222. .map(property => property.substr(`${key}.`.length));
  223. else if (typeof projection === "object")
  224. deeperProjection =
  225. projection[key] ??
  226. Object.keys(projection).reduce(
  227. (wipProjection, property) =>
  228. property.startsWith(`${key}.`)
  229. ? {
  230. ...wipProjection,
  231. [property.substr(`${key}.`.length)]:
  232. projection[property]
  233. }
  234. : wipProjection,
  235. {}
  236. );
  237. return deeperProjection;
  238. }
  239. /**
  240. * Whether a property is allowed in a projection array/object
  241. *
  242. * @param projection
  243. * @param property
  244. * @returns
  245. */
  246. private allowedByProjection(projection: any, property: string) {
  247. if (Array.isArray(projection))
  248. return projection.indexOf(property) !== -1;
  249. if (typeof projection === "object") return !!projection[property];
  250. return false;
  251. }
  252. /**
  253. * Strip a document object from any unneeded properties, or of any restricted properties
  254. * If a projection is given
  255. *
  256. * @param document The document object
  257. * @param schema The schema object
  258. * @param projection The projection, which can be null
  259. * @returns
  260. */
  261. private async stripDocument(document: any, schema: any, projection: any) {
  262. // TODO add better comments
  263. // TODO add support for nested objects in arrays
  264. // TODO handle projection excluding properties, rather than assume it's only including properties
  265. const unfilteredEntries = Object.entries(document);
  266. const filteredEntries = await async.reduce(
  267. unfilteredEntries,
  268. [],
  269. async (memo, [key, value]) => {
  270. // If the property does not exist in the schema, return the memo
  271. if (!schema[key]) return memo;
  272. // Handle nested object
  273. if (schema[key].type === undefined) {
  274. // If value is null, it can't be an object, so just return its value
  275. if (!value) return [...memo, [key, value]];
  276. // Get the projection for the next layer
  277. const deeperProjection = this.getDeeperProjection(
  278. projection,
  279. key
  280. );
  281. // Generate a stripped document/object for the current key/value
  282. const strippedDocument = await this.stripDocument(
  283. value,
  284. schema[key],
  285. deeperProjection
  286. );
  287. // If the returned stripped document/object has keys, add the current key with that document/object to the memeo
  288. if (Object.keys(strippedDocument).length > 0)
  289. return [...memo, [key, strippedDocument]];
  290. // The current key has no values that should be returned, so just return the memo
  291. return memo;
  292. }
  293. // If we have a projection, check if the current key is allowed by it. If it is, add the key/value to the memo, otherwise just return the memo
  294. if (projection)
  295. return this.allowedByProjection(projection, key)
  296. ? [...memo, [key, value]]
  297. : memo;
  298. // If the property is restricted, return memo
  299. if (schema[key].restricted) return memo;
  300. // The property exists in the schema, is not explicitly allowed, is not restricted, so add it to memo
  301. return [...memo, [key, value]];
  302. }
  303. );
  304. return Object.fromEntries(filteredEntries);
  305. }
  306. /**
  307. * Parse a projection based on the schema and any given projection
  308. * If no projection is given, it will exclude any restricted properties
  309. * If a projection is given, it will exclude restricted properties that are not explicitly allowed in a projection
  310. * It will return a projection used in Mongo, and if any restricted property is explicitly allowed, return that we can't use the cache
  311. *
  312. * @param schema The schema object
  313. * @param projection The project, which can be null
  314. * @returns
  315. */
  316. private async parseFindProjection(projection: any, schema: any) {
  317. // The mongo projection object we're going to build
  318. const mongoProjection = {};
  319. // This will be false if we let Mongo return any restricted properties
  320. let canCache = true;
  321. // TODO add better comments
  322. // TODO add support for nested objects in arrays
  323. const unfilteredEntries = Object.entries(schema);
  324. await async.forEach(unfilteredEntries, async ([key, value]) => {
  325. // If we have a projection set:
  326. if (projection) {
  327. const allowed = this.allowedByProjection(projection, key);
  328. const { restricted } = value;
  329. // If the property is explicitly allowed in the projection, but also restricted, find can't use cache
  330. if (allowed && restricted) {
  331. canCache = false;
  332. }
  333. // If the property is restricted, but not explicitly allowed, make sure to have mongo exclude it. As it's excluded from Mongo, caching isn't an issue for this property
  334. else if (restricted) {
  335. mongoProjection[key] = false;
  336. }
  337. // If the current property is a nested object
  338. else if (value.type === undefined) {
  339. // Get the projection for the next layer
  340. const deeperProjection = this.getDeeperProjection(
  341. projection,
  342. key
  343. );
  344. // Parse projection for the current value, so one level deeper
  345. const parsedProjection = await this.parseFindProjection(
  346. deeperProjection,
  347. value
  348. );
  349. // If the parsed projection mongo projection contains anything, update our own mongo projection
  350. if (
  351. Object.keys(parsedProjection.mongoProjection).length > 0
  352. )
  353. mongoProjection[key] = parsedProjection.mongoProjection;
  354. // If the parsed projection says we can't use the cache, make sure we can't use cache either
  355. canCache = canCache && parsedProjection.canCache;
  356. }
  357. }
  358. // If we have no projection set, and the current property is restricted, exclude the property from mongo, but don't say we can't use the cache
  359. else if (value.restricted) mongoProjection[key] = false;
  360. // If we have no projection set, and the current property is not restricted, and the current property is a nested object
  361. else if (value.type === undefined) {
  362. // Pass the nested schema object recursively into the parseFindProjection function
  363. const parsedProjection = await this.parseFindProjection(
  364. null,
  365. value
  366. );
  367. // If the returned mongo projection includes anything special, include it in the mongo projection we're returning
  368. if (Object.keys(parsedProjection.mongoProjection).length > 0)
  369. mongoProjection[key] = parsedProjection.mongoProjection;
  370. // Since we're not passing a projection into parseFindProjection, there's no chance that we can't cache
  371. }
  372. });
  373. return {
  374. canCache,
  375. mongoProjection
  376. };
  377. }
  378. /**
  379. * parseFindFilter - Ensure validity of filter and return a mongo filter ---, or the document itself re-constructed
  380. *
  381. * @param filter - Filter
  382. * @param schema - Schema of collection document
  383. * @param options - Parser options
  384. * @returns Promise returning object with query values cast to schema types
  385. * and whether query includes restricted attributes
  386. */
  387. private async parseFindFilter(
  388. filter: any,
  389. schema: any,
  390. options?: {
  391. operators?: boolean;
  392. }
  393. ): Promise<{
  394. mongoFilter: any;
  395. containsRestrictedProperties: boolean;
  396. canCache: boolean;
  397. }> {
  398. if (!filter || typeof filter !== "object")
  399. throw new Error(
  400. "Invalid filter provided. Filter must be an object."
  401. );
  402. const keys = Object.keys(filter);
  403. if (keys.length === 0)
  404. throw new Error(
  405. "Invalid filter provided. Filter must contain keys."
  406. );
  407. // Whether to parse operators or not
  408. const operators = !(options && options.operators === false);
  409. // The MongoDB filter we're building
  410. const mongoFilter: any = {};
  411. // If the filter references any properties that are restricted, this will be true, so that find knows not to cache the query object
  412. let containsRestrictedProperties = false;
  413. // Whether this filter is cachable or not
  414. let canCache = true;
  415. // Operators at the key level that we support right now
  416. const allowedKeyOperators = ["$or", "$and"];
  417. // Operators at the value level that we support right now
  418. const allowedValueOperators = ["$in"];
  419. // Loop through all key/value properties
  420. await async.each(Object.entries(filter), async ([key, value]) => {
  421. // Key must be 1 character and exist
  422. if (!key || key.length === 0)
  423. throw new Error(
  424. `Invalid filter provided. Key must be at least 1 character.`
  425. );
  426. // Handle key operators, which always start with a $
  427. if (operators && key[0] === "$") {
  428. // Operator isn't found, so throw an error
  429. if (allowedKeyOperators.indexOf(key) === -1)
  430. throw new Error(
  431. `Invalid filter provided. Operator "${key}" is not allowed.`
  432. );
  433. // We currently only support $or and $and, but here we can have different logic for different operators
  434. if (key === "$or" || key === "$and") {
  435. // $or and $and should always be an array, so check if it is
  436. if (!Array.isArray(value) || value.length === 0)
  437. throw new Error(
  438. `Key "${key}" must contain array of queries.`
  439. );
  440. // Add the operator to the mongo filter object as an empty array
  441. mongoFilter[key] = [];
  442. // Run parseFindQuery again for child objects and add them to the mongo query operator array
  443. await async.each(value, async _value => {
  444. const {
  445. mongoFilter: _mongoFilter,
  446. containsRestrictedProperties:
  447. _containsRestrictedProperties
  448. } = await this.parseFindFilter(_value, schema, options);
  449. // Actually add the returned filter object to the mongo query we're building
  450. mongoFilter[key].push(_mongoFilter);
  451. if (_containsRestrictedProperties)
  452. containsRestrictedProperties = true;
  453. });
  454. } else
  455. throw new Error(
  456. `Unhandled operator "${key}", this should never happen!`
  457. );
  458. } else {
  459. // Here we handle any normal keys in the query object
  460. // If the key doesn't exist in the schema, throw an error
  461. if (!Object.hasOwn(schema, key))
  462. throw new Error(
  463. `Key "${key} does not exist in the schema."`
  464. );
  465. // If the key in the schema is marked as restricted, containsRestrictedProperties will be true
  466. if (schema[key].restricted) containsRestrictedProperties = true;
  467. // Type will be undefined if it's a nested object
  468. if (schema[key].type === undefined) {
  469. // Run parseFindFilter on the nested schema object
  470. const {
  471. mongoFilter: _mongoFilter,
  472. containsRestrictedProperties:
  473. _containsRestrictedProperties
  474. } = await this.parseFindFilter(value, schema[key], options);
  475. mongoFilter[key] = _mongoFilter;
  476. if (_containsRestrictedProperties)
  477. containsRestrictedProperties = true;
  478. } else if (
  479. operators &&
  480. typeof value === "object" &&
  481. value &&
  482. Object.keys(value).length === 1 &&
  483. Object.keys(value)[0] &&
  484. Object.keys(value)[0][0] === "$"
  485. ) {
  486. // This entire if statement is for handling value operators
  487. const operator = Object.keys(value)[0];
  488. // Operator isn't found, so throw an error
  489. if (allowedValueOperators.indexOf(operator) === -1)
  490. throw new Error(
  491. `Invalid filter provided. Operator "${key}" is not allowed.`
  492. );
  493. // Handle the $in value operator
  494. if (operator === "$in") {
  495. mongoFilter[key] = {
  496. $in: []
  497. };
  498. if (value.$in.length > 0)
  499. mongoFilter[key].$in = await async.map(
  500. value.$in,
  501. async (_value: any) => {
  502. if (
  503. typeof schema[key].type === "function"
  504. ) {
  505. //
  506. // const Type = schema[key].type;
  507. // const castValue = new Type(_value);
  508. // if (schema[key].validate)
  509. // await schema[key]
  510. // .validate(castValue)
  511. // .catch(err => {
  512. // throw new Error(
  513. // `Invalid value for ${key}, ${err}`
  514. // );
  515. // });
  516. return _value;
  517. }
  518. throw new Error(
  519. `Invalid schema type for ${key}`
  520. );
  521. }
  522. );
  523. } else
  524. throw new Error(
  525. `Unhandled operator "${operator}", this should never happen!`
  526. );
  527. } else if (typeof schema[key].type === "function") {
  528. // Do type checking/casting here
  529. // const Type = schema[key].type;
  530. // // const castValue = new Type(value);
  531. // if (schema[key].validate)
  532. // await schema[key].validate(castValue).catch(err => {
  533. // throw new Error(`Invalid value for ${key}, ${err}`);
  534. // });
  535. mongoFilter[key] = value;
  536. } else throw new Error(`Invalid schema type for ${key}`);
  537. }
  538. });
  539. if (containsRestrictedProperties) canCache = false;
  540. return { mongoFilter, containsRestrictedProperties, canCache };
  541. }
  542. // TODO improve caching
  543. // TODO add support for computed fields
  544. // TODO parse query - validation
  545. // TODO add proper typescript support
  546. // TODO add proper jsdoc
  547. // TODO add support for enum document attributes
  548. // TODO add support for array document attributes
  549. // TODO add support for reference document attributes
  550. // TODO fix 2nd layer of schema
  551. /**
  552. * find - Get one or more document(s) from a single collection
  553. *
  554. * @param payload - Payload
  555. * @returns Returned object
  556. */
  557. public find<CollectionNameType extends keyof Collections>(
  558. context: JobContext,
  559. {
  560. collection, // Collection name
  561. filter, // Similar to MongoDB filter
  562. projection,
  563. limit = 0, // TODO have limit off by default?
  564. page = 1,
  565. useCache = true
  566. }: {
  567. collection: CollectionNameType;
  568. filter: Record<string, any>;
  569. projection?: Record<string, any> | string[];
  570. values?: Record<string, any>;
  571. limit?: number;
  572. page?: number;
  573. useCache?: boolean;
  574. }
  575. ): Promise<any | null> {
  576. return new Promise((resolve, reject) => {
  577. let queryHash: string | null = null;
  578. let cacheable = useCache !== false;
  579. let mongoFilter;
  580. let mongoProjection;
  581. async.waterfall(
  582. [
  583. // Verify whether the collection exists
  584. async () => {
  585. if (!collection)
  586. throw new Error("No collection specified");
  587. if (this.collections && !this.collections[collection])
  588. throw new Error("Collection not found");
  589. },
  590. // Verify whether the query is valid-enough to continue
  591. async () => {
  592. const parsedFilter = await this.parseFindFilter(
  593. filter,
  594. this.collections![collection].schema.document
  595. );
  596. cacheable = cacheable && parsedFilter.canCache;
  597. mongoFilter = parsedFilter.mongoFilter;
  598. },
  599. // Verify whether the query is valid-enough to continue
  600. async () => {
  601. const parsedProjection = await this.parseFindProjection(
  602. projection,
  603. this.collections![collection].schema.document
  604. );
  605. console.log(222, parsedProjection);
  606. cacheable = cacheable && parsedProjection.canCache;
  607. mongoProjection = parsedProjection.mongoProjection;
  608. },
  609. // If we can use cache, get from the cache, and if we get results return those
  610. async () => {
  611. // If we're allowed to cache, and the filter doesn't reference any restricted fields, try to cache the query and its response
  612. if (cacheable) {
  613. // Turn the query object into a sha1 hash that can be used as a Redis key
  614. queryHash = hash(
  615. {
  616. collection,
  617. mongoFilter,
  618. limit,
  619. page
  620. },
  621. {
  622. algorithm: "sha1"
  623. }
  624. );
  625. // Check if the query hash already exists in Redis, and get it if it is
  626. const cachedQuery = await this.redis?.GET(
  627. `query.find.${queryHash}`
  628. );
  629. // Return the mongoFilter along with the cachedDocuments, if any
  630. return {
  631. cachedDocuments: cachedQuery
  632. ? JSON.parse(cachedQuery)
  633. : null
  634. };
  635. }
  636. return { cachedDocuments: null };
  637. },
  638. // If we didn't get documents from the cache, get them from mongo
  639. async ({ cachedDocuments }: any) => {
  640. if (cachedDocuments) {
  641. cacheable = false;
  642. return cachedDocuments;
  643. }
  644. // const getFindValues = async (object: any) => {
  645. // const find: any = {};
  646. // await async.each(
  647. // Object.entries(object),
  648. // async ([key, value]) => {
  649. // if (
  650. // value.type === undefined &&
  651. // Object.keys(value).length > 0
  652. // ) {
  653. // const _find = await getFindValues(
  654. // value
  655. // );
  656. // if (Object.keys(_find).length > 0)
  657. // find[key] = _find;
  658. // } else if (!value.restricted)
  659. // find[key] = true;
  660. // }
  661. // );
  662. // return find;
  663. // };
  664. // const find: any = await getFindValues(
  665. // this.collections![collection].schema.document
  666. // );
  667. // TODO, add mongo projection. Make sure to keep in mind caching with queryHash.
  668. return this.collections?.[collection].model
  669. .find(mongoFilter, mongoProjection)
  670. .limit(limit)
  671. .skip((page - 1) * limit);
  672. },
  673. // Convert documents from Mongoose model to regular objects
  674. async (documents: any[]) =>
  675. async.map(documents, async (document: any) =>
  676. document._doc ? document._doc : document
  677. ),
  678. // Add documents to the cache
  679. async (documents: any[]) => {
  680. // Adds query results to cache but doesnt await
  681. if (cacheable && queryHash) {
  682. this.redis!.SET(
  683. `query.find.${queryHash}`,
  684. JSON.stringify(documents),
  685. {
  686. EX: 60
  687. }
  688. );
  689. }
  690. return documents;
  691. },
  692. // Strips the document of any unneeded properties or properties that are restricted
  693. async (documents: any[]) =>
  694. async.map(documents, async (document: any) =>
  695. this.stripDocument(
  696. document,
  697. this.collections![collection].schema.document,
  698. projection
  699. )
  700. )
  701. ],
  702. (err, documents?: any[]) => {
  703. if (err) reject(err);
  704. else if (!documents || documents!.length === 0)
  705. resolve(limit === 1 ? null : []);
  706. else resolve(limit === 1 ? documents![0] : documents);
  707. }
  708. );
  709. });
  710. }
  711. }
  712. export type DataModuleJobs = {
  713. [Property in keyof UniqueMethods<DataModule>]: {
  714. payload: Parameters<UniqueMethods<DataModule>[Property]>[1];
  715. returns: Awaited<ReturnType<UniqueMethods<DataModule>[Property]>>;
  716. };
  717. };