DataModule.ts 23 KB

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