@terreno/api 0.17.0 → 0.19.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.
@@ -328,6 +328,29 @@ var tests_1 = require("./tests");
328
328
  (0, bun_test_1.expect)(detail.properties.displayAmount.type).toBe("any");
329
329
  });
330
330
  });
331
+ (0, bun_test_1.describe)("getOpenApiSpecForModel populate with existing properties", function () {
332
+ (0, bun_test_1.it)("merges populated properties into a path that already has properties", function () {
333
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
334
+ populatePaths: [{ path: "likesIds.userId" }],
335
+ });
336
+ // likesIds is an array subschema with its own properties already;
337
+ // populating userId should merge the user properties into the existing structure.
338
+ (0, bun_test_1.expect)(result.properties.likesIds).toBeDefined();
339
+ var likesIds = result.properties.likesIds;
340
+ var items = likesIds.items;
341
+ (0, bun_test_1.expect)(items.properties.userId).toBeDefined();
342
+ });
343
+ (0, bun_test_1.it)("creates intermediate path structure when navigating to nested populate", function () {
344
+ // eatenBy is defined as [{ ref: "User", type: ObjectId }] - an array of refs.
345
+ // When we populate eatenBy, the openApiPath resolves through items.
346
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
347
+ populatePaths: [{ path: "eatenBy" }],
348
+ });
349
+ (0, bun_test_1.expect)(result.properties.eatenBy).toBeDefined();
350
+ var eatenBy = result.properties.eatenBy;
351
+ (0, bun_test_1.expect)(eatenBy.items).toBeDefined();
352
+ });
353
+ });
331
354
  (0, bun_test_1.describe)("filterKeys (via getOpenApiSpecForModel populatePaths)", function () {
332
355
  (0, bun_test_1.it)("filters populated fields using dot-notation keys", function () {
333
356
  var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
@@ -35,7 +35,6 @@ var __read = (this && this.__read) || function (o, n) {
35
35
  };
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.matchesQuery = void 0;
38
- // biome-ignore lint/suspicious/noExplicitAny: traversing arbitrary nested document fields by user-supplied dotted path
39
38
  var getNestedValue = function (doc, path) {
40
39
  var e_1, _a;
41
40
  var parts = path.split(".");
@@ -58,7 +57,6 @@ var getNestedValue = function (doc, path) {
58
57
  }
59
58
  return current;
60
59
  };
61
- // biome-ignore lint/suspicious/noExplicitAny: value may be any document field type (string, number, ObjectId, etc.)
62
60
  var normalize = function (value) {
63
61
  var _a;
64
62
  if (value === null || value === undefined) {
@@ -73,7 +71,6 @@ var normalize = function (value) {
73
71
  }
74
72
  return value;
75
73
  };
76
- // biome-ignore lint/suspicious/noExplicitAny: rawValue is an arbitrary document field, condition is an arbitrary user query operand
77
74
  var matchesCondition = function (rawValue, condition) {
78
75
  var e_2, _a;
79
76
  var value = normalize(rawValue);
@@ -127,7 +124,6 @@ var matchesCondition = function (rawValue, condition) {
127
124
  return false;
128
125
  }
129
126
  var inValues = operand.map(normalize);
130
- // biome-ignore lint/suspicious/noExplicitAny: normalized value of arbitrary document field
131
127
  if (!inValues.some(function (v) { return v === value || String(v) === String(value); })) {
132
128
  return false;
133
129
  }
@@ -138,7 +134,6 @@ var matchesCondition = function (rawValue, condition) {
138
134
  return false;
139
135
  }
140
136
  var ninValues = operand.map(normalize);
141
- // biome-ignore lint/suspicious/noExplicitAny: normalized value of arbitrary document field
142
137
  if (ninValues.some(function (v) { return v === value || String(v) === String(value); })) {
143
138
  return false;
144
139
  }
@@ -179,7 +174,6 @@ var matchesCondition = function (rawValue, condition) {
179
174
  * @param query - MongoDB-style query object
180
175
  * @returns true if the document matches all query conditions
181
176
  */
182
- // biome-ignore lint/suspicious/noExplicitAny: doc is arbitrary; query values are arbitrary user-supplied JSON
183
177
  var matchesQuery = function (doc, query) {
184
178
  var e_3, _a, e_4, _b, e_5, _c;
185
179
  try {
@@ -40,12 +40,9 @@ exports.clearQueryStore = exports.getQuerySubscriptionsForCollection = exports.r
40
40
  * Compute a deterministic queryId from collection and query on the server side.
41
41
  * This prevents clients from hijacking other subscriptions by providing a colliding queryId.
42
42
  */
43
- var computeQueryId = function (collection,
44
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
45
- query) {
43
+ var computeQueryId = function (collection, query) {
46
44
  var e_1, _a;
47
45
  var sortedKeys = Object.keys(query).sort();
48
- // biome-ignore lint/suspicious/noExplicitAny: mirrors the input query value shape
49
46
  var normalized = {};
50
47
  try {
51
48
  for (var sortedKeys_1 = __values(sortedKeys), sortedKeys_1_1 = sortedKeys_1.next(); !sortedKeys_1_1.done; sortedKeys_1_1 = sortedKeys_1.next()) {
@@ -71,9 +68,7 @@ var socketQueries = new Map();
71
68
  * Register a query subscription for a socket.
72
69
  * The socket joins the `query:{queryId}` room (handled by the caller).
73
70
  */
74
- var addQuerySubscription = function (socketId, collection,
75
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
76
- query, queryId) {
71
+ var addQuerySubscription = function (socketId, collection, query, queryId) {
77
72
  var _a;
78
73
  querySubscriptions.set(queryId, { collection: collection, query: query, queryId: queryId });
79
74
  if (!socketQueries.has(socketId)) {
@@ -161,11 +156,8 @@ exports.removeAllSocketQueries = removeAllSocketQueries;
161
156
  * Get all unique query subscriptions for a given collection.
162
157
  * Used by the change stream watcher to evaluate which query rooms to emit to.
163
158
  */
164
- var getQuerySubscriptionsForCollection = function (collection
165
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
166
- ) {
159
+ var getQuerySubscriptionsForCollection = function (collection) {
167
160
  var e_5, _a;
168
- // biome-ignore lint/suspicious/noExplicitAny: MongoDB query filter values are arbitrary user-supplied JSON
169
161
  var result = [];
170
162
  try {
171
163
  for (var querySubscriptions_1 = __values(querySubscriptions), querySubscriptions_1_1 = querySubscriptions_1.next(); !querySubscriptions_1_1.done; querySubscriptions_1_1 = querySubscriptions_1.next()) {
@@ -2201,6 +2201,22 @@ var createMockSocket = function (decodedToken) {
2201
2201
  },
2202
2202
  };
2203
2203
  };
2204
+ var invokeRegisteredChangeHandler = function (mockStream, event) { return __awaiter(void 0, void 0, void 0, function () {
2205
+ var changeHandler;
2206
+ return __generator(this, function (_a) {
2207
+ switch (_a.label) {
2208
+ case 0:
2209
+ changeHandler = mockStream.listeners.get("change");
2210
+ if (!changeHandler) {
2211
+ throw new Error("expected change handler");
2212
+ }
2213
+ return [4 /*yield*/, changeHandler(event)];
2214
+ case 1:
2215
+ _a.sent();
2216
+ return [2 /*return*/];
2217
+ }
2218
+ });
2219
+ }); };
2204
2220
  var createMockIo = function () {
2205
2221
  var rooms = new Map();
2206
2222
  var sockets = new Map();
@@ -2272,7 +2288,7 @@ var createMockSocket = function (decodedToken) {
2272
2288
  });
2273
2289
  }); });
2274
2290
  (0, bun_test_1.it)("handles change events for registered models", function () { return __awaiter(void 0, void 0, void 0, function () {
2275
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2291
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2276
2292
  return __generator(this, function (_a) {
2277
2293
  switch (_a.label) {
2278
2294
  case 0:
@@ -2304,22 +2320,22 @@ var createMockSocket = function (decodedToken) {
2304
2320
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2305
2321
  io = createMockIo();
2306
2322
  startChangeStreamWatcher(io, {}, true);
2307
- changeHandler = mockStream.listeners.get("change");
2308
- (0, bun_test_1.expect)(changeHandler).toBeDefined();
2309
- return [4 /*yield*/, changeHandler({
2323
+ // Trigger an insert change event
2324
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2310
2325
  documentKey: { _id: "doc-1" },
2311
2326
  fullDocument: { _id: "doc-1", name: "Test Todo" },
2312
2327
  ns: { coll: "todos" },
2313
2328
  operationType: "insert",
2314
2329
  })];
2315
2330
  case 2:
2331
+ // Trigger an insert change event
2316
2332
  _a.sent();
2317
2333
  return [2 /*return*/];
2318
2334
  }
2319
2335
  });
2320
2336
  }); });
2321
2337
  (0, bun_test_1.it)("skips events for unregistered collections", function () { return __awaiter(void 0, void 0, void 0, function () {
2322
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2338
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2323
2339
  return __generator(this, function (_a) {
2324
2340
  switch (_a.label) {
2325
2341
  case 0:
@@ -2333,9 +2349,8 @@ var createMockSocket = function (decodedToken) {
2333
2349
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2334
2350
  io = createMockIo();
2335
2351
  startChangeStreamWatcher(io, {}, true);
2336
- changeHandler = mockStream.listeners.get("change");
2337
2352
  // Trigger for an unregistered collection — should not throw
2338
- return [4 /*yield*/, changeHandler({
2353
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2339
2354
  documentKey: { _id: "doc-1" },
2340
2355
  fullDocument: { _id: "doc-1" },
2341
2356
  ns: { coll: "unknown_collection" },
@@ -2349,7 +2364,7 @@ var createMockSocket = function (decodedToken) {
2349
2364
  });
2350
2365
  }); });
2351
2366
  (0, bun_test_1.it)("skips events when method is not enabled for the model", function () { return __awaiter(void 0, void 0, void 0, function () {
2352
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2367
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2353
2368
  return __generator(this, function (_a) {
2354
2369
  switch (_a.label) {
2355
2370
  case 0:
@@ -2381,9 +2396,8 @@ var createMockSocket = function (decodedToken) {
2381
2396
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2382
2397
  io = createMockIo();
2383
2398
  startChangeStreamWatcher(io, {}, true);
2384
- changeHandler = mockStream.listeners.get("change");
2385
2399
  // Update event should be skipped because "update" not in methods
2386
- return [4 /*yield*/, changeHandler({
2400
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2387
2401
  documentKey: { _id: "doc-1" },
2388
2402
  fullDocument: { _id: "doc-1", name: "Updated" },
2389
2403
  ns: { coll: "todos" },
@@ -2398,7 +2412,7 @@ var createMockSocket = function (decodedToken) {
2398
2412
  });
2399
2413
  }); });
2400
2414
  (0, bun_test_1.it)("handles delete events for owner-strategy models", function () { return __awaiter(void 0, void 0, void 0, function () {
2401
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2415
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2402
2416
  return __generator(this, function (_a) {
2403
2417
  switch (_a.label) {
2404
2418
  case 0:
@@ -2430,9 +2444,8 @@ var createMockSocket = function (decodedToken) {
2430
2444
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2431
2445
  io = createMockIo();
2432
2446
  startChangeStreamWatcher(io, {}, true);
2433
- changeHandler = mockStream.listeners.get("change");
2434
2447
  // Hard delete (no fullDocument)
2435
- return [4 /*yield*/, changeHandler({
2448
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2436
2449
  documentKey: { _id: "doc-1" },
2437
2450
  ns: { coll: "todos" },
2438
2451
  operationType: "delete",
@@ -2445,7 +2458,7 @@ var createMockSocket = function (decodedToken) {
2445
2458
  });
2446
2459
  }); });
2447
2460
  (0, bun_test_1.it)("handles delete events for broadcast-strategy models", function () { return __awaiter(void 0, void 0, void 0, function () {
2448
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2461
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2449
2462
  return __generator(this, function (_a) {
2450
2463
  switch (_a.label) {
2451
2464
  case 0:
@@ -2477,9 +2490,8 @@ var createMockSocket = function (decodedToken) {
2477
2490
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2478
2491
  io = createMockIo();
2479
2492
  startChangeStreamWatcher(io, {}, true);
2480
- changeHandler = mockStream.listeners.get("change");
2481
2493
  // Hard delete for broadcast strategy
2482
- return [4 /*yield*/, changeHandler({
2494
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2483
2495
  documentKey: { _id: "doc-1" },
2484
2496
  ns: { coll: "broadcasts" },
2485
2497
  operationType: "delete",
@@ -2492,7 +2504,7 @@ var createMockSocket = function (decodedToken) {
2492
2504
  });
2493
2505
  }); });
2494
2506
  (0, bun_test_1.it)("includes updatedFields in event for update operations", function () { return __awaiter(void 0, void 0, void 0, function () {
2495
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2507
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2496
2508
  return __generator(this, function (_a) {
2497
2509
  switch (_a.label) {
2498
2510
  case 0:
@@ -2524,8 +2536,7 @@ var createMockSocket = function (decodedToken) {
2524
2536
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2525
2537
  io = createMockIo();
2526
2538
  startChangeStreamWatcher(io, {}, true);
2527
- changeHandler = mockStream.listeners.get("change");
2528
- return [4 /*yield*/, changeHandler({
2539
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2529
2540
  documentKey: { _id: "doc-1" },
2530
2541
  fullDocument: { _id: "doc-1", name: "Updated", status: "done" },
2531
2542
  ns: { coll: "todos" },
@@ -2563,7 +2574,7 @@ var createMockSocket = function (decodedToken) {
2563
2574
  });
2564
2575
  }); });
2565
2576
  (0, bun_test_1.it)("respects ignoredOperations config", function () { return __awaiter(void 0, void 0, void 0, function () {
2566
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2577
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2567
2578
  return __generator(this, function (_a) {
2568
2579
  switch (_a.label) {
2569
2580
  case 0:
@@ -2595,9 +2606,8 @@ var createMockSocket = function (decodedToken) {
2595
2606
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2596
2607
  io = createMockIo();
2597
2608
  startChangeStreamWatcher(io, { ignoredOperations: ["insert"] }, true);
2598
- changeHandler = mockStream.listeners.get("change");
2599
2609
  // This insert should be skipped because "insert" is ignored
2600
- return [4 /*yield*/, changeHandler({
2610
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2601
2611
  documentKey: { _id: "doc-1" },
2602
2612
  fullDocument: { _id: "doc-1" },
2603
2613
  ns: { coll: "todos" },
@@ -2611,7 +2621,7 @@ var createMockSocket = function (decodedToken) {
2611
2621
  });
2612
2622
  }); });
2613
2623
  (0, bun_test_1.it)("skips events with no collectionName or docId", function () { return __awaiter(void 0, void 0, void 0, function () {
2614
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2624
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2615
2625
  return __generator(this, function (_a) {
2616
2626
  switch (_a.label) {
2617
2627
  case 0:
@@ -2625,9 +2635,8 @@ var createMockSocket = function (decodedToken) {
2625
2635
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2626
2636
  io = createMockIo();
2627
2637
  startChangeStreamWatcher(io, {}, true);
2628
- changeHandler = mockStream.listeners.get("change");
2629
2638
  // Missing ns.coll
2630
- return [4 /*yield*/, changeHandler({
2639
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2631
2640
  documentKey: { _id: "doc-1" },
2632
2641
  ns: {},
2633
2642
  operationType: "insert",
@@ -2636,7 +2645,7 @@ var createMockSocket = function (decodedToken) {
2636
2645
  // Missing ns.coll
2637
2646
  _a.sent();
2638
2647
  // Missing documentKey
2639
- return [4 /*yield*/, changeHandler({
2648
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2640
2649
  documentKey: {},
2641
2650
  ns: { coll: "todos" },
2642
2651
  operationType: "insert",
@@ -2649,7 +2658,7 @@ var createMockSocket = function (decodedToken) {
2649
2658
  });
2650
2659
  }); });
2651
2660
  (0, bun_test_1.it)("skips non-CRUD operation types", function () { return __awaiter(void 0, void 0, void 0, function () {
2652
- var mockStream, mockDb, startChangeStreamWatcher, io, changeHandler;
2661
+ var mockStream, mockDb, startChangeStreamWatcher, io;
2653
2662
  return __generator(this, function (_a) {
2654
2663
  switch (_a.label) {
2655
2664
  case 0:
@@ -2663,9 +2672,8 @@ var createMockSocket = function (decodedToken) {
2663
2672
  startChangeStreamWatcher = (_a.sent()).startChangeStreamWatcher;
2664
2673
  io = createMockIo();
2665
2674
  startChangeStreamWatcher(io, {}, true);
2666
- changeHandler = mockStream.listeners.get("change");
2667
2675
  // "drop" is not in our pipeline filter, should be skipped
2668
- return [4 /*yield*/, changeHandler({
2676
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2669
2677
  operationType: "drop",
2670
2678
  })];
2671
2679
  case 2:
@@ -2721,7 +2729,7 @@ var createMockSocket = function (decodedToken) {
2721
2729
  });
2722
2730
  }); });
2723
2731
  (0, bun_test_1.it)("catches errors thrown in the change handler", function () { return __awaiter(void 0, void 0, void 0, function () {
2724
- var mockStream, mockDb, startChangeStreamWatcher, emissions, mockSocket, rooms, sockets, io, changeHandler;
2732
+ var mockStream, mockDb, startChangeStreamWatcher, emissions, mockSocket, rooms, sockets, io;
2725
2733
  return __generator(this, function (_a) {
2726
2734
  switch (_a.label) {
2727
2735
  case 0:
@@ -2776,9 +2784,8 @@ var createMockSocket = function (decodedToken) {
2776
2784
  to: function () { return ({ emit: function () { } }); },
2777
2785
  };
2778
2786
  startChangeStreamWatcher(io, {}, true);
2779
- changeHandler = mockStream.listeners.get("change");
2780
2787
  // Should not throw even though permission check throws
2781
- return [4 /*yield*/, changeHandler({
2788
+ return [4 /*yield*/, invokeRegisteredChangeHandler(mockStream, {
2782
2789
  documentKey: { _id: "doc-1" },
2783
2790
  fullDocument: { _id: "doc-1", name: "Test" },
2784
2791
  ns: { coll: "todos" },
@@ -2862,7 +2869,7 @@ var createMockSocket = function (decodedToken) {
2862
2869
  });
2863
2870
  }); });
2864
2871
  var makeServer = function () {
2865
- var http = require("http");
2872
+ var http = require("node:http");
2866
2873
  var server = http.createServer();
2867
2874
  servers.push(server);
2868
2875
  return server;
@@ -2925,7 +2932,7 @@ var createMockSocket = function (decodedToken) {
2925
2932
  });
2926
2933
  }); });
2927
2934
  var makeServer = function () {
2928
- var http = require("http");
2935
+ var http = require("node:http");
2929
2936
  var server = http.createServer();
2930
2937
  servers.push(server);
2931
2938
  return server;
package/package.json CHANGED
@@ -109,5 +109,5 @@
109
109
  "updateSnapshot": "bun test --update-snapshots"
110
110
  },
111
111
  "types": "dist/index.d.ts",
112
- "version": "0.17.0"
112
+ "version": "0.19.0"
113
113
  }
@@ -7,7 +7,7 @@ import {addAuthRoutes, setupAuth} from "./auth";
7
7
  import {setupServer} from "./expressServer";
8
8
  import {Permissions} from "./permissions";
9
9
  import {TerrenoApp} from "./terrenoApp";
10
- import {authAsUser, FoodModel, setupDb, UserModel} from "./tests";
10
+ import {FoodModel, setupDb, UserModel} from "./tests";
11
11
  import {z} from "./zodOpenApi";
12
12
 
13
13
  const foodActionPermissions = {
package/src/actions.ts CHANGED
@@ -9,7 +9,6 @@ import {loadDocOr404} from "./docLoader";
9
9
  import {APIError} from "./errors";
10
10
  import {defaultOpenApiErrorResponses} from "./openApi";
11
11
  import {checkPermissions, type PermissionMethod} from "./permissions";
12
- import {z} from "./zodOpenApi";
13
12
 
14
13
  // At least two characters: leading letter plus one or more alphanumeric/_/- chars.
15
14
  export const ACTION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]+$/;
package/src/api.test.ts CHANGED
@@ -2010,9 +2010,13 @@ describe("@terreno/api", () => {
2010
2010
  });
2011
2011
 
2012
2012
  it("returns 409 when precise conflict timestamp is older than doc.updated", async () => {
2013
+ const ifUnmodifiedSince = DateTime.fromISO("2025-06-15T12:00:01.000Z").toHTTP();
2014
+ if (ifUnmodifiedSince === null) {
2015
+ throw new Error("expected HTTP If-Unmodified-Since value");
2016
+ }
2013
2017
  await agent
2014
2018
  .patch(`/food/${spinach._id}`)
2015
- .set("If-Unmodified-Since", DateTime.fromISO("2025-06-15T12:00:01.000Z").toHTTP()!)
2019
+ .set("If-Unmodified-Since", ifUnmodifiedSince)
2016
2020
  .set("X-Unmodified-Since-ISO", "2025-06-15T11:59:59.500Z")
2017
2021
  .send({name: "Precise Stale"})
2018
2022
  .expect(409);
@@ -2024,9 +2028,13 @@ describe("@terreno/api", () => {
2024
2028
  {$unset: {updated: ""}}
2025
2029
  );
2026
2030
 
2031
+ const ifUnmodifiedSince = DateTime.fromISO("2025-06-15T11:59:59.999Z").toHTTP();
2032
+ if (ifUnmodifiedSince === null) {
2033
+ throw new Error("expected HTTP If-Unmodified-Since value");
2034
+ }
2027
2035
  const res = await agent
2028
2036
  .patch(`/food/${spinach._id}`)
2029
- .set("If-Unmodified-Since", DateTime.fromISO("2025-06-15T11:59:59.999Z").toHTTP()!)
2037
+ .set("If-Unmodified-Since", ifUnmodifiedSince)
2030
2038
  .send({name: "Created Fallback"})
2031
2039
  .expect(409);
2032
2040
 
package/src/auth.ts CHANGED
@@ -54,7 +54,7 @@ export interface GenerateTokensOptions {
54
54
  sessionId?: string;
55
55
  }
56
56
 
57
- export function authenticateMiddleware(anonymous = false) {
57
+ export const authenticateMiddleware = (anonymous = false) => {
58
58
  const strategies = ["jwt"];
59
59
  if (anonymous) {
60
60
  strategies.push("anonymous");
@@ -70,14 +70,14 @@ export function authenticateMiddleware(anonymous = false) {
70
70
  }
71
71
  return passportAuth(req, res, next);
72
72
  };
73
- }
73
+ };
74
74
 
75
- export async function signupUser(
75
+ export const signupUser = async (
76
76
  userModel: UserModel,
77
77
  email: string,
78
78
  password: string,
79
79
  body?: Record<string, unknown>
80
- ) {
80
+ ) => {
81
81
  // Strip email and password from the body. They can cause mongoose to throw an error if strict is
82
82
  // set.
83
83
  const {email: _email, password: _password, ...bodyRest} = body ?? {};
@@ -100,7 +100,7 @@ export async function signupUser(
100
100
  const message = errorMessage(error);
101
101
  throw new APIError({title: message});
102
102
  }
103
- }
103
+ };
104
104
 
105
105
  /**
106
106
  * Generates both an access token (JWT) and a refresh token for a given user.
@@ -126,7 +126,7 @@ export const generateTokens = async (
126
126
  ) => {
127
127
  const tokenSecretOrKey = process.env.TOKEN_SECRET;
128
128
  if (!tokenSecretOrKey) {
129
- throw new Error("TOKEN_SECRET must be set in env.");
129
+ throw new APIError({status: 500, title: "TOKEN_SECRET must be set in env."});
130
130
  }
131
131
  const tokenUser = user as {_id?: ObjectId | string} | null | undefined;
132
132
  if (!tokenUser?._id) {
@@ -186,7 +186,7 @@ export const generateTokens = async (
186
186
  };
187
187
 
188
188
  // TODO allow customization
189
- export function setupAuth(app: express.Application, userModel: UserModel) {
189
+ export const setupAuth = (app: express.Application, userModel: UserModel): void => {
190
190
  passport.use(new AnonymousStrategy());
191
191
  passport.use(userModel.createStrategy());
192
192
  passport.use(
@@ -208,7 +208,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
208
208
  );
209
209
 
210
210
  if (!userModel.createStrategy) {
211
- throw new Error("setupAuth userModel must have .createStrategy()");
211
+ throw new APIError({status: 500, title: "setupAuth userModel must have .createStrategy()"});
212
212
  }
213
213
 
214
214
  const customTokenExtractor: JwtFromRequestFunction = (req) => {
@@ -228,7 +228,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
228
228
 
229
229
  const secretOrKey = process.env.TOKEN_SECRET;
230
230
  if (!secretOrKey) {
231
- throw new Error("TOKEN_SECRET must be set in env.");
231
+ throw new APIError({status: 500, title: "TOKEN_SECRET must be set in env."});
232
232
  }
233
233
  const jwtOpts: StrategyOptions = {
234
234
  issuer: process.env.TOKEN_ISSUER,
@@ -264,11 +264,11 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
264
264
 
265
265
  // Adds req.user to the request. This may wind up duplicating requests with passport,
266
266
  // but passport doesn't give us req.user early enough.
267
- async function decodeJWTMiddleware(
267
+ const decodeJWTMiddleware = async (
268
268
  req: express.Request,
269
269
  res: express.Response,
270
270
  next: express.NextFunction
271
- ) {
271
+ ) => {
272
272
  if (!process.env.TOKEN_SECRET) {
273
273
  return next();
274
274
  }
@@ -324,17 +324,17 @@ export function setupAuth(app: express.Application, userModel: UserModel) {
324
324
  }
325
325
  }
326
326
  return next();
327
- }
327
+ };
328
328
  app.use(decodeJWTMiddleware);
329
329
  // biome-ignore lint/suspicious/noExplicitAny: express 5 type for urlencoded doesn't match RequestHandler
330
330
  app.use(express.urlencoded({extended: false}) as any);
331
- }
331
+ };
332
332
 
333
- export function addAuthRoutes(
333
+ export const addAuthRoutes = (
334
334
  app: express.Application,
335
335
  userModel: UserModel,
336
336
  authOptions?: AuthOptions
337
- ): void {
337
+ ): void => {
338
338
  const router = express.Router();
339
339
  router.post("/login", async (req, res, next) => {
340
340
  passport.authenticate(
@@ -430,13 +430,13 @@ export function addAuthRoutes(
430
430
  }
431
431
  app.set("etag", false);
432
432
  app.use("/auth", router);
433
- }
433
+ };
434
434
 
435
- export function addMeRoutes(
435
+ export const addMeRoutes = (
436
436
  app: express.Application,
437
437
  userModel: UserModel,
438
438
  _authOptions?: AuthOptions
439
- ): void {
439
+ ): void => {
440
440
  const router = express.Router();
441
441
  router.get("/me", authenticateMiddleware(), async (req, res) => {
442
442
  if (!req.user?.id) {
@@ -483,4 +483,4 @@ export function addMeRoutes(
483
483
  app.set("etag", false);
484
484
  app.use("/auth", router);
485
485
  app.use(apiErrorMiddleware);
486
- }
486
+ };
@@ -140,4 +140,41 @@ describe("envConfigurationPlugin", () => {
140
140
  // mapToObject(undefined) returns {}, so Config falls back to the registered default
141
141
  expect(Config.get("TERRENO_PLUGIN_KEY")).toBe("fallback");
142
142
  });
143
+
144
+ it("mapToObject handles a plain Record (non-Map) env field", async () => {
145
+ // Insert a document with env as a plain object via raw DB operation
146
+ const col = mongoose.connection.db?.collection("testenvconfigs");
147
+ await col?.insertOne({env: {TERRENO_PLUGIN_KEY: "plainObj"}});
148
+
149
+ // Trigger refresh via findOneAndUpdate hook
150
+ await TestEnvConfig.findOneAndUpdate({}, {$set: {__v: 1}});
151
+
152
+ expect(Config.get("TERRENO_PLUGIN_KEY")).toBe("plainObj");
153
+ });
154
+
155
+ it("refreshFromDoc logs a warning and does not throw when the model query fails", async () => {
156
+ // Seed a document so findOneAndUpdate has a target
157
+ const doc = new TestEnvConfig();
158
+ doc.env.set("TERRENO_PLUGIN_KEY", "initial");
159
+ await doc.save();
160
+
161
+ // Pre-set cache to verify it is NOT overwritten when the hook errors
162
+ Config.setCachedEnv({TERRENO_PLUGIN_KEY: "cached"});
163
+
164
+ // Override Model.find to simulate a DB error inside refreshFromDoc
165
+ // (findOneOrNoneFor falls back to model.find when the findOneOrNone static is absent)
166
+ const originalFind = TestEnvConfig.find;
167
+ (TestEnvConfig as any).find = () => {
168
+ throw new Error("Simulated DB error");
169
+ };
170
+
171
+ // Trigger the post-findOneAndUpdate hook → refreshFromDoc → findOneOrNoneFor → throws
172
+ await TestEnvConfig.findOneAndUpdate({_id: doc._id}, {$set: {__v: 2}});
173
+
174
+ // Restore immediately
175
+ (TestEnvConfig as any).find = originalFind;
176
+
177
+ // The catch block should have swallowed the error; cache keeps old value
178
+ expect(Config.get("TERRENO_PLUGIN_KEY")).toBe("cached");
179
+ });
143
180
  });
@@ -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) {