@terreno/api 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/__tests__/versionCheckPlugin.test.js +53 -3
  2. package/dist/api.arrayOperations.test.js +1 -0
  3. package/dist/api.asyncHandler.test.d.ts +1 -0
  4. package/dist/api.asyncHandler.test.js +236 -0
  5. package/dist/api.d.ts +15 -4
  6. package/dist/api.errors.test.js +1 -0
  7. package/dist/api.hooks.test.js +1 -0
  8. package/dist/api.js +153 -104
  9. package/dist/api.query.test.js +1 -0
  10. package/dist/api.test.js +174 -0
  11. package/dist/auth.d.ts +10 -5
  12. package/dist/auth.js +163 -90
  13. package/dist/auth.test.js +159 -0
  14. package/dist/betterAuthApp.test.js +1 -0
  15. package/dist/betterAuthSetup.d.ts +5 -6
  16. package/dist/betterAuthSetup.js +17 -14
  17. package/dist/betterAuthSetup.test.js +1 -0
  18. package/dist/config.d.ts +48 -0
  19. package/dist/config.js +248 -0
  20. package/dist/config.test.d.ts +1 -0
  21. package/dist/config.test.js +328 -0
  22. package/dist/configuration.test.js +1 -0
  23. package/dist/configurationApp.d.ts +1 -1
  24. package/dist/configurationApp.js +17 -13
  25. package/dist/configurationPlugin.test.js +1 -0
  26. package/dist/consentApp.test.js +1 -0
  27. package/dist/envConfigurationPlugin.d.ts +2 -0
  28. package/dist/envConfigurationPlugin.js +173 -0
  29. package/dist/envConfigurationPlugin.test.d.ts +1 -0
  30. package/dist/envConfigurationPlugin.test.js +322 -0
  31. package/dist/errors.d.ts +18 -7
  32. package/dist/errors.js +106 -10
  33. package/dist/errors.test.js +16 -1
  34. package/dist/example.js +16 -7
  35. package/dist/expressServer.d.ts +10 -9
  36. package/dist/expressServer.js +62 -53
  37. package/dist/expressServer.test.js +53 -2
  38. package/dist/githubAuth.d.ts +2 -1
  39. package/dist/githubAuth.js +41 -26
  40. package/dist/githubAuth.test.js +1 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +4 -0
  43. package/dist/logger.d.ts +1 -1
  44. package/dist/logger.js +42 -20
  45. package/dist/models/versionConfig.d.ts +2 -0
  46. package/dist/models/versionConfig.js +8 -0
  47. package/dist/notifiers/googleChatNotifier.js +14 -16
  48. package/dist/notifiers/googleChatNotifier.test.js +1 -0
  49. package/dist/notifiers/slackNotifier.js +16 -14
  50. package/dist/notifiers/slackNotifier.test.js +41 -3
  51. package/dist/notifiers/zoomNotifier.js +7 -10
  52. package/dist/notifiers/zoomNotifier.test.js +1 -0
  53. package/dist/openApi.d.ts +1 -1
  54. package/dist/openApi.test.js +1 -0
  55. package/dist/openApiBuilder.d.ts +39 -6
  56. package/dist/openApiBuilder.js +1 -31
  57. package/dist/openApiBuilder.test.js +1 -0
  58. package/dist/openApiValidator.js +1 -0
  59. package/dist/openApiValidator.test.js +65 -0
  60. package/dist/permissions.d.ts +4 -4
  61. package/dist/permissions.js +67 -65
  62. package/dist/permissions.middleware.test.js +1 -0
  63. package/dist/permissions.test.js +1 -0
  64. package/dist/plugins.d.ts +5 -5
  65. package/dist/plugins.js +18 -9
  66. package/dist/plugins.test.js +1 -1
  67. package/dist/populate.d.ts +15 -8
  68. package/dist/populate.js +23 -24
  69. package/dist/populate.test.js +1 -0
  70. package/dist/realtime/changeStreamWatcher.d.ts +73 -0
  71. package/dist/realtime/changeStreamWatcher.js +720 -0
  72. package/dist/realtime/index.d.ts +6 -0
  73. package/dist/realtime/index.js +27 -0
  74. package/dist/realtime/queryMatcher.d.ts +14 -0
  75. package/dist/realtime/queryMatcher.js +250 -0
  76. package/dist/realtime/queryStore.d.ts +37 -0
  77. package/dist/realtime/queryStore.js +195 -0
  78. package/dist/realtime/realtime.test.d.ts +10 -0
  79. package/dist/realtime/realtime.test.js +2158 -0
  80. package/dist/realtime/realtimeApp.d.ts +93 -0
  81. package/dist/realtime/realtimeApp.js +560 -0
  82. package/dist/realtime/registry.d.ts +40 -0
  83. package/dist/realtime/registry.js +38 -0
  84. package/dist/realtime/socketUser.d.ts +10 -0
  85. package/dist/realtime/socketUser.js +17 -0
  86. package/dist/realtime/types.d.ts +100 -0
  87. package/dist/realtime/types.js +2 -0
  88. package/dist/requestContext.d.ts +37 -0
  89. package/dist/requestContext.js +344 -0
  90. package/dist/requestContext.test.d.ts +1 -0
  91. package/dist/requestContext.test.js +241 -0
  92. package/dist/terrenoApp.d.ts +8 -0
  93. package/dist/terrenoApp.js +50 -13
  94. package/dist/terrenoApp.test.js +194 -21
  95. package/dist/terrenoPlugin.d.ts +11 -0
  96. package/dist/tests/bunSetup.js +1 -0
  97. package/dist/tests.js +1 -1
  98. package/dist/transformers.d.ts +2 -2
  99. package/dist/transformers.js +5 -3
  100. package/dist/transformers.test.js +90 -0
  101. package/dist/types/consentResponse.d.ts +6 -3
  102. package/dist/versionCheckPlugin.d.ts +2 -0
  103. package/dist/versionCheckPlugin.js +18 -12
  104. package/package.json +4 -2
  105. package/src/__tests__/versionCheckPlugin.test.ts +37 -3
  106. package/src/api.arrayOperations.test.ts +1 -0
  107. package/src/api.asyncHandler.test.ts +177 -0
  108. package/src/api.errors.test.ts +1 -0
  109. package/src/api.hooks.test.ts +1 -0
  110. package/src/api.query.test.ts +1 -0
  111. package/src/api.test.ts +132 -0
  112. package/src/api.ts +199 -84
  113. package/src/auth.test.ts +160 -0
  114. package/src/auth.ts +120 -50
  115. package/src/betterAuthApp.test.ts +1 -0
  116. package/src/betterAuthSetup.test.ts +1 -0
  117. package/src/betterAuthSetup.ts +46 -19
  118. package/src/config.test.ts +255 -0
  119. package/src/config.ts +206 -0
  120. package/src/configuration.test.ts +1 -0
  121. package/src/configurationApp.ts +59 -24
  122. package/src/configurationPlugin.test.ts +1 -0
  123. package/src/consentApp.test.ts +1 -0
  124. package/src/envConfigurationPlugin.test.ts +143 -0
  125. package/src/envConfigurationPlugin.ts +100 -0
  126. package/src/errors.test.ts +19 -1
  127. package/src/errors.ts +94 -20
  128. package/src/example.ts +46 -21
  129. package/src/express.d.ts +18 -1
  130. package/src/expressServer.test.ts +50 -2
  131. package/src/expressServer.ts +80 -50
  132. package/src/githubAuth.test.ts +1 -0
  133. package/src/githubAuth.ts +59 -38
  134. package/src/index.ts +4 -0
  135. package/src/logger.ts +47 -17
  136. package/src/models/versionConfig.ts +13 -2
  137. package/src/notifiers/googleChatNotifier.test.ts +1 -0
  138. package/src/notifiers/googleChatNotifier.ts +7 -9
  139. package/src/notifiers/slackNotifier.test.ts +29 -3
  140. package/src/notifiers/slackNotifier.ts +9 -7
  141. package/src/notifiers/zoomNotifier.test.ts +1 -0
  142. package/src/notifiers/zoomNotifier.ts +8 -11
  143. package/src/openApi.test.ts +1 -0
  144. package/src/openApi.ts +4 -4
  145. package/src/openApiBuilder.test.ts +1 -0
  146. package/src/openApiBuilder.ts +14 -11
  147. package/src/openApiValidator.test.ts +59 -0
  148. package/src/openApiValidator.ts +3 -2
  149. package/src/permissions.middleware.test.ts +1 -0
  150. package/src/permissions.test.ts +1 -0
  151. package/src/permissions.ts +30 -25
  152. package/src/plugins.test.ts +1 -1
  153. package/src/plugins.ts +21 -14
  154. package/src/populate.test.ts +1 -0
  155. package/src/populate.ts +44 -36
  156. package/src/realtime/changeStreamWatcher.ts +568 -0
  157. package/src/realtime/index.ts +34 -0
  158. package/src/realtime/queryMatcher.ts +179 -0
  159. package/src/realtime/queryStore.ts +132 -0
  160. package/src/realtime/realtime.test.ts +1755 -0
  161. package/src/realtime/realtimeApp.ts +478 -0
  162. package/src/realtime/registry.ts +64 -0
  163. package/src/realtime/socketUser.ts +25 -0
  164. package/src/realtime/types.ts +112 -0
  165. package/src/requestContext.test.ts +196 -0
  166. package/src/requestContext.ts +368 -0
  167. package/src/terrenoApp.test.ts +137 -11
  168. package/src/terrenoApp.ts +64 -17
  169. package/src/terrenoPlugin.ts +12 -0
  170. package/src/tests/bunSetup.ts +1 -0
  171. package/src/tests.ts +7 -2
  172. package/src/transformers.test.ts +70 -2
  173. package/src/transformers.ts +15 -7
  174. package/src/types/consentResponse.ts +8 -10
  175. package/src/versionCheckPlugin.ts +15 -7
package/src/openApi.ts CHANGED
@@ -199,7 +199,7 @@ export function listOpenApiMiddleware<T>(
199
199
  // Remove _id from queryFields, we handle that above.
200
200
  ?.filter((field) => field !== "_id")
201
201
  .map((field) => {
202
- const params: {name: string; in: "query"; schema: any}[] = [];
202
+ const params: {name: string; in: "query"; schema: Record<string, unknown>}[] = [];
203
203
 
204
204
  // Check for datetime/number to support gt/gte/lt/lte
205
205
  if (
@@ -462,10 +462,10 @@ export function deleteOpenApiMiddleware<T>(
462
462
  // Useful for endpoints that don't directly map to a model.
463
463
  export function readOpenApiMiddleware<T>(
464
464
  options: Partial<ModelRouterOptions<T>>,
465
- properties: any,
465
+ properties: Record<string, unknown>,
466
466
  required: string[],
467
- queryParameters: any
468
- ): any {
467
+ queryParameters: Array<Record<string, unknown>>
468
+ ): express.RequestHandler {
469
469
  if (!options.openApi?.path) {
470
470
  // Just log this once rather than for each middleware.
471
471
  logger.debug(
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {beforeEach, describe, expect, it} from "bun:test";
2
3
  import type express from "express";
3
4
  import type {Router} from "express";
@@ -29,6 +29,7 @@
29
29
  * router.get("/stats", middleware, statsHandler);
30
30
  * ```
31
31
  */
32
+ import type express from "express";
32
33
  import merge from "lodash/merge";
33
34
 
34
35
  import type {ModelRouterOptions} from "./api";
@@ -111,7 +112,7 @@ export interface OpenApiSchemaProperty {
111
112
  * };
112
113
  * ```
113
114
  */
114
- export type OpenApiSchema = {
115
+ export interface OpenApiSchema {
115
116
  /** The JSON Schema type (typically "object" or "array") */
116
117
  type: string;
117
118
  /** Property definitions for object types */
@@ -122,7 +123,8 @@ export type OpenApiSchema = {
122
123
  items?: OpenApiSchemaProperty;
123
124
  /** Schema for additional properties or boolean to allow/disallow them */
124
125
  additionalProperties?: OpenApiSchemaProperty | boolean;
125
- };
126
+ [key: string]: unknown;
127
+ }
126
128
 
127
129
  /**
128
130
  * Defines a parameter in an OpenAPI operation.
@@ -246,7 +248,7 @@ interface ValidationConfig {
246
248
  */
247
249
  export interface OpenApiBuildResult {
248
250
  /** The OpenAPI documentation middleware */
249
- middleware: any;
251
+ middleware: express.RequestHandler;
250
252
  /** Request body schema if defined */
251
253
  bodySchema?: Record<string, OpenApiSchemaProperty>;
252
254
  /** Query parameter schemas if defined */
@@ -397,7 +399,7 @@ export class OpenApiMiddlewareBuilder {
397
399
  * });
398
400
  * ```
399
401
  */
400
- withRequestBody<T extends Record<string, any>>(
402
+ withRequestBody<T extends Record<string, unknown>>(
401
403
  schema: {
402
404
  [K in keyof T]: OpenApiSchemaProperty;
403
405
  },
@@ -454,7 +456,7 @@ export class OpenApiMiddlewareBuilder {
454
456
  * builder.withResponse(204, "No content");
455
457
  * ```
456
458
  */
457
- withResponse<T extends Record<string, any>>(
459
+ withResponse<T extends Record<string, unknown>>(
458
460
  statusCode: number,
459
461
  schema:
460
462
  | {
@@ -508,7 +510,7 @@ export class OpenApiMiddlewareBuilder {
508
510
  * }, {description: "List of users"});
509
511
  * ```
510
512
  */
511
- withArrayResponse<T extends Record<string, any>>(
513
+ withArrayResponse<T extends Record<string, unknown>>(
512
514
  statusCode: number,
513
515
  itemSchema: {
514
516
  [K in keyof T]: OpenApiSchemaProperty;
@@ -671,10 +673,10 @@ export class OpenApiMiddlewareBuilder {
671
673
  * ```
672
674
  */
673
675
  buildWithSchemas(): OpenApiBuildResult {
674
- const noop = (_a: any, _b: any, next: () => void): void => next();
676
+ const noop: express.RequestHandler = (_a, _b, next) => next();
675
677
 
676
678
  // Build the OpenAPI documentation middleware only (no validation middleware)
677
- let openApiMiddleware: any = noop;
679
+ let openApiMiddleware: express.RequestHandler = noop;
678
680
  if (this.options.openApi?.path) {
679
681
  openApiMiddleware = this.options.openApi.path(
680
682
  merge(
@@ -733,11 +735,12 @@ export class OpenApiMiddlewareBuilder {
733
735
  * router.get("/users/:id", middleware, getUserHandler);
734
736
  * ```
735
737
  */
738
+ // biome-ignore lint/suspicious/noExplicitAny: returns either a single RequestHandler or an array depending on validation config — callers spread or invoke
736
739
  build(): any {
737
- const noop = (_a: any, _b: any, next: () => void): void => next();
740
+ const noop: express.RequestHandler = (_a, _b, next) => next();
738
741
 
739
742
  // Build the OpenAPI documentation middleware
740
- let openApiMiddleware: any = noop;
743
+ let openApiMiddleware: express.RequestHandler = noop;
741
744
  if (this.options.openApi?.path) {
742
745
  openApiMiddleware = this.options.openApi.path(
743
746
  merge(
@@ -768,7 +771,7 @@ export class OpenApiMiddlewareBuilder {
768
771
  }
769
772
 
770
773
  // Build validation middleware
771
- const validators: any[] = [openApiMiddleware];
774
+ const validators: express.RequestHandler[] = [openApiMiddleware];
772
775
 
773
776
  // Add body validation if we have a request body schema
774
777
  if (this.validationConfig.validateBody && this.requestBodySchema) {
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterEach, beforeEach, describe, expect, it} from "bun:test";
2
3
  import type {ErrorObject} from "ajv";
3
4
 
@@ -161,6 +162,64 @@ describe("openApiValidator", () => {
161
162
  });
162
163
  });
163
164
 
165
+ describe("per-route validation: object-form options", () => {
166
+ it("applies object validation options with per-operation control", async () => {
167
+ configureOpenApiValidator({removeAdditional: true});
168
+
169
+ const removedProps: string[] = [];
170
+ const errorsCaught: ErrorObject[][] = [];
171
+
172
+ const freshApp = await setupFreshApp();
173
+ freshApp.use(
174
+ "/required",
175
+ modelRouter(RequiredModel, {
176
+ ...requiredRouterOptions,
177
+ validation: {
178
+ excludeFromCreate: ["about"],
179
+ onAdditionalPropertiesRemoved: (props) => {
180
+ removedProps.push(...props);
181
+ },
182
+ onError: (errors) => {
183
+ errorsCaught.push(errors);
184
+ },
185
+ validateCreate: true,
186
+ validateQuery: true,
187
+ validateUpdate: true,
188
+ },
189
+ })
190
+ );
191
+ const admin = await authAsUser(freshApp, "admin");
192
+
193
+ const res = await admin.post("/required").send({name: "Validated"}).expect(201);
194
+ expect(res.body.data.name).toBe("Validated");
195
+ });
196
+
197
+ it("disables create validation when validateCreate is false", async () => {
198
+ configureOpenApiValidator({removeAdditional: true});
199
+
200
+ const freshApp = await setupFreshApp();
201
+ freshApp.use(
202
+ "/required",
203
+ modelRouter(RequiredModel, {
204
+ ...requiredRouterOptions,
205
+ validation: {
206
+ excludeFromUpdate: ["about"],
207
+ validateCreate: false,
208
+ validateUpdate: true,
209
+ },
210
+ })
211
+ );
212
+ const admin = await authAsUser(freshApp, "admin");
213
+
214
+ // Even with extra field, should pass since create validation is disabled
215
+ const res = await admin
216
+ .post("/required")
217
+ .send({extraField: "ignored", name: "NoCreateValidation"})
218
+ .expect(201);
219
+ expect(res.body.data.name).toBe("NoCreateValidation");
220
+ });
221
+ });
222
+
164
223
  describe("sanitization of non-standard mongoose-to-swagger types", () => {
165
224
  it("validates models with ObjectId and DateOnly fields after sanitization", async () => {
166
225
  configureOpenApiValidator({removeAdditional: true});
@@ -184,6 +184,7 @@ const getAjvInstance = (): Ajv => {
184
184
  useDefaults: true,
185
185
  validateSchema: false,
186
186
  });
187
+ // biome-ignore lint/suspicious/noExplicitAny: ajv-formats has a known type compat issue with AJV instances
187
188
  addFormats(instance as any);
188
189
  ajvCache.set(key, instance);
189
190
  }
@@ -644,7 +645,7 @@ export const createValidator = (
644
645
  return (req: Request, res: Response, next: NextFunction): void => {
645
646
  // Run body validation first
646
647
  if (bodyValidator) {
647
- bodyValidator(req, res, ((err?: any) => {
648
+ bodyValidator(req, res, ((err?: unknown) => {
648
649
  if (err) {
649
650
  next(err);
650
651
  return;
@@ -714,7 +715,7 @@ const m2sOptions = {
714
715
  */
715
716
  export const getSchemaFromModel = <T>(model: Model<T>): Record<string, OpenApiSchemaProperty> => {
716
717
  const modelSwagger = m2s(model, m2sOptions);
717
- fixMixedFields((model as any).schema, modelSwagger.properties);
718
+ fixMixedFields(model.schema, modelSwagger.properties);
718
719
  return modelSwagger.properties as Record<string, OpenApiSchemaProperty>;
719
720
  };
720
721
 
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {describe, expect, it, mock, spyOn} from "bun:test";
2
3
  import * as Sentry from "@sentry/bun";
3
4
  import type express from "express";
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {beforeEach, describe, expect, it} from "bun:test";
2
3
  import type express from "express";
3
4
  import supertest from "supertest";
@@ -1,4 +1,3 @@
1
- // Defaults closed
2
1
  import * as Sentry from "@sentry/bun";
3
2
  import type express from "express";
4
3
  import type {NextFunction} from "express";
@@ -27,7 +26,6 @@ export const OwnerQueryFilter = (user?: User) => {
27
26
  if (user) {
28
27
  return {ownerId: user?.id};
29
28
  }
30
- // Return a null, so we know to return no results.
31
29
  return null;
32
30
  };
33
31
 
@@ -50,7 +48,7 @@ export const Permissions = {
50
48
  }
51
49
  return method === "list" || method === "read";
52
50
  },
53
- IsOwner: (_method: RESTMethod, user?: User, obj?: any) => {
51
+ IsOwner: (_method: RESTMethod, user?: User, obj?: unknown) => {
54
52
  // When checking if we can possibly perform the action, return true.
55
53
  if (!obj) {
56
54
  return true;
@@ -61,10 +59,12 @@ export const Permissions = {
61
59
  if (user?.admin) {
62
60
  return true;
63
61
  }
64
- const ownerId = obj?.ownerId?._id || obj?.ownerId;
65
- return user?.id && ownerId && String(ownerId) === String(user?.id);
62
+ const withOwner = obj as {ownerId?: {_id?: unknown} | unknown};
63
+ const ownerObj = withOwner.ownerId as {_id?: unknown} | undefined;
64
+ const ownerId = ownerObj?._id ?? withOwner.ownerId;
65
+ return Boolean(user?.id && ownerId && String(ownerId) === String(user?.id));
66
66
  },
67
- IsOwnerOrReadOnly: (method: RESTMethod, user?: User, obj?: any) => {
67
+ IsOwnerOrReadOnly: (method: RESTMethod, user?: User, obj?: unknown) => {
68
68
  // When checking if we can possibly perform the action, return true.
69
69
  if (!obj) {
70
70
  return true;
@@ -73,37 +73,37 @@ export const Permissions = {
73
73
  return true;
74
74
  }
75
75
 
76
- if (user?.id && obj?.ownerId && String(obj?.ownerId) === String(user?.id)) {
76
+ const withOwner = obj as {ownerId?: unknown};
77
+ if (user?.id && withOwner.ownerId && String(withOwner.ownerId) === String(user?.id)) {
77
78
  return true;
78
79
  }
79
80
  return method === "list" || method === "read";
80
81
  },
81
82
  };
82
83
 
83
- export async function checkPermissions<T>(
84
+ export const checkPermissions = async <T>(
84
85
  method: RESTMethod,
85
86
  permissions: PermissionMethod<T>[],
86
87
  user?: User,
87
88
  obj?: T
88
- ): Promise<boolean> {
89
+ ): Promise<boolean> => {
89
90
  let anyTrue = false;
90
91
  for (const perm of permissions) {
91
- // May or may not be a promise.
92
92
  if (!(await perm(method, user, obj))) {
93
93
  return false;
94
94
  }
95
95
  anyTrue = true;
96
96
  }
97
97
  return anyTrue;
98
- }
98
+ };
99
99
 
100
100
  // Check the permissions for a given model and method. If the method is a read, update, or delete,
101
101
  // finds the relevant object, checks the permissions, and attaches the object to the request as
102
102
  // req.obj.
103
- export function permissionMiddleware<T>(
103
+ export const permissionMiddleware = <T>(
104
104
  model: Model<T>,
105
105
  options: Pick<ModelRouterOptions<T>, "permissions" | "populatePaths">
106
- ) {
106
+ ) => {
107
107
  return async (req: express.Request, _res: express.Response, next: NextFunction) => {
108
108
  if (req.method === "OPTIONS") {
109
109
  return next();
@@ -146,10 +146,14 @@ export function permissionMiddleware<T>(
146
146
  }
147
147
 
148
148
  const builtQuery = model.findById(req.params.id);
149
- const populatedQuery = addPopulateToQuery(builtQuery as any, options.populatePaths);
150
- let data;
149
+ const populatedQuery = addPopulateToQuery(
150
+ // biome-ignore lint/suspicious/noExplicitAny: Query types vary based on populate paths
151
+ builtQuery as any,
152
+ options.populatePaths
153
+ );
154
+ let data: T | null;
151
155
  try {
152
- data = await populatedQuery.exec();
156
+ data = (await populatedQuery.exec()) as T | null;
153
157
  } catch (error: unknown) {
154
158
  throw new APIError({
155
159
  error: error as Error,
@@ -174,13 +178,14 @@ export function permissionMiddleware<T>(
174
178
  }
175
179
 
176
180
  // Document exists but is hidden
177
- const reason: {[key: string]: string} | null = hiddenDoc.deleted
178
- ? {deleted: "true"}
179
- : hiddenDoc.disabled
180
- ? {disabled: "true"}
181
- : hiddenDoc.archived
182
- ? {archived: "true"}
183
- : null;
181
+ let reason: {[key: string]: string} | null = null;
182
+ if (hiddenDoc.deleted) {
183
+ reason = {deleted: "true"};
184
+ } else if (hiddenDoc.disabled) {
185
+ reason = {disabled: "true"};
186
+ } else if (hiddenDoc.archived) {
187
+ reason = {archived: "true"};
188
+ }
184
189
 
185
190
  // If no reason found, treat as not found
186
191
  if (!reason) {
@@ -207,7 +212,7 @@ export function permissionMiddleware<T>(
207
212
  });
208
213
  }
209
214
 
210
- (req as any).obj = data;
215
+ (req as express.Request & {obj?: T | null}).obj = data;
211
216
 
212
217
  return next();
213
218
  } catch (error) {
@@ -215,4 +220,4 @@ export function permissionMiddleware<T>(
215
220
  return next(error);
216
221
  }
217
222
  };
218
- }
223
+ };
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {beforeEach, describe, expect, it, setSystemTime} from "bun:test";
2
3
  import type express from "express";
3
4
  import {type Document, type Model, model, Schema} from "mongoose";
@@ -58,7 +59,6 @@ const StuffModel = model<Stuff>("Stuff", stuffSchema) as unknown as StuffModelTy
58
59
  describe("baseUserPlugin", () => {
59
60
  it("adds admin and email fields to the schema", () => {
60
61
  const testSchema = new Schema({});
61
- // biome-ignore lint/suspicious/noExplicitAny: test schema
62
62
  baseUserPlugin(testSchema as Schema<any, any, any, any>);
63
63
 
64
64
  const adminPath = testSchema.path("admin");
package/src/plugins.ts CHANGED
@@ -16,6 +16,7 @@ export interface BaseUser {
16
16
  email: string;
17
17
  }
18
18
 
19
+ // biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
19
20
  export const baseUserPlugin = (schema: Schema<any, any, any, any>): void => {
20
21
  schema.add({
21
22
  admin: {default: false, description: "Whether the user has admin privileges", type: Boolean},
@@ -29,6 +30,7 @@ export interface IsDeleted {
29
30
  deleted: boolean;
30
31
  }
31
32
 
33
+ // biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
32
34
  export const isDeletedPlugin = (schema: Schema<any, any, any, any>, defaultValue = false): void => {
33
35
  schema.add({
34
36
  deleted: {
@@ -40,6 +42,7 @@ export const isDeletedPlugin = (schema: Schema<any, any, any, any>, defaultValue
40
42
  type: Boolean,
41
43
  },
42
44
  });
45
+ // biome-ignore lint/suspicious/noExplicitAny: Query<any, any> must be loose to accept arbitrary consumer queries
43
46
  const applyDeleteFilter = (q: Query<any, any>): void => {
44
47
  const query = q.getQuery();
45
48
  if (query && query.deleted === undefined) {
@@ -55,6 +58,7 @@ export const isDeletedPlugin = (schema: Schema<any, any, any, any>, defaultValue
55
58
  };
56
59
 
57
60
  export const isDisabledPlugin = (
61
+ // biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
58
62
  schema: Schema<any, any, any, any>,
59
63
  defaultValue = false
60
64
  ): void => {
@@ -73,6 +77,7 @@ export interface CreatedDeleted {
73
77
  created: {type: Date; required: true};
74
78
  }
75
79
 
80
+ // biome-ignore lint/suspicious/noExplicitAny: Schema generics must be loose to accept arbitrary consumer schemas
76
81
  export const createdUpdatedPlugin = (schema: Schema<any, any, any, any>): void => {
77
82
  schema.add({
78
83
  updated: {description: "When this document was last updated", index: true, type: Date},
@@ -111,7 +116,7 @@ export const firebaseJWTPlugin = (schema: Schema): void => {
111
116
  */
112
117
  export const findOneOrNone = <T>(schema: Schema<T>): void => {
113
118
  schema.statics.findOneOrNone = async function (
114
- query: Record<string, any>,
119
+ query: Record<string, unknown>,
115
120
  errorArgs?: Partial<APIErrorConstructor>
116
121
  ): Promise<(Document & T) | null> {
117
122
  const results = await this.find(query);
@@ -174,7 +179,7 @@ export const findOneOrNoneFor = async <T>(
174
179
  */
175
180
  export const findExactlyOne = <T>(schema: Schema<T>): void => {
176
181
  schema.statics.findExactlyOne = async function (
177
- query: Record<string, any>,
182
+ query: Record<string, unknown>,
178
183
  errorArgs?: Partial<APIErrorConstructor>
179
184
  ): Promise<Document & T> {
180
185
  const results = await this.find(query);
@@ -204,10 +209,12 @@ export const findExactlyOne = <T>(schema: Schema<T>): void => {
204
209
  * match the conditions to prevent ambiguous updates.
205
210
  * @param schema Mongoose Schema
206
211
  */
212
+ // biome-ignore lint/suspicious/noExplicitAny: Schema generics with unknown collide with mongoose's loose this-binding on schema.statics
207
213
  export const upsertPlugin = <T>(schema: Schema<any, any, any, any>): void => {
208
214
  schema.statics.upsert = async function (
209
- conditions: Record<string, any>,
210
- update: Record<string, any>
215
+ this: mongoose.Model<T>,
216
+ conditions: Record<string, unknown>,
217
+ update: Record<string, unknown>
211
218
  ): Promise<T> {
212
219
  // Try to find the document with the given conditions.
213
220
  const docs = await this.find(conditions);
@@ -223,31 +230,31 @@ export const upsertPlugin = <T>(schema: Schema<any, any, any, any>): void => {
223
230
  if (doc) {
224
231
  // If the document exists, update it with the provided update values.
225
232
  Object.assign(doc, update);
226
- return doc.save();
233
+ return (await doc.save()) as unknown as T;
227
234
  }
228
235
  // If the document doesn't exist, create a new one with the combined conditions and update
229
236
  // values.
230
237
  const combinedData = {...conditions, ...update};
231
238
  const newDoc = new this(combinedData);
232
- return newDoc.save();
239
+ return (await newDoc.save()) as unknown as T;
233
240
  };
234
241
  };
235
242
 
236
243
  /** For models with the upsertPlugin, extend this interface to add the upsert static method. */
237
244
  export interface HasUpsert<T> {
238
- upsert(conditions: Record<string, any>, update: Record<string, any>): Promise<T>;
245
+ upsert(conditions: Record<string, unknown>, update: Record<string, unknown>): Promise<T>;
239
246
  }
240
247
 
241
248
  export interface FindOneOrNonePlugin<T> {
242
249
  findOneOrNone(
243
- query: Record<string, any>,
250
+ query: Record<string, unknown>,
244
251
  errorArgs?: Partial<APIErrorConstructor>
245
252
  ): Promise<(Document & T) | null>;
246
253
  }
247
254
 
248
255
  export interface FindExactlyOnePlugin<T> {
249
256
  findExactlyOne(
250
- query: Record<string, any>,
257
+ query: Record<string, unknown>,
251
258
  errorArgs?: Partial<APIErrorConstructor>
252
259
  ): Promise<Document & T>;
253
260
  }
@@ -257,12 +264,12 @@ export class DateOnly extends SchemaType {
257
264
  super(key, options, "DateOnly");
258
265
  }
259
266
 
260
- handleSingle(val) {
267
+ handleSingle(val: unknown) {
261
268
  return this.cast(val);
262
269
  }
263
270
 
264
271
  $conditionalHandlers = {
265
- // noExplicitAny: $conditionalHandlers is not exposed on SchemaType's prototype in Mongoose's public type definitions
272
+ // biome-ignore lint/suspicious/noExplicitAny: $conditionalHandlers is not exposed on SchemaType's prototype in Mongoose's public type definitions
266
273
  ...(SchemaType as any).prototype.$conditionalHandlers,
267
274
  $gt: this.handleSingle,
268
275
  $gte: this.handleSingle,
@@ -272,9 +279,9 @@ export class DateOnly extends SchemaType {
272
279
 
273
280
  // Based on castForQuery in mongoose/lib/schema/date.js
274
281
  // When using $gt, $gte, $lt, $lte, etc, we need to cast the value to a Date
275
- castForQuery($conditional, val, context): Date | undefined {
282
+ castForQuery($conditional: string | undefined, val: unknown, context: unknown): Date | undefined {
276
283
  if ($conditional == null) {
277
- // noExplicitAny: applySetters is an internal Mongoose SchemaType method not in public type definitions
284
+ // biome-ignore lint/suspicious/noExplicitAny: applySetters is an internal Mongoose SchemaType method not in public type definitions
278
285
  return (this as any).applySetters(val, context);
279
286
  }
280
287
 
@@ -334,5 +341,5 @@ export class DateOnly extends SchemaType {
334
341
  }
335
342
 
336
343
  // Register DateOnly with Mongoose's Schema.Types
337
- // noExplicitAny: DateOnly is a custom SchemaType not declared in Mongoose's Schema.Types interface
344
+ // biome-ignore lint/suspicious/noExplicitAny: DateOnly is a custom SchemaType not declared in Mongoose's Schema.Types interface
338
345
  (mongoose.Schema.Types as any).DateOnly = DateOnly;
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {beforeEach, describe, expect, it} from "bun:test";
2
3
  import mongoose, {type Document, type HydratedDocument, Schema} from "mongoose";
3
4