@terreno/api 0.3.1 → 0.4.2

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 (42) hide show
  1. package/dist/api.js +9 -8
  2. package/dist/betterAuthSetup.js +1 -1
  3. package/dist/configuration.test.d.ts +1 -0
  4. package/dist/configuration.test.js +699 -0
  5. package/dist/configurationApp.d.ts +91 -0
  6. package/dist/configurationApp.js +407 -0
  7. package/dist/configurationPlugin.d.ts +102 -0
  8. package/dist/configurationPlugin.js +285 -0
  9. package/dist/configurationPlugin.test.d.ts +1 -0
  10. package/dist/configurationPlugin.test.js +509 -0
  11. package/dist/example.js +1 -1
  12. package/dist/expressServer.js +5 -1
  13. package/dist/githubAuth.js +2 -2
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.js +5 -0
  16. package/dist/openApiCompat.d.ts +23 -0
  17. package/dist/openApiCompat.js +198 -0
  18. package/dist/scriptRunner.d.ts +52 -0
  19. package/dist/scriptRunner.js +231 -0
  20. package/dist/secretProviders.d.ts +47 -0
  21. package/dist/secretProviders.js +214 -0
  22. package/dist/terrenoApp.d.ts +25 -0
  23. package/dist/terrenoApp.js +49 -2
  24. package/dist/tests.d.ts +27 -9
  25. package/dist/tests.js +10 -1
  26. package/package.json +13 -13
  27. package/src/api.ts +9 -8
  28. package/src/betterAuthSetup.ts +2 -2
  29. package/src/configuration.test.ts +398 -0
  30. package/src/configurationApp.ts +359 -0
  31. package/src/configurationPlugin.test.ts +299 -0
  32. package/src/configurationPlugin.ts +288 -0
  33. package/src/example.ts +1 -1
  34. package/src/expressServer.ts +6 -1
  35. package/src/githubAuth.ts +4 -4
  36. package/src/index.ts +5 -0
  37. package/src/openApiCompat.ts +147 -0
  38. package/src/permissions.ts +1 -1
  39. package/src/scriptRunner.ts +219 -0
  40. package/src/secretProviders.ts +109 -0
  41. package/src/terrenoApp.ts +44 -2
  42. package/src/tests.ts +12 -1
@@ -0,0 +1,288 @@
1
+ import type {Document, Model, Schema} from "mongoose";
2
+
3
+ import {APIError} from "./errors";
4
+ import {logger} from "./logger";
5
+
6
+ /**
7
+ * Metadata for a secret field discovered by the configuration plugin.
8
+ */
9
+ export interface SecretFieldMeta {
10
+ path: string;
11
+ secretProvider?: string;
12
+ secretName: string;
13
+ }
14
+
15
+ /**
16
+ * Interface for adapters that resolve secret values from external providers.
17
+ */
18
+ export interface SecretProvider {
19
+ name: string;
20
+ getSecret(secretName: string): Promise<string | null>;
21
+ }
22
+
23
+ /**
24
+ * Options passed to configurationPlugin.
25
+ */
26
+ export interface ConfigurationPluginOptions {
27
+ /**
28
+ * Secret provider used when resolveSecrets() is called without an explicit provider.
29
+ * Typically set during app startup so the model can resolve secrets on demand.
30
+ */
31
+ secretProvider?: SecretProvider;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Path type utilities
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * All dot-notation paths for a type T.
40
+ * @example Paths<{a: {b: string}; c: number}> = "a" | "a.b" | "c"
41
+ */
42
+ export type Paths<T extends object> = {
43
+ [K in keyof T & string]: T[K] extends object ? K | `${K}.${Paths<T[K]>}` : K;
44
+ }[keyof T & string];
45
+
46
+ /**
47
+ * The value type at a dot-notation path P within type T.
48
+ * @example PathValue<{a: {b: string}}, "a.b"> = string
49
+ */
50
+ export type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
51
+ ? K extends keyof T
52
+ ? PathValue<NonNullable<T[K]>, Rest>
53
+ : never
54
+ : P extends keyof T
55
+ ? T[P]
56
+ : never;
57
+
58
+ /**
59
+ * Deeply partial version of T, for use in updateConfig.
60
+ */
61
+ export type DeepPartial<T> = {
62
+ [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
63
+ };
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Statics interface
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Static methods added by configurationPlugin to the Mongoose model.
71
+ */
72
+ export interface ConfigurationStatics<T extends object> {
73
+ /** Get the full singleton configuration document. */
74
+ getConfig(): Promise<T & Document>;
75
+ /** Get a specific value by dot-notation key. */
76
+ getConfig<P extends Paths<T>>(key: P): Promise<PathValue<T, P>>;
77
+ /** Update the singleton configuration document (deep merge). */
78
+ updateConfig(updates: DeepPartial<T>): Promise<T & Document>;
79
+ /** Get secret field metadata discovered from the schema. */
80
+ getSecretFields(): SecretFieldMeta[];
81
+ /**
82
+ * Resolve all secret field values from a provider.
83
+ * Uses the provider passed here, or falls back to the one configured in the plugin options.
84
+ * Returns a map of path -> value.
85
+ */
86
+ resolveSecrets(provider?: SecretProvider): Promise<Map<string, string>>;
87
+ }
88
+
89
+ /**
90
+ * Convenience type for a Mongoose model with configurationPlugin applied.
91
+ *
92
+ * Use this when declaring your configuration model to get full type safety:
93
+ * ```typescript
94
+ * export const AppConfig = mongoose.model<AppConfigDocument, ConfigurationModel<AppConfigDocument>>(
95
+ * "AppConfig",
96
+ * appConfigSchema,
97
+ * );
98
+ * // Then call:
99
+ * const name = await AppConfig.getConfig("general.appName"); // typed as string
100
+ * const full = await AppConfig.getConfig(); // typed as AppConfigDocument
101
+ * ```
102
+ */
103
+ export type ConfigurationModel<T extends object> = Model<T> & ConfigurationStatics<T>;
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Plugin
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Mongoose schema plugin that adds singleton configuration behavior.
111
+ *
112
+ * Adds:
113
+ * - Pre-save hook enforcing exactly one document
114
+ * - `getConfig()` static: fetches or creates the singleton (full doc or keyed value)
115
+ * - `updateConfig(updates)` static: patches the singleton
116
+ * - `getSecretFields()` static: returns metadata for fields with `secret: true`
117
+ * - `resolveSecrets(provider?)` static: fetches secret values, using the plugin provider by default
118
+ *
119
+ * Mark fields as secrets using schema path options:
120
+ * ```typescript
121
+ * const configSchema = new Schema({
122
+ * apiKey: {
123
+ * type: String,
124
+ * description: "Third-party API key",
125
+ * secret: true,
126
+ * secretName: "my-api-key",
127
+ * },
128
+ * });
129
+ * configSchema.plugin(configurationPlugin, {secretProvider: new EnvSecretProvider()});
130
+ * ```
131
+ */
132
+ export const configurationPlugin = (schema: Schema, options?: ConfigurationPluginOptions): void => {
133
+ const pluginOptions = options ?? {};
134
+
135
+ // Add a sentinel field with a unique index to enforce singleton at the DB level.
136
+ // All config documents get _singleton: "config", and the unique index prevents duplicates.
137
+ schema.add({
138
+ _singleton: {default: "config", immutable: true, select: false, type: String},
139
+ });
140
+ schema.index({_singleton: 1}, {unique: true});
141
+
142
+ // Enforce singleton: only one document allowed (application-level guard)
143
+ schema.pre("save", async function () {
144
+ if (this.isNew) {
145
+ // Intentional unfiltered findOne — checking if any singleton document exists
146
+ const existing = await (this.constructor as Model<unknown>).findOne({});
147
+ if (existing) {
148
+ throw new APIError({
149
+ status: 409,
150
+ title: "Only one configuration document is allowed. Use updateConfig() instead.",
151
+ });
152
+ }
153
+ }
154
+ });
155
+
156
+ // Prevent hard deletion of the singleton (soft deletes via isDeletedPlugin still work)
157
+ const createHardDeleteError = (): APIError =>
158
+ new APIError({
159
+ status: 400,
160
+ title:
161
+ "Cannot hard-delete the configuration document. Use updateConfig() or soft delete instead.",
162
+ });
163
+
164
+ schema.pre("deleteOne", {document: true, query: true}, () => {
165
+ throw createHardDeleteError();
166
+ });
167
+ schema.pre("deleteMany", () => {
168
+ throw createHardDeleteError();
169
+ });
170
+ schema.pre("findOneAndDelete", () => {
171
+ throw createHardDeleteError();
172
+ });
173
+
174
+ // Static: get the singleton configuration document or a value at a path (race-safe via upsert)
175
+ schema.statics.getConfig = async function (key?: string): Promise<unknown> {
176
+ let config = await this.findOne({});
177
+ if (!config) {
178
+ try {
179
+ // Use `new` + `save` instead of `create({})` so Mongoose initializes
180
+ // nested subdocument defaults (create({}) skips them).
181
+ config = new this();
182
+ await config.save();
183
+ } catch (err: unknown) {
184
+ // If another process created the document between findOne and create,
185
+ // the pre-save hook will throw a 409. Just fetch the existing one.
186
+ if ((err as {status?: number})?.status === 409) {
187
+ config = await this.findOne({});
188
+ } else {
189
+ throw err;
190
+ }
191
+ }
192
+ }
193
+
194
+ if (key === undefined) {
195
+ return config;
196
+ }
197
+
198
+ // Resolve dot-notation key into the document
199
+ const parts = key.split(".");
200
+ let value: unknown = config.toObject();
201
+ for (const part of parts) {
202
+ if (value == null || typeof value !== "object") {
203
+ return undefined;
204
+ }
205
+ value = (value as Record<string, unknown>)[part];
206
+ }
207
+ return value;
208
+ };
209
+
210
+ // Static: update the singleton configuration document (race-safe)
211
+ schema.statics.updateConfig = async function (
212
+ updates: Record<string, unknown>
213
+ ): Promise<unknown> {
214
+ const config = await (this as ConfigurationModel<Record<string, unknown>>).getConfig();
215
+ Object.assign(config, updates);
216
+ await (config as Document).save();
217
+ return config;
218
+ };
219
+
220
+ // Static: discover secret fields from schema options
221
+ schema.statics.getSecretFields = function (): SecretFieldMeta[] {
222
+ const secrets: SecretFieldMeta[] = [];
223
+ const discoverSecrets = (s: Schema, prefix: string) => {
224
+ s.eachPath((pathName, schemaType) => {
225
+ const opts = schemaType.options as Record<string, unknown>;
226
+ if (opts?.secret === true) {
227
+ secrets.push({
228
+ path: prefix ? `${prefix}.${pathName}` : pathName,
229
+ secretName: (opts.secretName as string) ?? pathName,
230
+ secretProvider: opts.secretProvider as string | undefined,
231
+ });
232
+ }
233
+ // Recurse into subschemas
234
+ if ((schemaType as {schema?: Schema}).schema) {
235
+ discoverSecrets(
236
+ (schemaType as {schema: Schema}).schema,
237
+ prefix ? `${prefix}.${pathName}` : pathName
238
+ );
239
+ }
240
+ });
241
+ };
242
+ discoverSecrets(this.schema, "");
243
+ return secrets;
244
+ };
245
+
246
+ // Static: resolve secret values from a provider
247
+ schema.statics.resolveSecrets = async function (
248
+ provider?: SecretProvider
249
+ ): Promise<Map<string, string>> {
250
+ const resolvedProvider = provider ?? pluginOptions.secretProvider;
251
+ if (!resolvedProvider) {
252
+ logger.warn(
253
+ "resolveSecrets called with no provider. Pass a SecretProvider to resolveSecrets() or configurationPlugin options."
254
+ );
255
+ return new Map();
256
+ }
257
+
258
+ const secrets = (this as ConfigurationModel<Record<string, unknown>>).getSecretFields();
259
+ const resolved = new Map<string, string>();
260
+
261
+ const results = await Promise.allSettled(
262
+ secrets.map(async (meta: SecretFieldMeta) => {
263
+ const value = await resolvedProvider.getSecret(meta.secretName);
264
+ if (value !== null) {
265
+ resolved.set(meta.path, value);
266
+ }
267
+ })
268
+ );
269
+
270
+ let failCount = 0;
271
+ for (const result of results) {
272
+ if (result.status === "rejected") {
273
+ failCount++;
274
+ logger.error(`Failed to resolve secret: ${result.reason}`);
275
+ }
276
+ }
277
+
278
+ if (failCount > 0) {
279
+ logger.warn(`${failCount}/${secrets.length} secrets failed to resolve`);
280
+ } else if (secrets.length > 0) {
281
+ logger.info(
282
+ `Resolved ${resolved.size}/${secrets.length} secrets from ${resolvedProvider.name}`
283
+ );
284
+ }
285
+
286
+ return resolved;
287
+ };
288
+ };
package/src/example.ts CHANGED
@@ -54,7 +54,7 @@ const FoodModel = model<Food>("Food", schema);
54
54
  function getBaseServer() {
55
55
  const app = express();
56
56
 
57
- app.all("/*", (req, res, next) => {
57
+ app.use((req, res, next) => {
58
58
  res.header("Access-Control-Allow-Origin", "*");
59
59
  res.header("Access-Control-Allow-Headers", "*");
60
60
  // intercepts OPTIONS method
@@ -15,6 +15,7 @@ import {apiErrorMiddleware, apiUnauthorizedMiddleware} from "./errors";
15
15
  import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
16
16
  import {type LoggingOptions, logger, setupLogging} from "./logger";
17
17
  import {sendToSlack} from "./notifiers";
18
+ import {openApiCompatMiddleware, patchAppUse} from "./openApiCompat";
18
19
  import {openApiEtagMiddleware} from "./openApiEtag";
19
20
 
20
21
  const SLOW_READ_MAX = 200;
@@ -180,6 +181,9 @@ function initializeRoutes(
180
181
  ): express.Application {
181
182
  const app = express();
182
183
 
184
+ // Record mount paths on layers for Express 5 → OpenAPI compat
185
+ patchAppUse(app);
186
+
183
187
  // TODO: Log a warning when we hit the array limit.
184
188
  app.set("query parser", (str: string) => qs.parse(str, {arrayLimit: options.arrayLimit ?? 200}));
185
189
 
@@ -210,7 +214,7 @@ function initializeRoutes(
210
214
  });
211
215
 
212
216
  // Add Sentry scopes for session, transaction, and userId if any are set
213
- app.all("*", (req: any, _res: any, next: any) => {
217
+ app.use((req: any, _res: any, next: any) => {
214
218
  const transactionId = req.header("X-Transaction-ID");
215
219
  const sessionId = req.header("X-Session-ID");
216
220
  if (transactionId) {
@@ -226,6 +230,7 @@ function initializeRoutes(
226
230
  });
227
231
 
228
232
  // Add ETag middleware for OpenAPI JSON endpoint before the openapi middleware
233
+ app.use(openApiCompatMiddleware);
229
234
  app.use(openApiEtagMiddleware);
230
235
 
231
236
  const oapi = openapi({
package/src/githubAuth.ts CHANGED
@@ -79,7 +79,7 @@ export function setupGitHubAuth(
79
79
 
80
80
  passport.use(
81
81
  "github",
82
- new GitHubStrategy(
82
+ new (GitHubStrategy as any)(
83
83
  {
84
84
  callbackURL: githubOptions.callbackURL,
85
85
  clientID: githubOptions.clientId,
@@ -87,8 +87,8 @@ export function setupGitHubAuth(
87
87
  passReqToCallback: true,
88
88
  scope,
89
89
  },
90
- async (
91
- req: express.Request,
90
+ (async (
91
+ req: any,
92
92
  accessToken: string,
93
93
  refreshToken: string,
94
94
  profile: Profile,
@@ -192,7 +192,7 @@ export function setupGitHubAuth(
192
192
  logger.error(`GitHub auth error: ${error}`);
193
193
  return done(error);
194
194
  }
195
- }
195
+ }) as any
196
196
  ) as passport.Strategy
197
197
  );
198
198
  }
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ export * from "./auth";
3
3
  export * from "./betterAuth";
4
4
  export * from "./betterAuthApp";
5
5
  export * from "./betterAuthSetup";
6
+ export * from "./configurationApp";
7
+ export * from "./configurationPlugin";
6
8
  export * from "./errors";
7
9
  export * from "./expressServer";
8
10
  export * from "./githubAuth";
@@ -10,11 +12,14 @@ export * from "./logger";
10
12
  export * from "./middleware";
11
13
  export * from "./notifiers";
12
14
  export * from "./openApiBuilder";
15
+ export * from "./openApiCompat";
13
16
  export * from "./openApiEtag";
14
17
  export * from "./openApiValidator";
15
18
  export * from "./permissions";
16
19
  export * from "./plugins";
17
20
  export * from "./populate";
21
+ export * from "./scriptRunner";
22
+ export * from "./secretProviders";
18
23
  export * from "./terrenoApp";
19
24
  export * from "./terrenoPlugin";
20
25
  export * from "./transformers";
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Patches the Express router stack to add `.regexp` on layers for
3
+ * compatibility with @wesleytodd/openapi, which expects Express 4-style
4
+ * layers with `.regexp.fast_slash`.
5
+ *
6
+ * In Express 5 (router@2.x), layers use `.slash` (boolean) and `.matchers`
7
+ * (array of functions) instead of `.regexp`.
8
+ *
9
+ * @see https://github.com/wesleytodd/express-openapi/issues/70
10
+ */
11
+
12
+ const MOUNT_PATH_KEY = "__openApiMountPath";
13
+
14
+ /**
15
+ * Extract Express 4-style keys from a path string.
16
+ * Parses `:paramName` and `*paramName` segments into `{name, optional}` objects
17
+ * that @wesleytodd/openapi expects.
18
+ */
19
+ const extractKeysFromPath = (path: string): Array<{name: string; optional: boolean}> => {
20
+ const keys: Array<{name: string; optional: boolean}> = [];
21
+ const paramRegex = /[:*](\w+)\??/g;
22
+ let match: RegExpExecArray | null;
23
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
24
+ while ((match = paramRegex.exec(path)) !== null) {
25
+ keys.push({name: match[1], optional: match[0].endsWith("?")});
26
+ }
27
+ return keys;
28
+ };
29
+
30
+ /**
31
+ * Build an Express 4-style regexp from a path string for the openapi parser.
32
+ *
33
+ * For paths without params (e.g., `/food`), produces a simple escaped regexp
34
+ * that the `split()` function in @wesleytodd/openapi can parse directly.
35
+ *
36
+ * For paths with `:params` (e.g., `/food/:id`), replaces each param with the
37
+ * Express 4-style capture group `(?:([^\/]+?))` so that `processComplexMatch()`
38
+ * in the openapi library can map them to `{paramName}` using `layer.keys`.
39
+ */
40
+ const buildRegexpForPath = (pathStr: string, isMount: boolean): RegExp => {
41
+ // Replace :param segments with Express 4-style capture groups, then escape the rest
42
+ const parts = pathStr.split("/").map((segment) => {
43
+ if (segment.startsWith(":")) {
44
+ return "(?:([^\\/]+?))";
45
+ }
46
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ });
48
+ const pattern = parts.join("\\/");
49
+ if (isMount) {
50
+ return new RegExp(`^${pattern}\\/?(?=\\/|$)`);
51
+ }
52
+ return new RegExp(`^${pattern}\\/?$`);
53
+ };
54
+
55
+ const patchRouterStack = (stack: any[]): void => {
56
+ for (const layer of stack) {
57
+ if (layer.regexp !== undefined) {
58
+ continue;
59
+ }
60
+
61
+ // Determine the path string for this layer
62
+ let pathStr: string | undefined;
63
+ const isMount = layer.name === "router" || !!layer.handle?.stack;
64
+
65
+ if (layer.slash) {
66
+ // Express 5 layers use .slash instead of .regexp.fast_slash
67
+ layer.regexp = {fast_slash: true};
68
+ } else if (layer[MOUNT_PATH_KEY]) {
69
+ pathStr = layer[MOUNT_PATH_KEY] as string;
70
+ layer.regexp = buildRegexpForPath(pathStr, isMount);
71
+ } else if (layer.path && typeof layer.path === "string") {
72
+ pathStr = layer.path as string;
73
+ layer.regexp = buildRegexpForPath(pathStr, false);
74
+ } else if (layer.route?.path && typeof layer.route.path === "string") {
75
+ pathStr = layer.route.path as string;
76
+ layer.regexp = buildRegexpForPath(pathStr, false);
77
+ } else {
78
+ layer.regexp = /^\/?$/;
79
+ }
80
+
81
+ // Populate keys in Express 4 format: [{name, optional}]
82
+ // @wesleytodd/openapi reads layer.keys[i].name for path parameters
83
+ if (!layer.keys || (Array.isArray(layer.keys) && layer.keys.length === 0)) {
84
+ if (pathStr) {
85
+ layer.keys = extractKeysFromPath(pathStr);
86
+ } else {
87
+ layer.keys = [];
88
+ }
89
+ } else if (Array.isArray(layer.keys) && typeof layer.keys[0] === "string") {
90
+ // Express 5 stores keys as plain strings after match() — convert to objects
91
+ layer.keys = layer.keys.map((k: string) => ({name: k, optional: false}));
92
+ }
93
+
94
+ // Recursively patch nested stacks
95
+ if (layer.handle?.stack) {
96
+ patchRouterStack(layer.handle.stack);
97
+ }
98
+ if (layer.route?.stack) {
99
+ patchRouterStack(layer.route.stack);
100
+ }
101
+ }
102
+ };
103
+
104
+ /**
105
+ * Wraps an Express app's `use` method to record the mount path on each
106
+ * layer added to the router stack. This runs at setup time so that
107
+ * `patchRouterStack` can read the original path later.
108
+ *
109
+ * Must be called before any routes are registered.
110
+ */
111
+ export const patchAppUse = (app: any): void => {
112
+ const originalUse = app.use.bind(app);
113
+ app.use = function patchedUse(...args: any[]) {
114
+ // Track stack length before the call
115
+ const router = app._router || app.router;
116
+ const stackBefore = router?.stack?.length ?? 0;
117
+
118
+ const result = originalUse(...args);
119
+
120
+ // After use(), check if new layers were added and annotate them
121
+ const routerAfter = app._router || app.router;
122
+ if (routerAfter?.stack) {
123
+ const stackAfter = routerAfter.stack.length;
124
+ // The first arg is the mount path if it's a string
125
+ const mountPath = typeof args[0] === "string" ? args[0] : undefined;
126
+ if (mountPath && mountPath !== "/") {
127
+ for (let i = stackBefore; i < stackAfter; i++) {
128
+ routerAfter.stack[i][MOUNT_PATH_KEY] = mountPath.replace(/\/+$/, "");
129
+ }
130
+ }
131
+ }
132
+
133
+ return result;
134
+ };
135
+ };
136
+
137
+ /**
138
+ * Express middleware that patches the router stack before OpenAPI doc
139
+ * generation. Must be mounted before the openapi middleware.
140
+ */
141
+ export const openApiCompatMiddleware = (req: any, _res: any, next: () => void): void => {
142
+ const router = req.app._router || req.app.router;
143
+ if (router?.stack) {
144
+ patchRouterStack(router.stack);
145
+ }
146
+ next();
147
+ };
@@ -160,7 +160,7 @@ export function permissionMiddleware<T>(
160
160
  if (!data) {
161
161
  // Check if document exists but is hidden. Completely skip plugins.
162
162
  const hiddenDoc = await model.collection.findOne({
163
- _id: new mongoose.Types.ObjectId(req.params.id),
163
+ _id: new mongoose.Types.ObjectId(req.params.id as string),
164
164
  });
165
165
 
166
166
  if (!hiddenDoc) {