@terreno/api 0.18.0 → 0.20.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 (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/api.test.js +18 -8
  3. package/dist/auth.d.ts +5 -5
  4. package/dist/auth.js +123 -131
  5. package/dist/configuration.test.js +289 -10
  6. package/dist/configurationApp.d.ts +72 -5
  7. package/dist/configurationApp.js +168 -48
  8. package/dist/configurationPlugin.d.ts +64 -7
  9. package/dist/configurationPlugin.js +161 -39
  10. package/dist/configurationPlugin.test.js +238 -1
  11. package/dist/expressServer.test.js +0 -1
  12. package/dist/openApi.d.ts +6 -6
  13. package/dist/openApi.js +21 -21
  14. package/dist/populate.test.js +23 -0
  15. package/dist/realtime/queryMatcher.js +0 -6
  16. package/dist/realtime/queryStore.js +3 -11
  17. package/dist/realtime/realtime.test.js +41 -34
  18. package/dist/secretProviders.d.ts +79 -2
  19. package/dist/secretProviders.js +177 -9
  20. package/dist/secretProviders.test.d.ts +1 -0
  21. package/dist/secretProviders.test.js +391 -0
  22. package/package.json +1 -1
  23. package/src/actions.openApi.test.ts +1 -1
  24. package/src/actions.ts +0 -1
  25. package/src/api.test.ts +10 -2
  26. package/src/auth.ts +19 -19
  27. package/src/configuration.test.ts +171 -7
  28. package/src/configurationApp.ts +213 -30
  29. package/src/configurationPlugin.test.ts +174 -2
  30. package/src/configurationPlugin.ts +157 -28
  31. package/src/expressServer.test.ts +0 -1
  32. package/src/openApi.ts +21 -21
  33. package/src/populate.test.ts +25 -0
  34. package/src/realtime/queryMatcher.ts +0 -6
  35. package/src/realtime/queryStore.ts +1 -10
  36. package/src/realtime/realtime.test.ts +24 -24
  37. package/src/realtime/realtimeApp.ts +0 -1
  38. package/src/realtime/registry.ts +0 -1
  39. package/src/realtime/types.ts +0 -4
  40. package/src/secretProviders.test.ts +186 -0
  41. package/src/secretProviders.ts +145 -5
@@ -11,6 +11,12 @@ export interface SecretFieldMeta {
11
11
  path: string;
12
12
  secretProvider?: string;
13
13
  secretName: string;
14
+ /**
15
+ * Optional secret version to pin resolution to. When omitted the provider
16
+ * resolves the latest version. Discovered from the `secretVersion` schema
17
+ * path option.
18
+ */
19
+ version?: string;
14
20
  }
15
21
 
16
22
  /**
@@ -18,7 +24,15 @@ export interface SecretFieldMeta {
18
24
  */
19
25
  export interface SecretProvider {
20
26
  name: string;
21
- getSecret(secretName: string): Promise<string | null>;
27
+ /**
28
+ * Resolve a secret value by name. Returns `null` when the secret is not found.
29
+ *
30
+ * @param secretName - The secret identifier (short name or provider-specific path).
31
+ * @param version - Optional version to pin resolution to. Providers that do not
32
+ * support versioning (e.g. environment variables) ignore this parameter. When
33
+ * omitted, the latest version is resolved.
34
+ */
35
+ getSecret(secretName: string, version?: string): Promise<string | null>;
22
36
  }
23
37
 
24
38
  /**
@@ -30,6 +44,18 @@ export interface ConfigurationPluginOptions {
30
44
  * Typically set during app startup so the model can resolve secrets on demand.
31
45
  */
32
46
  secretProvider?: SecretProvider;
47
+ /**
48
+ * When `true`, adds a `_singleton` sentinel field with a unique index to
49
+ * enforce the singleton constraint at the database level.
50
+ *
51
+ * Defaults to `false`. Leave this off when the consuming app already enforces
52
+ * a single non-deleted document via the pre-save guard (the default behavior)
53
+ * or via its own indexes/soft-delete plugin, to avoid double-enforcement and
54
+ * conflicting indexes.
55
+ *
56
+ * @defaultValue false
57
+ */
58
+ enforceSingletonIndex?: boolean;
33
59
  }
34
60
 
35
61
  // ---------------------------------------------------------------------------
@@ -75,14 +101,26 @@ export interface ConfigurationStatics<T extends object> {
75
101
  getConfig(): Promise<T & Document>;
76
102
  /** Get a specific value by dot-notation key. */
77
103
  getConfig<P extends Paths<T>>(key: P): Promise<PathValue<T, P>>;
78
- /** Update the singleton configuration document (deep merge). */
104
+ /**
105
+ * Update the singleton configuration document.
106
+ *
107
+ * The patch is flattened into MongoDB dotted paths and applied with
108
+ * `findOneAndUpdate({$set})`. This preserves sibling fields inside nested
109
+ * subdocuments when a partial nested patch is supplied, and tolerates legacy /
110
+ * out-of-schema fields already persisted on the document (unlike a full
111
+ * `doc.save()`, which throws under `strict: "throw"`).
112
+ */
79
113
  updateConfig(updates: DeepPartial<T>): Promise<T & Document>;
80
114
  /** Get secret field metadata discovered from the schema. */
81
115
  getSecretFields(): SecretFieldMeta[];
82
116
  /**
83
117
  * Resolve all secret field values from a provider.
84
118
  * Uses the provider passed here, or falls back to the one configured in the plugin options.
85
- * Returns a map of path -> value.
119
+ * Returns an **in-memory** map of path -> value for programmatic use (startup
120
+ * self-checks, request-time resolution).
121
+ *
122
+ * This method never persists resolved values. Secret material must never be
123
+ * written to the configuration document.
86
124
  */
87
125
  resolveSecrets(provider?: SecretProvider): Promise<Map<string, string>>;
88
126
  }
@@ -103,6 +141,47 @@ export interface ConfigurationStatics<T extends object> {
103
141
  */
104
142
  export interface ConfigurationModel<T extends object> extends Model<T>, ConfigurationStatics<T> {}
105
143
 
144
+ // ---------------------------------------------------------------------------
145
+ // Helpers
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Flattens a nested patch into MongoDB-style dotted paths, recursing into plain
150
+ * objects only; arrays and other values are treated as leaves.
151
+ *
152
+ * @example
153
+ * flattenToDotPaths({a: {b: 1}}) // => [["a.b", 1]]
154
+ */
155
+ export const flattenToDotPaths = (
156
+ obj: Record<string, unknown>,
157
+ prefix = ""
158
+ ): Array<[string, unknown]> => {
159
+ const out: Array<[string, unknown]> = [];
160
+ for (const [key, value] of Object.entries(obj)) {
161
+ const path = prefix ? `${prefix}.${key}` : key;
162
+ const isPlainObject =
163
+ value !== null &&
164
+ typeof value === "object" &&
165
+ !Array.isArray(value) &&
166
+ Object.getPrototypeOf(value) === Object.prototype;
167
+ if (isPlainObject) {
168
+ out.push(...flattenToDotPaths(value as Record<string, unknown>, path));
169
+ } else {
170
+ out.push([path, value]);
171
+ }
172
+ }
173
+ return out;
174
+ };
175
+
176
+ /**
177
+ * Builds the filter used to locate the singleton document. When the schema is
178
+ * soft-delete aware (has a `deleted` path, e.g. via `isDeletedPlugin`), the
179
+ * singleton is "the one non-deleted document"; otherwise any document matches.
180
+ */
181
+ const buildSingletonFilter = (schema: Schema): Record<string, unknown> => {
182
+ return schema.path("deleted") ? {deleted: false} : {};
183
+ };
184
+
106
185
  // ---------------------------------------------------------------------------
107
186
  // Plugin
108
187
  // ---------------------------------------------------------------------------
@@ -111,13 +190,23 @@ export interface ConfigurationModel<T extends object> extends Model<T>, Configur
111
190
  * Mongoose schema plugin that adds singleton configuration behavior.
112
191
  *
113
192
  * Adds:
114
- * - Pre-save hook enforcing exactly one document
193
+ * - Pre-save hook enforcing exactly one non-deleted document (soft-delete aware
194
+ * when the schema has a `deleted` path, e.g. via `isDeletedPlugin`)
115
195
  * - `getConfig()` static: fetches or creates the singleton (full doc or keyed value)
116
- * - `updateConfig(updates)` static: patches the singleton
196
+ * - `updateConfig(updates)` static: patches the singleton via `findOneAndUpdate({$set})`
197
+ * with dotted paths (preserves sibling subdoc fields; tolerates legacy fields)
117
198
  * - `getSecretFields()` static: returns metadata for fields with `secret: true`
118
- * - `resolveSecrets(provider?)` static: fetches secret values, using the plugin provider by default
199
+ * - `resolveSecrets(provider?)` static: resolves secret values into an in-memory map,
200
+ * using the plugin provider by default (never persists values)
201
+ * - Hard-delete blockers (`deleteOne`/`deleteMany`/`findOneAndDelete`); soft deletes
202
+ * (setting `deleted: true`) are allowed
203
+ *
204
+ * Soft deletes are allowed and a soft-deleted document does not block creating a
205
+ * new singleton. The `_singleton` unique index is opt-in via
206
+ * `enforceSingletonIndex` (default off).
119
207
  *
120
- * Mark fields as secrets using schema path options:
208
+ * Mark fields as secrets using schema path options. Pin a version with the
209
+ * optional `secretVersion` option:
121
210
  * ```typescript
122
211
  * const configSchema = new Schema({
123
212
  * apiKey: {
@@ -125,6 +214,7 @@ export interface ConfigurationModel<T extends object> extends Model<T>, Configur
125
214
  * description: "Third-party API key",
126
215
  * secret: true,
127
216
  * secretName: "my-api-key",
217
+ * secretVersion: "3", // optional — resolves "latest" when omitted
128
218
  * },
129
219
  * });
130
220
  * configSchema.plugin(configurationPlugin, {secretProvider: new EnvSecretProvider()});
@@ -136,24 +226,31 @@ export const configurationPlugin = (schema: Schema, options?: ConfigurationPlugi
136
226
  // Apply findOneOrNone so the singleton lookup avoids bare Model.findOne (idempotent).
137
227
  findOneOrNone(schema);
138
228
 
139
- // Add a sentinel field with a unique index to enforce singleton at the DB level.
140
- // All config documents get _singleton: "config", and the unique index prevents duplicates.
141
- schema.add({
142
- _singleton: {
143
- default: "config",
144
- description: "Sentinel field enforcing singleton constraint",
145
- immutable: true,
146
- select: false,
147
- type: String,
148
- },
149
- });
150
- schema.index({_singleton: 1}, {unique: true});
229
+ // Optionally add a sentinel field with a unique index to enforce the singleton
230
+ // at the database level. This is opt-in (default off) so it does not conflict
231
+ // with consumers that already enforce a single non-deleted document via the
232
+ // pre-save guard below or via their own soft-delete plugin/indexes.
233
+ if (pluginOptions.enforceSingletonIndex) {
234
+ schema.add({
235
+ _singleton: {
236
+ default: "config",
237
+ description: "Sentinel field enforcing singleton constraint",
238
+ immutable: true,
239
+ select: false,
240
+ type: String,
241
+ },
242
+ });
243
+ schema.index({_singleton: 1}, {unique: true});
244
+ }
151
245
 
152
- // Enforce singleton: only one document allowed (application-level guard)
246
+ // Enforce singleton: only one non-deleted document allowed (application-level
247
+ // guard). Soft-delete-aware: a soft-deleted document does not block creating a
248
+ // new singleton.
153
249
  schema.pre("save", async function () {
154
250
  if (this.isNew) {
251
+ const filter = buildSingletonFilter(schema);
155
252
  // Cheap existence check — no document needs to be returned.
156
- const existing = await (this.constructor as Model<unknown>).exists({});
253
+ const existing = await (this.constructor as Model<unknown>).exists(filter);
157
254
  if (existing) {
158
255
  throw new APIError({
159
256
  status: 409,
@@ -183,9 +280,10 @@ export const configurationPlugin = (schema: Schema, options?: ConfigurationPlugi
183
280
 
184
281
  // Static: get the singleton configuration document or a value at a path (race-safe via upsert)
185
282
  schema.statics.getConfig = async function (key?: string): Promise<unknown> {
283
+ const singletonFilter = buildSingletonFilter(this.schema);
186
284
  const findSingleton = (): Promise<Document | null> =>
187
285
  (this as unknown as FindOneOrNonePlugin<unknown>).findOneOrNone(
188
- {}
286
+ singletonFilter
189
287
  ) as Promise<Document | null>;
190
288
  let config: Document | null = await findSingleton();
191
289
  if (!config) {
@@ -222,14 +320,44 @@ export const configurationPlugin = (schema: Schema, options?: ConfigurationPlugi
222
320
  return value;
223
321
  };
224
322
 
225
- // Static: update the singleton configuration document (race-safe)
323
+ // Static: update the singleton configuration document via $set dotted paths.
324
+ // Flattening to dotted paths preserves sibling subdoc fields and tolerates
325
+ // legacy/out-of-schema fields already persisted on the document.
226
326
  schema.statics.updateConfig = async function (
227
327
  updates: Record<string, unknown>
228
328
  ): Promise<unknown> {
229
- const config = await (this as ConfigurationModel<Record<string, unknown>>).getConfig();
230
- Object.assign(config, updates);
231
- await (config as Document).save();
232
- return config;
329
+ const singletonFilter = buildSingletonFilter(this.schema);
330
+ const setFields: Record<string, unknown> = {};
331
+ for (const [path, value] of flattenToDotPaths(updates)) {
332
+ setFields[path] = value;
333
+ }
334
+
335
+ // Nothing to set — return the current singleton (creating it if missing).
336
+ if (Object.keys(setFields).length === 0) {
337
+ return (this as unknown as ConfigurationModel<Record<string, unknown>>).getConfig();
338
+ }
339
+
340
+ // runValidators keeps schema validation (enum/min/custom validators) on the
341
+ // patched paths, matching the prior doc.save() behavior. Legacy/out-of-schema
342
+ // fields already on the document are untouched (not in $set), so they are not
343
+ // re-validated.
344
+ const updated = await this.findOneAndUpdate(
345
+ singletonFilter,
346
+ {$set: setFields},
347
+ {new: true, runValidators: true}
348
+ );
349
+ if (updated) {
350
+ return updated;
351
+ }
352
+
353
+ // No singleton yet — create one (with subdocument defaults applied), then
354
+ // apply the patch.
355
+ await (this as unknown as ConfigurationModel<Record<string, unknown>>).getConfig();
356
+ return this.findOneAndUpdate(
357
+ singletonFilter,
358
+ {$set: setFields},
359
+ {new: true, runValidators: true}
360
+ ).orFail();
233
361
  };
234
362
 
235
363
  // Static: discover secret fields from schema options
@@ -243,6 +371,7 @@ export const configurationPlugin = (schema: Schema, options?: ConfigurationPlugi
243
371
  path: prefix ? `${prefix}.${pathName}` : pathName,
244
372
  secretName: (opts.secretName as string) ?? pathName,
245
373
  secretProvider: opts.secretProvider as string | undefined,
374
+ version: opts.secretVersion as string | undefined,
246
375
  });
247
376
  }
248
377
  // Recurse into subschemas
@@ -275,7 +404,7 @@ export const configurationPlugin = (schema: Schema, options?: ConfigurationPlugi
275
404
 
276
405
  const results = await Promise.allSettled(
277
406
  secrets.map(async (meta: SecretFieldMeta) => {
278
- const value = await resolvedProvider.getSecret(meta.secretName);
407
+ const value = await resolvedProvider.getSecret(meta.secretName, meta.version);
279
408
  if (value !== null) {
280
409
  resolved.set(meta.path, value);
281
410
  }
@@ -812,7 +812,6 @@ describe("expressServer", () => {
812
812
  // Mock app.listen on the Express prototype to avoid opening a real port
813
813
  const express = await import("express");
814
814
  const originalListen = express.default.application.listen;
815
- // biome-ignore lint/suspicious/noExplicitAny: mocking Express internals requires type escape
816
815
  express.default.application.listen = mock(function (this: unknown, ...args: unknown[]) {
817
816
  const cb = args.find((a: unknown) => typeof a === "function") as (() => void) | undefined;
818
817
  if (cb) {
package/src/openApi.ts CHANGED
@@ -8,7 +8,7 @@ import type {ModelRouterOptions, OpenApiMiddleware} from "./api";
8
8
  import {logger} from "./logger";
9
9
  import {getOpenApiSpecForModel} from "./populate";
10
10
 
11
- const noop = (_a, _b, next) => next();
11
+ const noop = (_a: unknown, _b: unknown, next: () => void) => next();
12
12
 
13
13
  const m2sOptions = {
14
14
  props: ["readOnly", "required", "enum", "default"],
@@ -44,7 +44,7 @@ export const defaultOpenApiErrorResponses = {
44
44
  };
45
45
 
46
46
  // We repeat this constantly, so we make it a component so we only have to define it once.
47
- function createAPIErrorComponent(openApi?: OpenApiMiddleware) {
47
+ const createAPIErrorComponent = (openApi?: OpenApiMiddleware): void => {
48
48
  // Create a schema component called APIError
49
49
  openApi?.component("schemas", "APIError", {
50
50
  properties: {
@@ -111,12 +111,12 @@ function createAPIErrorComponent(openApi?: OpenApiMiddleware) {
111
111
  },
112
112
  type: "object",
113
113
  });
114
- }
114
+ };
115
115
 
116
- export function getOpenApiMiddleware<T>(
116
+ export const getOpenApiMiddleware = <T>(
117
117
  model: Model<T>,
118
118
  options: Partial<ModelRouterOptions<T>>
119
- ): express.RequestHandler {
119
+ ): express.RequestHandler => {
120
120
  createAPIErrorComponent(options.openApi);
121
121
  if (!options.openApi?.path) {
122
122
  // Just log this once rather than for each middleware.
@@ -158,12 +158,12 @@ export function getOpenApiMiddleware<T>(
158
158
  options.openApiOverwrite?.get ?? {}
159
159
  )
160
160
  );
161
- }
161
+ };
162
162
 
163
- export function listOpenApiMiddleware<T>(
163
+ export const listOpenApiMiddleware = <T>(
164
164
  model: Model<T>,
165
165
  options: Partial<ModelRouterOptions<T>>
166
- ): express.RequestHandler {
166
+ ): express.RequestHandler => {
167
167
  if (!options.openApi?.path) {
168
168
  return noop;
169
169
  }
@@ -324,12 +324,12 @@ export function listOpenApiMiddleware<T>(
324
324
  options.openApiOverwrite?.list ?? {}
325
325
  )
326
326
  );
327
- }
327
+ };
328
328
 
329
- export function createOpenApiMiddleware<T>(
329
+ export const createOpenApiMiddleware = <T>(
330
330
  model: Model<T>,
331
331
  options: Partial<ModelRouterOptions<T>>
332
- ): express.RequestHandler {
332
+ ): express.RequestHandler => {
333
333
  if (!options.openApi?.path) {
334
334
  return noop;
335
335
  }
@@ -376,12 +376,12 @@ export function createOpenApiMiddleware<T>(
376
376
  options.openApiOverwrite?.create ?? {}
377
377
  )
378
378
  );
379
- }
379
+ };
380
380
 
381
- export function patchOpenApiMiddleware<T>(
381
+ export const patchOpenApiMiddleware = <T>(
382
382
  model: Model<T>,
383
383
  options: Partial<ModelRouterOptions<T>>
384
- ): express.RequestHandler {
384
+ ): express.RequestHandler => {
385
385
  if (!options.openApi?.path) {
386
386
  return noop;
387
387
  }
@@ -428,12 +428,12 @@ export function patchOpenApiMiddleware<T>(
428
428
  options.openApiOverwrite?.update ?? {}
429
429
  )
430
430
  );
431
- }
431
+ };
432
432
 
433
- export function deleteOpenApiMiddleware<T>(
433
+ export const deleteOpenApiMiddleware = <T>(
434
434
  model: Model<T>,
435
435
  options: Partial<ModelRouterOptions<T>>
436
- ): express.RequestHandler {
436
+ ): express.RequestHandler => {
437
437
  if (!options.openApi?.path) {
438
438
  return noop;
439
439
  }
@@ -456,16 +456,16 @@ export function deleteOpenApiMiddleware<T>(
456
456
  options.openApiOverwrite?.delete ?? {}
457
457
  )
458
458
  );
459
- }
459
+ };
460
460
 
461
461
  // This is a generic OpenAPI wrapper for a read that returns any object described by `properties`.
462
462
  // Useful for endpoints that don't directly map to a model.
463
- export function readOpenApiMiddleware<T>(
463
+ export const readOpenApiMiddleware = <T>(
464
464
  options: Partial<ModelRouterOptions<T>>,
465
465
  properties: Record<string, unknown>,
466
466
  required: string[],
467
467
  queryParameters: Array<Record<string, unknown>>
468
- ): express.RequestHandler {
468
+ ): express.RequestHandler => {
469
469
  if (!options.openApi?.path) {
470
470
  // Just log this once rather than for each middleware.
471
471
  logger.debug(
@@ -502,4 +502,4 @@ export function readOpenApiMiddleware<T>(
502
502
  options.openApiOverwrite?.get ?? {}
503
503
  )
504
504
  );
505
- }
505
+ };
@@ -265,6 +265,31 @@ describe("getOpenApiSpecForModel edge cases", () => {
265
265
  });
266
266
  });
267
267
 
268
+ describe("getOpenApiSpecForModel populate with existing properties", () => {
269
+ it("merges populated properties into a path that already has properties", () => {
270
+ const result = getOpenApiSpecForModel(FoodModel, {
271
+ populatePaths: [{path: "likesIds.userId"}],
272
+ });
273
+ // likesIds is an array subschema with its own properties already;
274
+ // populating userId should merge the user properties into the existing structure.
275
+ expect(result.properties.likesIds).toBeDefined();
276
+ const likesIds = result.properties.likesIds as Record<string, unknown>;
277
+ const items = likesIds.items as Record<string, Record<string, unknown>>;
278
+ expect(items.properties.userId).toBeDefined();
279
+ });
280
+
281
+ it("creates intermediate path structure when navigating to nested populate", () => {
282
+ // eatenBy is defined as [{ ref: "User", type: ObjectId }] - an array of refs.
283
+ // When we populate eatenBy, the openApiPath resolves through items.
284
+ const result = getOpenApiSpecForModel(FoodModel, {
285
+ populatePaths: [{path: "eatenBy"}],
286
+ });
287
+ expect(result.properties.eatenBy).toBeDefined();
288
+ const eatenBy = result.properties.eatenBy as Record<string, unknown>;
289
+ expect(eatenBy.items).toBeDefined();
290
+ });
291
+ });
292
+
268
293
  describe("filterKeys (via getOpenApiSpecForModel populatePaths)", () => {
269
294
  it("filters populated fields using dot-notation keys", () => {
270
295
  const result = getOpenApiSpecForModel(FoodModel, {
@@ -6,7 +6,6 @@
6
6
  * Supports: equality, $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or, $not.
7
7
  */
8
8
 
9
- // biome-ignore lint/suspicious/noExplicitAny: traversing arbitrary nested document fields by user-supplied dotted path
10
9
  const getNestedValue = (doc: any, path: string): any => {
11
10
  const parts = path.split(".");
12
11
  let current = doc;
@@ -19,7 +18,6 @@ const getNestedValue = (doc: any, path: string): any => {
19
18
  return current;
20
19
  };
21
20
 
22
- // biome-ignore lint/suspicious/noExplicitAny: value may be any document field type (string, number, ObjectId, etc.)
23
21
  const normalize = (value: any): any => {
24
22
  if (value === null || value === undefined) {
25
23
  return value;
@@ -36,7 +34,6 @@ const normalize = (value: any): any => {
36
34
  return value;
37
35
  };
38
36
 
39
- // biome-ignore lint/suspicious/noExplicitAny: rawValue is an arbitrary document field, condition is an arbitrary user query operand
40
37
  const matchesCondition = (rawValue: any, condition: any): boolean => {
41
38
  const value = normalize(rawValue);
42
39
 
@@ -91,7 +88,6 @@ const matchesCondition = (rawValue: any, condition: any): boolean => {
91
88
  return false;
92
89
  }
93
90
  const inValues = operand.map(normalize);
94
- // biome-ignore lint/suspicious/noExplicitAny: normalized value of arbitrary document field
95
91
  if (!inValues.some((v: any) => v === value || String(v) === String(value))) {
96
92
  return false;
97
93
  }
@@ -102,7 +98,6 @@ const matchesCondition = (rawValue: any, condition: any): boolean => {
102
98
  return false;
103
99
  }
104
100
  const ninValues = operand.map(normalize);
105
- // biome-ignore lint/suspicious/noExplicitAny: normalized value of arbitrary document field
106
101
  if (ninValues.some((v: any) => v === value || String(v) === String(value))) {
107
102
  return false;
108
103
  }
@@ -137,7 +132,6 @@ const matchesCondition = (rawValue: any, condition: any): boolean => {
137
132
  * @param query - MongoDB-style query object
138
133
  * @returns true if the document matches all query conditions
139
134
  */
140
- // biome-ignore lint/suspicious/noExplicitAny: doc is arbitrary; query values are arbitrary user-supplied JSON
141
135
  export const matchesQuery = (doc: any, query: Record<string, any>): boolean => {
142
136
  for (const [key, condition] of Object.entries(query)) {
143
137
  if (key === "$and") {
@@ -9,7 +9,6 @@
9
9
 
10
10
  interface QuerySubscription {
11
11
  collection: string;
12
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
13
12
  query: Record<string, any>;
14
13
  queryId: string;
15
14
  }
@@ -18,13 +17,8 @@ interface QuerySubscription {
18
17
  * Compute a deterministic queryId from collection and query on the server side.
19
18
  * This prevents clients from hijacking other subscriptions by providing a colliding queryId.
20
19
  */
21
- export const computeQueryId = (
22
- collection: string,
23
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
24
- query: Record<string, any>
25
- ): string => {
20
+ export const computeQueryId = (collection: string, query: Record<string, any>): string => {
26
21
  const sortedKeys = Object.keys(query).sort();
27
- // biome-ignore lint/suspicious/noExplicitAny: mirrors the input query value shape
28
22
  const normalized: Record<string, any> = {};
29
23
  for (const key of sortedKeys) {
30
24
  normalized[key] = query[key];
@@ -45,7 +39,6 @@ const socketQueries = new Map<string, Set<string>>();
45
39
  export const addQuerySubscription = (
46
40
  socketId: string,
47
41
  collection: string,
48
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
49
42
  query: Record<string, any>,
50
43
  queryId: string
51
44
  ): void => {
@@ -109,9 +102,7 @@ export const removeAllSocketQueries = (socketId: string): void => {
109
102
  */
110
103
  export const getQuerySubscriptionsForCollection = (
111
104
  collection: string
112
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
113
105
  ): {queryId: string; query: Record<string, any>}[] => {
114
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
115
106
  const result: {queryId: string; query: Record<string, any>}[] = [];
116
107
 
117
108
  for (const [queryId, sub] of querySubscriptions) {