getData.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import { PipelineStage, Schema, SchemaOptions } from "mongoose";
  2. import JobContext from "@/JobContext";
  3. export enum FilterType {
  4. REGEX = "regex",
  5. CONTAINS = "contains",
  6. EXACT = "exact",
  7. DATETIME_BEFORE = "datetimeBefore",
  8. DATETIME_AFTER = "datetimeAfter",
  9. NUMBER_LESSER_EQUAL = "numberLesserEqual",
  10. NUMBER_LESSER = "numberLesser",
  11. NUMBER_GREATER = "numberGreater",
  12. NUMBER_GREATER_EQUAL = "numberGreaterEqual",
  13. NUMBER_EQUAL = "numberEquals",
  14. BOOLEAN = "boolean",
  15. SPECIAL = "special"
  16. }
  17. export interface GetDataSchemaOptions extends SchemaOptions {
  18. getData?: {
  19. blacklistedProperties?: string[];
  20. specialProperties?: Record<string, PipelineStage[]>;
  21. specialQueries?: Record<
  22. string,
  23. (query: Record<string, any>) => Record<string, any>
  24. >;
  25. specialFilters?: Record<string, (...args: any[]) => PipelineStage[]>;
  26. };
  27. }
  28. export interface GetData {
  29. getData(payload: {
  30. page: number;
  31. pageSize: number;
  32. properties: string[];
  33. sort: Record<string, "ascending" | "descending">;
  34. queries: {
  35. data: any;
  36. filter: {
  37. property: string;
  38. };
  39. filterType: FilterType;
  40. }[];
  41. operator: "and" | "or" | "nor";
  42. }): Promise<{
  43. data: any[];
  44. count: number;
  45. }>;
  46. }
  47. export default function getDataPlugin(schema: Schema) {
  48. schema.static(
  49. "getData",
  50. async function getData(
  51. payload: Parameters<GetData["getData"]>[0]
  52. ): ReturnType<GetData["getData"]> {
  53. const { page, pageSize, properties, sort, queries, operator } =
  54. payload;
  55. const {
  56. blacklistedProperties,
  57. specialFilters,
  58. specialProperties,
  59. specialQueries
  60. } = schema.options?.getData ?? {};
  61. const pipeline: PipelineStage[] = [];
  62. // If a query filter property or sort property is blacklisted, throw error
  63. if (Array.isArray(blacklistedProperties)) {
  64. if (
  65. queries.some(query =>
  66. blacklistedProperties.some(blacklistedProperty =>
  67. blacklistedProperty.startsWith(
  68. query.filter.property
  69. )
  70. )
  71. )
  72. )
  73. throw new Error(
  74. "Unable to filter by blacklisted property."
  75. );
  76. if (
  77. Object.keys(sort).some(property =>
  78. blacklistedProperties.some(blacklistedProperty =>
  79. blacklistedProperty.startsWith(property)
  80. )
  81. )
  82. )
  83. throw new Error("Unable to sort by blacklisted property.");
  84. }
  85. // If a filter or property exists for a special property, add some custom pipeline steps
  86. if (typeof specialProperties === "object")
  87. Object.entries(specialProperties).forEach(
  88. ([specialProperty, pipelineSteps]) => {
  89. // Check if a filter with the special property exists
  90. const filterExists =
  91. queries
  92. .map(query => query.filter.property)
  93. .indexOf(specialProperty) !== -1;
  94. // Check if a property with the special property exists
  95. const propertyExists =
  96. properties.indexOf(specialProperty) !== -1;
  97. // If no such filter or property exists, skip this function
  98. if (!filterExists && !propertyExists) return;
  99. // Add the specified pipeline steps into the pipeline
  100. pipeline.push(...pipelineSteps);
  101. }
  102. );
  103. // Adds the match stage to aggregation pipeline, which is responsible for filtering
  104. const filterQueries = queries.flatMap(query => {
  105. const { data, filter, filterType } = query;
  106. const { property } = filter;
  107. const newQuery: any = {};
  108. switch (filterType) {
  109. case FilterType.REGEX:
  110. newQuery[property] = new RegExp(
  111. `${data.slice(1, data.length - 1)}`,
  112. "i"
  113. );
  114. break;
  115. case FilterType.CONTAINS:
  116. newQuery[property] = new RegExp(
  117. `${data.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
  118. "i"
  119. );
  120. break;
  121. case FilterType.EXACT:
  122. newQuery[property] = data.toString();
  123. break;
  124. case FilterType.DATETIME_BEFORE:
  125. newQuery[property] = { $lte: new Date(data) };
  126. break;
  127. case FilterType.DATETIME_AFTER:
  128. newQuery[property] = { $gte: new Date(data) };
  129. break;
  130. case FilterType.NUMBER_LESSER_EQUAL:
  131. newQuery[property] = { $lte: Number(data) };
  132. break;
  133. case FilterType.NUMBER_LESSER:
  134. newQuery[property] = { $lt: Number(data) };
  135. break;
  136. case FilterType.NUMBER_GREATER:
  137. newQuery[property] = { $gt: Number(data) };
  138. break;
  139. case FilterType.NUMBER_GREATER_EQUAL:
  140. newQuery[property] = { $gte: Number(data) };
  141. break;
  142. case FilterType.NUMBER_EQUAL:
  143. newQuery[property] = { $eq: Number(data) };
  144. break;
  145. case FilterType.BOOLEAN:
  146. newQuery[property] = { $eq: !!data };
  147. break;
  148. case FilterType.SPECIAL:
  149. if (
  150. typeof specialFilters === "object" &&
  151. typeof specialFilters[filter.property] ===
  152. "function"
  153. ) {
  154. pipeline.push(
  155. ...specialFilters[filter.property](data)
  156. );
  157. newQuery[property] = { $eq: true };
  158. }
  159. break;
  160. default:
  161. throw new Error(`Invalid filter type for "${filter}"`);
  162. }
  163. if (
  164. typeof specialQueries === "object" &&
  165. typeof specialQueries[filter.property] === "function"
  166. ) {
  167. return specialQueries[filter.property](newQuery);
  168. }
  169. return newQuery;
  170. });
  171. const filterQuery: any = {};
  172. if (filterQueries.length > 0)
  173. filterQuery[`$${operator}`] = filterQueries;
  174. pipeline.push({ $match: filterQuery });
  175. // Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
  176. if (Object.keys(sort).length > 0)
  177. pipeline.push({
  178. $sort: Object.fromEntries(
  179. Object.entries(sort).map(([property, direction]) => [
  180. property,
  181. direction === "ascending" ? 1 : -1
  182. ])
  183. )
  184. });
  185. // Adds first project stage to aggregation pipeline, responsible for including only the requested properties
  186. pipeline.push({
  187. $project: Object.fromEntries(
  188. properties.map(property => [property, 1])
  189. )
  190. });
  191. // Adds second project stage to aggregation pipeline, responsible for excluding some specific properties
  192. if (
  193. Array.isArray(blacklistedProperties) &&
  194. blacklistedProperties.length > 0
  195. )
  196. pipeline.push({
  197. $project: Object.fromEntries(
  198. blacklistedProperties.map(property => [property, 0])
  199. )
  200. });
  201. // Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
  202. pipeline.push({
  203. $facet: {
  204. count: [{ $count: "count" }],
  205. documents: [
  206. { $skip: pageSize * (page - 1) },
  207. { $limit: pageSize }
  208. ]
  209. }
  210. });
  211. // Executes the aggregation pipeline
  212. const [result] = await this.aggregate(pipeline).exec();
  213. if (result.count.length === 0) return { data: [], count: 0 };
  214. const { documents: data } = result;
  215. const { count } = result.count[0];
  216. return { data, count };
  217. }
  218. );
  219. schema.set("jobConfig", {
  220. async getData(
  221. context: JobContext,
  222. payload: Parameters<GetData["getData"]>[0]
  223. ) {
  224. return this.getData(payload);
  225. },
  226. ...(schema.get("jobConfig") ?? {})
  227. });
  228. }