getData.ts 6.5 KB

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