@terreno/api 0.20.1 → 0.21.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 (65) hide show
  1. package/.ai/guidelines/core.md +71 -0
  2. package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
  3. package/README.md +54 -1
  4. package/dist/__tests__/versionCheckPlugin.test.js +29 -7
  5. package/dist/actions.openApi.test.js +13 -11
  6. package/dist/api.js +98 -11
  7. package/dist/api.query.test.js +31 -1
  8. package/dist/api.test.js +211 -0
  9. package/dist/auth.test.js +10 -10
  10. package/dist/betterAuth.d.ts +1 -1
  11. package/dist/consentApp.test.js +1 -0
  12. package/dist/example.js +4 -4
  13. package/dist/expressServer.d.ts +0 -22
  14. package/dist/expressServer.js +1 -125
  15. package/dist/expressServer.test.js +90 -91
  16. package/dist/githubAuth.test.js +22 -22
  17. package/dist/logger.d.ts +154 -0
  18. package/dist/logger.js +445 -26
  19. package/dist/logger.test.js +435 -0
  20. package/dist/middleware.d.ts +7 -0
  21. package/dist/middleware.js +58 -1
  22. package/dist/middleware.test.js +159 -0
  23. package/dist/openApi.test.js +10 -17
  24. package/dist/openApiBuilder.test.js +18 -10
  25. package/dist/realtime/changeStreamWatcher.d.ts +4 -4
  26. package/dist/realtime/changeStreamWatcher.js +2 -4
  27. package/dist/realtime/queryMatcher.d.ts +1 -1
  28. package/dist/realtime/queryMatcher.js +39 -14
  29. package/dist/realtime/types.d.ts +3 -3
  30. package/dist/requestContext.d.ts +61 -0
  31. package/dist/requestContext.js +74 -0
  32. package/dist/secretProviders.test.js +335 -0
  33. package/dist/terrenoApp.d.ts +27 -15
  34. package/dist/terrenoApp.js +24 -14
  35. package/dist/terrenoApp.test.js +52 -0
  36. package/dist/tests/bunSetup.js +61 -7
  37. package/dist/tests.js +27 -4
  38. package/package.json +1 -1
  39. package/src/__tests__/versionCheckPlugin.test.ts +43 -15
  40. package/src/actions.openApi.test.ts +12 -10
  41. package/src/api.query.test.ts +24 -1
  42. package/src/api.test.ts +169 -0
  43. package/src/api.ts +71 -0
  44. package/src/auth.test.ts +10 -10
  45. package/src/betterAuth.ts +1 -1
  46. package/src/consentApp.test.ts +1 -0
  47. package/src/example.ts +4 -4
  48. package/src/expressServer.test.ts +82 -85
  49. package/src/expressServer.ts +1 -213
  50. package/src/githubAuth.test.ts +22 -22
  51. package/src/logger.test.ts +466 -1
  52. package/src/logger.ts +477 -14
  53. package/src/middleware.test.ts +74 -2
  54. package/src/middleware.ts +57 -0
  55. package/src/openApi.test.ts +10 -17
  56. package/src/openApiBuilder.test.ts +18 -10
  57. package/src/realtime/changeStreamWatcher.ts +15 -10
  58. package/src/realtime/queryMatcher.ts +54 -27
  59. package/src/realtime/types.ts +4 -4
  60. package/src/requestContext.ts +86 -0
  61. package/src/secretProviders.test.ts +219 -1
  62. package/src/terrenoApp.test.ts +38 -0
  63. package/src/terrenoApp.ts +37 -15
  64. package/src/tests/bunSetup.ts +16 -3
  65. package/src/tests.ts +17 -4
package/dist/api.js CHANGED
@@ -173,9 +173,95 @@ exports.addPopulateToQuery = addPopulateToQuery;
173
173
  var PAGINATION_QUERY_PARAMS = ["limit", "page", "sort"];
174
174
  // Add support for more complex queries.
175
175
  var COMPLEX_QUERY_PARAMS = ["$and", "$or"];
176
+ /**
177
+ * Parses a date-range query bound from an ISO-8601 string using Luxon, throwing a 400
178
+ * APIError when the value cannot be parsed. Centralizes date-string parsing so the repo's
179
+ * Luxon convention is honored for admin changelist date-range filters.
180
+ */
181
+ var parseDateRangeBound = function (rawValue, queryKey) {
182
+ var parsed = luxon_1.DateTime.fromISO(String(rawValue), { zone: "utc" });
183
+ if (!parsed.isValid) {
184
+ throw new errors_1.APIError({
185
+ status: 400,
186
+ title: "Invalid date for query parameter ".concat(queryKey),
187
+ });
188
+ }
189
+ return parsed.toJSDate();
190
+ };
191
+ /**
192
+ * Collapses `field_gte` / `field_lte` query pairs into `{ field: { $gte, $lte } }` for Date paths,
193
+ * so admin changelist date-range filters map to valid Mongoose range queries.
194
+ */
195
+ var mergeDateRangeQueryParams = function (model, query) {
196
+ var e_2, _a, e_3, _b;
197
+ var schema = model.schema;
198
+ var result = __assign({}, query);
199
+ var dateRangeBases = new Set();
200
+ try {
201
+ for (var _c = __values(Object.keys(result)), _d = _c.next(); !_d.done; _d = _c.next()) {
202
+ var key = _d.value;
203
+ var match = /^(.+)_(gte|lte)$/.exec(key);
204
+ if (!match) {
205
+ continue;
206
+ }
207
+ var baseField = match[1];
208
+ var path = schema.path(baseField);
209
+ if (!path || path.instance !== "Date") {
210
+ continue;
211
+ }
212
+ dateRangeBases.add(baseField);
213
+ }
214
+ }
215
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
216
+ finally {
217
+ try {
218
+ if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
219
+ }
220
+ finally { if (e_2) throw e_2.error; }
221
+ }
222
+ try {
223
+ for (var dateRangeBases_1 = __values(dateRangeBases), dateRangeBases_1_1 = dateRangeBases_1.next(); !dateRangeBases_1_1.done; dateRangeBases_1_1 = dateRangeBases_1.next()) {
224
+ var baseField = dateRangeBases_1_1.value;
225
+ var gteKey = "".concat(baseField, "_gte");
226
+ var lteKey = "".concat(baseField, "_lte");
227
+ var gteRaw = result[gteKey];
228
+ var lteRaw = result[lteKey];
229
+ var bounds = {};
230
+ if (gteRaw !== undefined && gteRaw !== null && String(gteRaw).trim() !== "") {
231
+ bounds.$gte = parseDateRangeBound(gteRaw, gteKey);
232
+ }
233
+ if (lteRaw !== undefined && lteRaw !== null && String(lteRaw).trim() !== "") {
234
+ bounds.$lte = parseDateRangeBound(lteRaw, lteKey);
235
+ }
236
+ if (Object.keys(bounds).length === 0) {
237
+ continue;
238
+ }
239
+ delete result[gteKey];
240
+ delete result[lteKey];
241
+ var direct = result[baseField];
242
+ if (direct !== undefined && direct !== null && typeof direct !== "object") {
243
+ delete result[baseField];
244
+ }
245
+ if (typeof direct === "object" && direct !== null && !Array.isArray(direct)) {
246
+ result[baseField] = __assign(__assign({}, direct), bounds);
247
+ }
248
+ else {
249
+ result[baseField] = bounds;
250
+ }
251
+ }
252
+ }
253
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
254
+ finally {
255
+ try {
256
+ if (dateRangeBases_1_1 && !dateRangeBases_1_1.done && (_b = dateRangeBases_1.return)) _b.call(dateRangeBases_1);
257
+ }
258
+ finally { if (e_3) throw e_3.error; }
259
+ }
260
+ return result;
261
+ };
176
262
  // Ensures query params are allowed. Also checks nested query params when using $and/$or.
177
263
  var checkQueryParamAllowed = function (queryParam, queryParamValue, queryFields) {
178
- var e_2, _a, e_3, _b;
264
+ var e_4, _a, e_5, _b;
179
265
  if (queryFields === void 0) { queryFields = []; }
180
266
  // Cast for iteration through complex query values
181
267
  var complexValue = queryParamValue;
@@ -187,26 +273,26 @@ var checkQueryParamAllowed = function (queryParam, queryParamValue, queryFields)
187
273
  for (var complexValue_1 = __values(complexValue), complexValue_1_1 = complexValue_1.next(); !complexValue_1_1.done; complexValue_1_1 = complexValue_1.next()) {
188
274
  var subQuery = complexValue_1_1.value;
189
275
  try {
190
- for (var _c = (e_3 = void 0, __values(Object.keys(subQuery))), _d = _c.next(); !_d.done; _d = _c.next()) {
276
+ for (var _c = (e_5 = void 0, __values(Object.keys(subQuery))), _d = _c.next(); !_d.done; _d = _c.next()) {
191
277
  var subKey = _d.value;
192
278
  checkQueryParamAllowed(subKey, subQuery[subKey], queryFields);
193
279
  }
194
280
  }
195
- catch (e_3_1) { e_3 = { error: e_3_1 }; }
281
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
196
282
  finally {
197
283
  try {
198
284
  if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
199
285
  }
200
- finally { if (e_3) throw e_3.error; }
286
+ finally { if (e_5) throw e_5.error; }
201
287
  }
202
288
  }
203
289
  }
204
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
290
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
205
291
  finally {
206
292
  try {
207
293
  if (complexValue_1_1 && !complexValue_1_1.done && (_a = complexValue_1.return)) _a.call(complexValue_1);
208
294
  }
209
- finally { if (e_2) throw e_2.error; }
295
+ finally { if (e_4) throw e_4.error; }
210
296
  }
211
297
  return;
212
298
  }
@@ -489,7 +575,7 @@ function _buildModelRouter(model, options) {
489
575
  queryValidation,
490
576
  ], (0, exports.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
491
577
  var query, _a, _b, queryParam, _c, _d, queryParam, queryFilter, error_6, limit, builtQuery, total, populatedQuery, data, error_7, serialized, error_8, more, msg;
492
- var e_4, _e, e_5, _f;
578
+ var e_6, _e, e_7, _f;
493
579
  var _g, _h, _j, _k, _l, _m;
494
580
  return __generator(this, function (_o) {
495
581
  switch (_o.label) {
@@ -501,12 +587,12 @@ function _buildModelRouter(model, options) {
501
587
  query[queryParam] = (_h = options.defaultQueryParams) === null || _h === void 0 ? void 0 : _h[queryParam];
502
588
  }
503
589
  }
504
- catch (e_4_1) { e_4 = { error: e_4_1 }; }
590
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
505
591
  finally {
506
592
  try {
507
593
  if (_b && !_b.done && (_e = _a.return)) _e.call(_a);
508
594
  }
509
- finally { if (e_4) throw e_4.error; }
595
+ finally { if (e_6) throw e_6.error; }
510
596
  }
511
597
  try {
512
598
  for (_c = __values(Object.keys(req.query)), _d = _c.next(); !_d.done; _d = _c.next()) {
@@ -527,13 +613,14 @@ function _buildModelRouter(model, options) {
527
613
  }
528
614
  }
529
615
  }
530
- catch (e_5_1) { e_5 = { error: e_5_1 }; }
616
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
531
617
  finally {
532
618
  try {
533
619
  if (_d && !_d.done && (_f = _c.return)) _f.call(_c);
534
620
  }
535
- finally { if (e_5) throw e_5.error; }
621
+ finally { if (e_7) throw e_7.error; }
536
622
  }
623
+ query = mergeDateRangeQueryParams(model, query);
537
624
  // Special operators. NOTE: these request Mongo Atlas.
538
625
  if (req.query.$search) {
539
626
  (_j = mongoose_1.default.connection.db) === null || _j === void 0 ? void 0 : _j.collection(model.collection.collectionName);
@@ -184,7 +184,17 @@ var tests_1 = require("./tests");
184
184
  update: [permissions_1.Permissions.IsOwner],
185
185
  },
186
186
  populatePaths: [{ path: "ownerId" }],
187
- queryFields: ["hidden", "name", "calories", "created", "source.name", "tags", "eatenBy"],
187
+ queryFields: [
188
+ "hidden",
189
+ "name",
190
+ "calories",
191
+ "created",
192
+ "created_gte",
193
+ "created_lte",
194
+ "source.name",
195
+ "tags",
196
+ "eatenBy",
197
+ ],
188
198
  sort: { created: "descending" },
189
199
  }));
190
200
  server = (0, supertest_1.default)(app);
@@ -344,6 +354,26 @@ var tests_1 = require("./tests");
344
354
  }
345
355
  });
346
356
  }); });
357
+ (0, bun_test_1.it)("list applies created_gte and created_lte as a Date range", function () { return __awaiter(void 0, void 0, void 0, function () {
358
+ var res, names;
359
+ return __generator(this, function (_a) {
360
+ switch (_a.label) {
361
+ case 0: return [4 /*yield*/, agent
362
+ .get("/food")
363
+ .query({
364
+ created_gte: "2021-12-03T00:00:05.000Z",
365
+ created_lte: "2021-12-03T00:00:25.000Z",
366
+ limit: 10,
367
+ })
368
+ .expect(200)];
369
+ case 1:
370
+ res = _a.sent();
371
+ names = res.body.data.map(function (d) { return d.name; }).sort();
372
+ (0, bun_test_1.expect)(names).toEqual(["Pizza", "Spinach"]);
373
+ return [2 /*return*/];
374
+ }
375
+ });
376
+ }); });
347
377
  (0, bun_test_1.it)("list query params not in list", function () { return __awaiter(void 0, void 0, void 0, function () {
348
378
  var res;
349
379
  return __generator(this, function (_a) {
package/dist/api.test.js CHANGED
@@ -2588,5 +2588,216 @@ var transformers_1 = require("./transformers");
2588
2588
  }
2589
2589
  });
2590
2590
  }); });
2591
+ (0, bun_test_1.it)("returns 400 when X-Unmodified-Since-ISO header is an invalid date", function () { return __awaiter(void 0, void 0, void 0, function () {
2592
+ var res;
2593
+ return __generator(this, function (_a) {
2594
+ switch (_a.label) {
2595
+ case 0: return [4 /*yield*/, agent
2596
+ .patch("/food/".concat(spinach._id))
2597
+ .set("X-Unmodified-Since-ISO", "not-a-date")
2598
+ .send({ name: "Bad Date" })
2599
+ .expect(400)];
2600
+ case 1:
2601
+ res = _a.sent();
2602
+ (0, bun_test_1.expect)(res.body.title).toBe("Invalid conflict-detection timestamp");
2603
+ (0, bun_test_1.expect)(res.body.detail).toContain("X-Unmodified-Since-ISO");
2604
+ return [2 /*return*/];
2605
+ }
2606
+ });
2607
+ }); });
2608
+ (0, bun_test_1.it)("returns 400 when If-Unmodified-Since header is an invalid HTTP date", function () { return __awaiter(void 0, void 0, void 0, function () {
2609
+ var res;
2610
+ return __generator(this, function (_a) {
2611
+ switch (_a.label) {
2612
+ case 0: return [4 /*yield*/, agent
2613
+ .patch("/food/".concat(spinach._id))
2614
+ .set("If-Unmodified-Since", "not-a-valid-http-date")
2615
+ .send({ name: "Bad HTTP Date" })
2616
+ .expect(400)];
2617
+ case 1:
2618
+ res = _a.sent();
2619
+ (0, bun_test_1.expect)(res.body.title).toBe("Invalid conflict-detection timestamp");
2620
+ (0, bun_test_1.expect)(res.body.detail).toContain("If-Unmodified-Since");
2621
+ return [2 /*return*/];
2622
+ }
2623
+ });
2624
+ }); });
2625
+ (0, bun_test_1.it)("returns 400 when _updatedAt body field is an invalid date", function () { return __awaiter(void 0, void 0, void 0, function () {
2626
+ var res;
2627
+ return __generator(this, function (_a) {
2628
+ switch (_a.label) {
2629
+ case 0: return [4 /*yield*/, agent
2630
+ .patch("/food/".concat(spinach._id))
2631
+ .send({ _updatedAt: "garbage-date", name: "Bad Body Date" })
2632
+ .expect(400)];
2633
+ case 1:
2634
+ res = _a.sent();
2635
+ (0, bun_test_1.expect)(res.body.title).toBe("Invalid conflict-detection timestamp");
2636
+ (0, bun_test_1.expect)(res.body.detail).toContain("_updatedAt");
2637
+ return [2 /*return*/];
2638
+ }
2639
+ });
2640
+ }); });
2641
+ (0, bun_test_1.it)("handles doc timestamp stored as ISO string", function () { return __awaiter(void 0, void 0, void 0, function () {
2642
+ var res;
2643
+ return __generator(this, function (_a) {
2644
+ switch (_a.label) {
2645
+ case 0: return [4 /*yield*/, tests_1.FoodModel.collection.updateOne({ _id: spinach._id }, { $set: { updated: "2025-06-15T12:00:00.000Z" } })];
2646
+ case 1:
2647
+ _a.sent();
2648
+ return [4 /*yield*/, agent
2649
+ .patch("/food/".concat(spinach._id))
2650
+ .set("If-Unmodified-Since", luxon_1.DateTime.fromISO("2025-06-15T11:00:00.000Z").toHTTP())
2651
+ .send({ name: "String Timestamp" })
2652
+ .expect(409)];
2653
+ case 2:
2654
+ res = _a.sent();
2655
+ (0, bun_test_1.expect)(res.body.error).toBe("Conflict");
2656
+ return [2 /*return*/];
2657
+ }
2658
+ });
2659
+ }); });
2660
+ });
2661
+ (0, bun_test_1.describe)("three-arg modelRouter registration", function () {
2662
+ (0, bun_test_1.it)("returns a ModelRouterRegistration with path and realtime", function () {
2663
+ var registration = (0, api_1.modelRouter)("/food", tests_1.FoodModel, {
2664
+ permissions: {
2665
+ create: [permissions_1.Permissions.IsAny],
2666
+ delete: [permissions_1.Permissions.IsAny],
2667
+ list: [permissions_1.Permissions.IsAny],
2668
+ read: [permissions_1.Permissions.IsAny],
2669
+ update: [permissions_1.Permissions.IsAny],
2670
+ },
2671
+ realtime: { methods: ["create", "update", "delete"], roomStrategy: "model" },
2672
+ });
2673
+ (0, bun_test_1.expect)(registration).toHaveProperty("__type", "modelRouter");
2674
+ (0, bun_test_1.expect)(registration).toHaveProperty("path", "/food");
2675
+ (0, bun_test_1.expect)(registration).toHaveProperty("router");
2676
+ (0, bun_test_1.expect)(registration).toHaveProperty("_buildWithOpenApi");
2677
+ });
2678
+ (0, bun_test_1.it)("logs a warning when realtime config is used without the path form", function () {
2679
+ var router = (0, api_1.modelRouter)(tests_1.FoodModel, {
2680
+ permissions: {
2681
+ create: [permissions_1.Permissions.IsAny],
2682
+ delete: [permissions_1.Permissions.IsAny],
2683
+ list: [permissions_1.Permissions.IsAny],
2684
+ read: [permissions_1.Permissions.IsAny],
2685
+ update: [permissions_1.Permissions.IsAny],
2686
+ },
2687
+ realtime: { methods: ["create", "update", "delete"], roomStrategy: "model" },
2688
+ });
2689
+ // Returns a plain Router, not a registration, because no path was given
2690
+ (0, bun_test_1.expect)(router).toBeDefined();
2691
+ (0, bun_test_1.expect)(router).not.toHaveProperty("__type");
2692
+ });
2693
+ });
2694
+ (0, bun_test_1.describe)("create transform error wrapping", function () {
2695
+ var admin;
2696
+ var agent;
2697
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
2698
+ var _a;
2699
+ return __generator(this, function (_b) {
2700
+ switch (_b.label) {
2701
+ case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
2702
+ case 1:
2703
+ _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
2704
+ app = (0, tests_1.getBaseServer)();
2705
+ (0, auth_1.setupAuth)(app, tests_1.UserModel);
2706
+ (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
2707
+ app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
2708
+ permissions: {
2709
+ create: [permissions_1.Permissions.IsAny],
2710
+ delete: [permissions_1.Permissions.IsAny],
2711
+ list: [permissions_1.Permissions.IsAny],
2712
+ read: [permissions_1.Permissions.IsAny],
2713
+ update: [permissions_1.Permissions.IsAny],
2714
+ },
2715
+ transformer: {
2716
+ transform: function () {
2717
+ throw new Error("generic transform error");
2718
+ },
2719
+ },
2720
+ }));
2721
+ server = (0, supertest_1.default)(app);
2722
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
2723
+ case 2:
2724
+ agent = _b.sent();
2725
+ return [2 /*return*/];
2726
+ }
2727
+ });
2728
+ }); });
2729
+ (0, bun_test_1.it)("wraps non-APIError transform errors in a 400 APIError on create", function () { return __awaiter(void 0, void 0, void 0, function () {
2730
+ var res;
2731
+ return __generator(this, function (_a) {
2732
+ switch (_a.label) {
2733
+ case 0: return [4 /*yield*/, agent
2734
+ .post("/food")
2735
+ .send({ calories: 10, name: "New Food", ownerId: admin._id })
2736
+ .expect(400)];
2737
+ case 1:
2738
+ res = _a.sent();
2739
+ (0, bun_test_1.expect)(res.body.title).toContain("generic transform error");
2740
+ return [2 /*return*/];
2741
+ }
2742
+ });
2743
+ }); });
2744
+ });
2745
+ (0, bun_test_1.describe)("update transform error wrapping", function () {
2746
+ var admin;
2747
+ var agent;
2748
+ var spinach;
2749
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
2750
+ var _a;
2751
+ return __generator(this, function (_b) {
2752
+ switch (_b.label) {
2753
+ case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
2754
+ case 1:
2755
+ _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
2756
+ return [4 /*yield*/, tests_1.FoodModel.create({
2757
+ calories: 10,
2758
+ created: new Date(),
2759
+ hidden: false,
2760
+ name: "Spinach",
2761
+ ownerId: admin._id,
2762
+ })];
2763
+ case 2:
2764
+ spinach = _b.sent();
2765
+ app = (0, tests_1.getBaseServer)();
2766
+ (0, auth_1.setupAuth)(app, tests_1.UserModel);
2767
+ (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
2768
+ app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
2769
+ permissions: {
2770
+ create: [permissions_1.Permissions.IsAny],
2771
+ delete: [permissions_1.Permissions.IsAny],
2772
+ list: [permissions_1.Permissions.IsAny],
2773
+ read: [permissions_1.Permissions.IsAny],
2774
+ update: [permissions_1.Permissions.IsAny],
2775
+ },
2776
+ transformer: {
2777
+ transform: function () {
2778
+ throw new Error("update transform error");
2779
+ },
2780
+ },
2781
+ }));
2782
+ server = (0, supertest_1.default)(app);
2783
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
2784
+ case 3:
2785
+ agent = _b.sent();
2786
+ return [2 /*return*/];
2787
+ }
2788
+ });
2789
+ }); });
2790
+ (0, bun_test_1.it)("wraps non-APIError transform errors in a 403 APIError on update", function () { return __awaiter(void 0, void 0, void 0, function () {
2791
+ var res;
2792
+ return __generator(this, function (_a) {
2793
+ switch (_a.label) {
2794
+ case 0: return [4 /*yield*/, agent.patch("/food/".concat(spinach._id)).send({ name: "Updated" }).expect(403)];
2795
+ case 1:
2796
+ res = _a.sent();
2797
+ (0, bun_test_1.expect)(res.body.title).toContain("update transform error");
2798
+ return [2 /*return*/];
2799
+ }
2800
+ });
2801
+ }); });
2591
2802
  });
2592
2803
  });
package/dist/auth.test.js CHANGED
@@ -104,9 +104,9 @@ var bun_test_1 = require("bun:test");
104
104
  var supertest_1 = __importDefault(require("supertest"));
105
105
  var api_1 = require("./api");
106
106
  var auth_1 = require("./auth");
107
- var expressServer_1 = require("./expressServer");
108
107
  var permissions_1 = require("./permissions");
109
108
  var requestContext_1 = require("./requestContext");
109
+ var terrenoApp_1 = require("./terrenoApp");
110
110
  var tests_1 = require("./tests");
111
111
  var transformers_1 = require("./transformers");
112
112
  var utils_1 = require("./utils");
@@ -236,11 +236,11 @@ var decodeTokenPayload = function (token) {
236
236
  ])];
237
237
  case 2:
238
238
  _b.sent();
239
- app = (0, expressServer_1.setupServer)({
240
- addRoutes: addRoutes,
239
+ app = new terrenoApp_1.TerrenoApp({
240
+ configureApp: addRoutes,
241
241
  skipListen: true,
242
242
  userModel: tests_1.UserModel,
243
- });
243
+ }).build();
244
244
  agent = supertest_1.default.agent(app);
245
245
  return [2 /*return*/];
246
246
  }
@@ -1158,11 +1158,11 @@ var decodeTokenPayload = function (token) {
1158
1158
  return [4 /*yield*/, (0, tests_1.setupDb)()];
1159
1159
  case 1:
1160
1160
  _a.sent();
1161
- app = (0, expressServer_1.setupServer)({
1162
- addRoutes: function () { },
1161
+ app = new terrenoApp_1.TerrenoApp({
1162
+ configureApp: function () { },
1163
1163
  skipListen: true,
1164
1164
  userModel: tests_1.UserModel,
1165
- });
1165
+ }).build();
1166
1166
  agent = supertest_1.default.agent(app);
1167
1167
  return [2 /*return*/];
1168
1168
  }
@@ -1268,11 +1268,11 @@ var decodeTokenPayload = function (token) {
1268
1268
  return [4 /*yield*/, (0, tests_1.setupDb)()];
1269
1269
  case 1:
1270
1270
  _a.sent();
1271
- app = (0, expressServer_1.setupServer)({
1272
- addRoutes: function () { },
1271
+ app = new terrenoApp_1.TerrenoApp({
1272
+ configureApp: function () { },
1273
1273
  skipListen: true,
1274
1274
  userModel: tests_1.UserModel,
1275
- });
1275
+ }).build();
1276
1276
  agent = supertest_1.default.agent(app);
1277
1277
  return [2 /*return*/];
1278
1278
  }
@@ -53,7 +53,7 @@ export interface BetterAuthConfig {
53
53
  baseURL?: string;
54
54
  }
55
55
  /**
56
- * Auth provider selection for setupServer.
56
+ * Auth provider selection for TerrenoApp.
57
57
  * - "jwt": Traditional JWT/Passport authentication (default)
58
58
  * - "better-auth": Better Auth with OAuth support
59
59
  */
@@ -118,6 +118,7 @@ var buildApp = function (consentAppOptions) {
118
118
  case 1:
119
119
  res = _a.sent();
120
120
  (0, bun_test_1.expect)(res.body.data).toHaveLength(0);
121
+ (0, bun_test_1.expect)(res.body.requestId).toBe(res.headers["x-request-id"]);
121
122
  return [2 /*return*/];
122
123
  }
123
124
  });
package/dist/example.js CHANGED
@@ -52,10 +52,10 @@ var mongoose_1 = __importStar(require("mongoose"));
52
52
  var passport_local_mongoose_1 = __importDefault(require("passport-local-mongoose"));
53
53
  var api_1 = require("./api");
54
54
  var auth_1 = require("./auth");
55
- var expressServer_1 = require("./expressServer");
56
55
  var logger_1 = require("./logger");
57
56
  var permissions_1 = require("./permissions");
58
57
  var plugins_1 = require("./plugins");
58
+ var terrenoApp_1 = require("./terrenoApp");
59
59
  mongoose_1.default
60
60
  .connect("mongodb://localhost:27017/example")
61
61
  .then(function () {
@@ -119,12 +119,12 @@ var getBaseServer = function () {
119
119
  update: [permissions_1.Permissions.IsOwner],
120
120
  }, queryFields: ["name", "calories", "created", "ownerId", "hidden"] })));
121
121
  };
122
- return (0, expressServer_1.setupServer)({
123
- addRoutes: addRoutes,
122
+ return new terrenoApp_1.TerrenoApp({
123
+ configureApp: addRoutes,
124
124
  loggingOptions: {
125
125
  level: "debug",
126
126
  },
127
127
  userModel: UserModel,
128
- });
128
+ }).build();
129
129
  };
130
130
  getBaseServer();
@@ -1,10 +1,6 @@
1
- import * as Sentry from "@sentry/bun";
2
1
  import express, { type Router } from "express";
3
2
  import type jwt from "jsonwebtoken";
4
3
  import type { ModelRouterOptions } from "./api";
5
- import { type UserModel as UserMongooseModel } from "./auth";
6
- import { type GitHubAuthOptions } from "./githubAuth";
7
- import { type LoggingOptions } from "./logger";
8
4
  export declare const setupEnvironment: () => void;
9
5
  export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<unknown>>) => void;
10
6
  export declare const logRequests: (req: any, res: any, next: express.NextFunction) => void;
@@ -15,24 +11,6 @@ export interface AuthOptions {
15
11
  generateTokenExpiration?: (user: any) => number | jwt.SignOptions["expiresIn"];
16
12
  generateRefreshTokenExpiration?: (user: any) => number | jwt.SignOptions["expiresIn"];
17
13
  }
18
- export interface SetupServerOptions {
19
- userModel: UserMongooseModel;
20
- addRoutes: AddRoutes;
21
- loggingOptions?: LoggingOptions;
22
- logRequests?: boolean;
23
- authOptions?: AuthOptions;
24
- /**
25
- * GitHub OAuth configuration. When provided, enables GitHub authentication.
26
- * Requires the user schema to have GitHub fields (use githubUserPlugin).
27
- */
28
- githubAuth?: GitHubAuthOptions;
29
- skipListen?: boolean;
30
- corsOrigin?: string | boolean | RegExp | Array<boolean | string | RegExp> | ((requestOrigin: string | undefined, callback: (err: Error | null, origin?: boolean | string | RegExp | Array<boolean | string | RegExp>) => void) => void);
31
- addMiddleware?: AddRoutes;
32
- ignoreTraces?: string[];
33
- sentryOptions?: Sentry.BunOptions;
34
- }
35
- export declare const setupServer: (options: SetupServerOptions) => express.Application;
36
14
  export declare const cronjob: (name: string, schedule: "hourly" | "minutely" | string, callback: () => void) => void;
37
15
  export interface WrapScriptOptions {
38
16
  onFinish?: (result?: unknown) => void | Promise<void>;