@terreno/api 0.15.1 → 0.16.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.
@@ -0,0 +1,946 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ var __read = (this && this.__read) || function (o, n) {
50
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
51
+ if (!m) return o;
52
+ var i = m.call(o), r, ar = [], e;
53
+ try {
54
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
55
+ }
56
+ catch (error) { e = { error: error }; }
57
+ finally {
58
+ try {
59
+ if (r && !r.done && (m = i["return"])) m.call(i);
60
+ }
61
+ finally { if (e) throw e.error; }
62
+ }
63
+ return ar;
64
+ };
65
+ var __importDefault = (this && this.__importDefault) || function (mod) {
66
+ return (mod && mod.__esModule) ? mod : { "default": mod };
67
+ };
68
+ Object.defineProperty(exports, "__esModule", { value: true });
69
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
70
+ var bun_test_1 = require("bun:test");
71
+ var mongoose_1 = require("mongoose");
72
+ var supertest_1 = __importDefault(require("supertest"));
73
+ var zod_1 = require("zod");
74
+ var actions_1 = require("./actions");
75
+ var api_1 = require("./api");
76
+ var auth_1 = require("./auth");
77
+ var errors_1 = require("./errors");
78
+ var permissions_1 = require("./permissions");
79
+ var plugins_1 = require("./plugins");
80
+ var tests_1 = require("./tests");
81
+ var stuffSchema = new mongoose_1.Schema({
82
+ name: { type: String },
83
+ ownerId: { type: String },
84
+ });
85
+ stuffSchema.plugin(plugins_1.isDeletedPlugin);
86
+ var StuffModel = (0, mongoose_1.model)("ActionStuff", stuffSchema);
87
+ var allPermissions = {
88
+ create: [permissions_1.Permissions.IsAny],
89
+ delete: [permissions_1.Permissions.IsAny],
90
+ list: [permissions_1.Permissions.IsAny],
91
+ read: [permissions_1.Permissions.IsAny],
92
+ update: [permissions_1.Permissions.IsAny],
93
+ };
94
+ (0, bun_test_1.describe)("modelRouter actions", function () {
95
+ (0, bun_test_1.describe)("registration validation", function () {
96
+ (0, bun_test_1.it)("throws when permissions are missing", function () {
97
+ (0, bun_test_1.expect)(function () {
98
+ return (0, api_1.modelRouter)(tests_1.FoodModel, {
99
+ collectionActions: {
100
+ broken: {
101
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
102
+ return [2 /*return*/, ({ ok: true })];
103
+ }); }); },
104
+ method: "POST",
105
+ },
106
+ },
107
+ permissions: allPermissions,
108
+ });
109
+ }).toThrow(/missing required "permissions"/);
110
+ });
111
+ (0, bun_test_1.it)("rejects single-character action names by design", function () {
112
+ (0, bun_test_1.expect)(function () {
113
+ return (0, api_1.modelRouter)(tests_1.FoodModel, {
114
+ collectionActions: {
115
+ a: {
116
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
117
+ return [2 /*return*/, ({})];
118
+ }); }); },
119
+ method: "GET",
120
+ permissions: [permissions_1.Permissions.IsAny],
121
+ },
122
+ },
123
+ permissions: allPermissions,
124
+ });
125
+ }).toThrow(actions_1.ACTION_NAME_PATTERN.toString());
126
+ });
127
+ (0, bun_test_1.it)("throws on invalid action name", function () {
128
+ (0, bun_test_1.expect)(function () {
129
+ return (0, api_1.modelRouter)(tests_1.FoodModel, {
130
+ collectionActions: {
131
+ "foo*bar": {
132
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
133
+ return [2 /*return*/, ({})];
134
+ }); }); },
135
+ method: "GET",
136
+ permissions: [permissions_1.Permissions.IsAny],
137
+ },
138
+ },
139
+ permissions: allPermissions,
140
+ });
141
+ }).toThrow(actions_1.ACTION_NAME_PATTERN.toString());
142
+ });
143
+ (0, bun_test_1.it)("throws on empty action name", function () {
144
+ (0, bun_test_1.expect)(function () {
145
+ return (0, api_1.modelRouter)(tests_1.FoodModel, {
146
+ collectionActions: {
147
+ "": {
148
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
149
+ return [2 /*return*/, ({})];
150
+ }); }); },
151
+ method: "GET",
152
+ permissions: [permissions_1.Permissions.IsAny],
153
+ },
154
+ },
155
+ permissions: allPermissions,
156
+ });
157
+ }).toThrow("Action name cannot be empty");
158
+ });
159
+ (0, bun_test_1.it)("throws when instance action collides with array field", function () {
160
+ (0, bun_test_1.expect)(function () {
161
+ return (0, api_1.modelRouter)(tests_1.FoodModel, {
162
+ instanceActions: {
163
+ tags: {
164
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
165
+ return [2 /*return*/, ({})];
166
+ }); }); },
167
+ method: "GET",
168
+ permissions: [permissions_1.Permissions.IsAny],
169
+ },
170
+ },
171
+ permissions: allPermissions,
172
+ });
173
+ }).toThrow(/collides with array field/);
174
+ });
175
+ (0, bun_test_1.it)("allows same action name on instance and collection scopes", function () {
176
+ (0, bun_test_1.expect)(function () {
177
+ return (0, api_1.modelRouter)(tests_1.FoodModel, {
178
+ collectionActions: {
179
+ sync: {
180
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
181
+ return [2 /*return*/, ({ scope: "collection" })];
182
+ }); }); },
183
+ method: "POST",
184
+ permissions: [permissions_1.Permissions.IsAny],
185
+ },
186
+ },
187
+ instanceActions: {
188
+ sync: {
189
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
190
+ return [2 /*return*/, ({ scope: "instance" })];
191
+ }); }); },
192
+ method: "POST",
193
+ permissions: [permissions_1.Permissions.IsAny],
194
+ },
195
+ },
196
+ permissions: allPermissions,
197
+ });
198
+ }).not.toThrow();
199
+ });
200
+ });
201
+ (0, bun_test_1.describe)("integration", function () {
202
+ var app;
203
+ var server;
204
+ var admin;
205
+ var notAdmin;
206
+ var spinach;
207
+ var mountFoodRouter = function (options) {
208
+ app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, __assign({ allowAnonymous: true }, options)));
209
+ if (!app.get("terrenoUnauthorizedMiddleware")) {
210
+ app.use(errors_1.apiUnauthorizedMiddleware);
211
+ app.set("terrenoUnauthorizedMiddleware", true);
212
+ }
213
+ };
214
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
215
+ var _a, _b;
216
+ return __generator(this, function (_c) {
217
+ switch (_c.label) {
218
+ case 0:
219
+ process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
220
+ return [4 /*yield*/, (0, tests_1.setupDb)()];
221
+ case 1:
222
+ _a = __read.apply(void 0, [_c.sent(), 2]), admin = _a[0], notAdmin = _a[1];
223
+ return [4 /*yield*/, Promise.all([
224
+ tests_1.FoodModel.create({
225
+ calories: 1,
226
+ created: new Date(),
227
+ hidden: false,
228
+ name: "Spinach",
229
+ ownerId: notAdmin._id,
230
+ source: { name: "test" },
231
+ }),
232
+ ])];
233
+ case 2:
234
+ _b = __read.apply(void 0, [_c.sent(), 1]), spinach = _b[0];
235
+ app = (0, tests_1.getBaseServer)();
236
+ (0, auth_1.setupAuth)(app, tests_1.UserModel);
237
+ (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
238
+ server = (0, supertest_1.default)(app);
239
+ return [2 /*return*/];
240
+ }
241
+ });
242
+ }); });
243
+ (0, bun_test_1.describe)("routing and permissions", function () {
244
+ (0, bun_test_1.it)("allows empty permissions array and returns 405 at runtime", function () { return __awaiter(void 0, void 0, void 0, function () {
245
+ var agent, res;
246
+ return __generator(this, function (_a) {
247
+ switch (_a.label) {
248
+ case 0:
249
+ mountFoodRouter({
250
+ collectionActions: {
251
+ disabled: {
252
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
253
+ return [2 /*return*/, ({ ok: true })];
254
+ }); }); },
255
+ method: "POST",
256
+ permissions: [],
257
+ },
258
+ },
259
+ permissions: allPermissions,
260
+ });
261
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
262
+ case 1:
263
+ agent = _a.sent();
264
+ return [4 /*yield*/, agent.post("/food/disabled").send({}).expect(405)];
265
+ case 2:
266
+ res = _a.sent();
267
+ (0, bun_test_1.expect)(res.body.title).toContain("Access to CREATE on Food denied");
268
+ return [2 /*return*/];
269
+ }
270
+ });
271
+ }); });
272
+ (0, bun_test_1.it)("runs instance POST action with ctx.doc and req.obj", function () { return __awaiter(void 0, void 0, void 0, function () {
273
+ var seenDoc, seenObj, res;
274
+ return __generator(this, function (_a) {
275
+ switch (_a.label) {
276
+ case 0:
277
+ mountFoodRouter({
278
+ instanceActions: {
279
+ mark: {
280
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
281
+ var doc = _b.doc, req = _b.req;
282
+ return __generator(this, function (_c) {
283
+ seenDoc = doc;
284
+ seenObj = req.obj;
285
+ return [2 /*return*/, { marked: true }];
286
+ });
287
+ }); },
288
+ method: "POST",
289
+ permissions: [permissions_1.Permissions.IsAny],
290
+ },
291
+ },
292
+ permissions: allPermissions,
293
+ });
294
+ return [4 /*yield*/, server.post("/food/".concat(spinach._id, "/mark")).send({}).expect(200)];
295
+ case 1:
296
+ res = _a.sent();
297
+ (0, bun_test_1.expect)(res.body.data).toEqual({ marked: true });
298
+ (0, bun_test_1.expect)(seenDoc === null || seenDoc === void 0 ? void 0 : seenDoc._id.toString()).toBe(spinach._id.toString());
299
+ (0, bun_test_1.expect)(seenObj === null || seenObj === void 0 ? void 0 : seenObj._id.toString()).toBe(spinach._id.toString());
300
+ return [2 /*return*/];
301
+ }
302
+ });
303
+ }); });
304
+ (0, bun_test_1.it)("runs collection POST action without doc", function () { return __awaiter(void 0, void 0, void 0, function () {
305
+ var res;
306
+ return __generator(this, function (_a) {
307
+ switch (_a.label) {
308
+ case 0:
309
+ mountFoodRouter({
310
+ collectionActions: {
311
+ bulk: {
312
+ handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
313
+ return __generator(this, function (_a) {
314
+ (0, bun_test_1.expect)(ctx.doc).toBeUndefined();
315
+ return [2 /*return*/, { count: 1 }];
316
+ });
317
+ }); },
318
+ method: "POST",
319
+ permissions: [permissions_1.Permissions.IsAny],
320
+ },
321
+ },
322
+ permissions: allPermissions,
323
+ });
324
+ return [4 /*yield*/, server.post("/food/bulk").send({}).expect(200)];
325
+ case 1:
326
+ res = _a.sent();
327
+ (0, bun_test_1.expect)(res.body.data).toEqual({ count: 1 });
328
+ return [2 /*return*/];
329
+ }
330
+ });
331
+ }); });
332
+ (0, bun_test_1.it)("runs GET instance and collection actions", function () { return __awaiter(void 0, void 0, void 0, function () {
333
+ var collectionRes, instanceRes;
334
+ return __generator(this, function (_a) {
335
+ switch (_a.label) {
336
+ case 0:
337
+ mountFoodRouter({
338
+ collectionActions: {
339
+ stats: {
340
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
341
+ return [2 /*return*/, ({ total: 1 })];
342
+ }); }); },
343
+ method: "GET",
344
+ permissions: [permissions_1.Permissions.IsAny],
345
+ },
346
+ },
347
+ instanceActions: {
348
+ peek: {
349
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
350
+ var doc = _b.doc;
351
+ return __generator(this, function (_c) {
352
+ return [2 /*return*/, ({ name: doc === null || doc === void 0 ? void 0 : doc.name })];
353
+ });
354
+ }); },
355
+ method: "GET",
356
+ permissions: [permissions_1.Permissions.IsAny],
357
+ },
358
+ },
359
+ permissions: allPermissions,
360
+ });
361
+ return [4 /*yield*/, server.get("/food/stats").expect(200)];
362
+ case 1:
363
+ collectionRes = _a.sent();
364
+ (0, bun_test_1.expect)(collectionRes.body.data).toEqual({ total: 1 });
365
+ return [4 /*yield*/, server.get("/food/".concat(spinach._id, "/peek")).expect(200)];
366
+ case 2:
367
+ instanceRes = _a.sent();
368
+ (0, bun_test_1.expect)(instanceRes.body.data).toEqual({ name: "Spinach" });
369
+ return [2 /*return*/];
370
+ }
371
+ });
372
+ }); });
373
+ (0, bun_test_1.it)("returns 404 for missing instance doc", function () { return __awaiter(void 0, void 0, void 0, function () {
374
+ var missingId, res;
375
+ return __generator(this, function (_a) {
376
+ switch (_a.label) {
377
+ case 0:
378
+ mountFoodRouter({
379
+ instanceActions: {
380
+ peek: {
381
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
382
+ return [2 /*return*/, ({})];
383
+ }); }); },
384
+ method: "GET",
385
+ permissions: [permissions_1.Permissions.IsAny],
386
+ },
387
+ },
388
+ permissions: allPermissions,
389
+ });
390
+ missingId = "507f1f77bcf86cd799439011";
391
+ return [4 /*yield*/, server.get("/food/".concat(missingId, "/peek")).expect(404)];
392
+ case 1:
393
+ res = _a.sent();
394
+ (0, bun_test_1.expect)(res.body.title).toContain(missingId);
395
+ (0, bun_test_1.expect)(res.body.meta).toBeUndefined();
396
+ return [2 /*return*/];
397
+ }
398
+ });
399
+ }); });
400
+ (0, bun_test_1.it)("returns 404 with soft-delete metadata on instance action", function () { return __awaiter(void 0, void 0, void 0, function () {
401
+ var doc, agent, res;
402
+ return __generator(this, function (_a) {
403
+ switch (_a.label) {
404
+ case 0: return [4 /*yield*/, StuffModel.deleteMany({})];
405
+ case 1:
406
+ _a.sent();
407
+ return [4 /*yield*/, StuffModel.create({ deleted: true, name: "hidden", ownerId: "1" })];
408
+ case 2:
409
+ doc = _a.sent();
410
+ app = (0, tests_1.getBaseServer)();
411
+ (0, auth_1.setupAuth)(app, tests_1.UserModel);
412
+ (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
413
+ app.use("/stuff", (0, api_1.modelRouter)(StuffModel, {
414
+ allowAnonymous: true,
415
+ instanceActions: {
416
+ peek: {
417
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
418
+ return [2 /*return*/, ({})];
419
+ }); }); },
420
+ method: "GET",
421
+ permissions: [permissions_1.Permissions.IsAny],
422
+ },
423
+ },
424
+ permissions: allPermissions,
425
+ }));
426
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
427
+ case 3:
428
+ agent = _a.sent();
429
+ return [4 /*yield*/, agent.get("/stuff/".concat(doc._id, "/peek")).expect(404)];
430
+ case 4:
431
+ res = _a.sent();
432
+ (0, bun_test_1.expect)(res.body.meta).toEqual({ deleted: "true" });
433
+ return [2 /*return*/];
434
+ }
435
+ });
436
+ }); });
437
+ (0, bun_test_1.it)("returns 401 when unauthenticated and IsAuthenticated required", function () { return __awaiter(void 0, void 0, void 0, function () {
438
+ var res;
439
+ return __generator(this, function (_a) {
440
+ switch (_a.label) {
441
+ case 0:
442
+ mountFoodRouter({
443
+ allowAnonymous: false,
444
+ collectionActions: {
445
+ secure: {
446
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
447
+ return [2 /*return*/, ({ ok: true })];
448
+ }); }); },
449
+ method: "POST",
450
+ permissions: [permissions_1.Permissions.IsAuthenticated],
451
+ },
452
+ },
453
+ permissions: allPermissions,
454
+ });
455
+ return [4 /*yield*/, server.post("/food/secure").send({}).expect(401)];
456
+ case 1:
457
+ res = _a.sent();
458
+ (0, bun_test_1.expect)(res.body.title).toBe("Unauthorized");
459
+ return [2 /*return*/];
460
+ }
461
+ });
462
+ }); });
463
+ (0, bun_test_1.it)("returns 405 for collection action when pre-doc permission denied", function () { return __awaiter(void 0, void 0, void 0, function () {
464
+ var agent, res;
465
+ return __generator(this, function (_a) {
466
+ switch (_a.label) {
467
+ case 0:
468
+ mountFoodRouter({
469
+ collectionActions: {
470
+ adminOnly: {
471
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
472
+ return [2 /*return*/, ({ ok: true })];
473
+ }); }); },
474
+ method: "POST",
475
+ permissions: [permissions_1.Permissions.IsAdmin],
476
+ },
477
+ },
478
+ permissions: allPermissions,
479
+ });
480
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
481
+ case 1:
482
+ agent = _a.sent();
483
+ return [4 /*yield*/, agent.post("/food/adminOnly").send({}).expect(405)];
484
+ case 2:
485
+ res = _a.sent();
486
+ (0, bun_test_1.expect)(res.body.title).toContain("Access to CREATE on Food denied");
487
+ return [2 /*return*/];
488
+ }
489
+ });
490
+ }); });
491
+ (0, bun_test_1.it)("returns 403 for instance action when post-doc permission denied", function () { return __awaiter(void 0, void 0, void 0, function () {
492
+ var adminFood, agent, res;
493
+ return __generator(this, function (_a) {
494
+ switch (_a.label) {
495
+ case 0: return [4 /*yield*/, tests_1.FoodModel.create({
496
+ calories: 2,
497
+ created: new Date(),
498
+ hidden: false,
499
+ name: "AdminApple",
500
+ ownerId: admin._id,
501
+ source: { name: "test" },
502
+ })];
503
+ case 1:
504
+ adminFood = _a.sent();
505
+ mountFoodRouter({
506
+ instanceActions: {
507
+ ownerOnly: {
508
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
509
+ return [2 /*return*/, ({ ok: true })];
510
+ }); }); },
511
+ method: "POST",
512
+ permissions: [permissions_1.Permissions.IsOwner],
513
+ },
514
+ },
515
+ permissions: allPermissions,
516
+ });
517
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
518
+ case 2:
519
+ agent = _a.sent();
520
+ return [4 /*yield*/, agent.post("/food/".concat(adminFood._id, "/ownerOnly")).send({}).expect(403)];
521
+ case 3:
522
+ res = _a.sent();
523
+ (0, bun_test_1.expect)(res.body.title).toContain("Access to UPDATE on Food:".concat(adminFood._id, " denied"));
524
+ return [2 /*return*/];
525
+ }
526
+ });
527
+ }); });
528
+ (0, bun_test_1.it)("allows IsAuthenticatedOrReadOnly on GET with allowAnonymous", function () { return __awaiter(void 0, void 0, void 0, function () {
529
+ var res;
530
+ return __generator(this, function (_a) {
531
+ switch (_a.label) {
532
+ case 0:
533
+ mountFoodRouter({
534
+ allowAnonymous: true,
535
+ instanceActions: {
536
+ publicRead: {
537
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
538
+ return [2 /*return*/, ({ ok: true })];
539
+ }); }); },
540
+ method: "GET",
541
+ permissions: [permissions_1.Permissions.IsAuthenticatedOrReadOnly],
542
+ },
543
+ },
544
+ permissions: allPermissions,
545
+ });
546
+ return [4 /*yield*/, server.get("/food/".concat(spinach._id, "/publicRead")).expect(200)];
547
+ case 1:
548
+ res = _a.sent();
549
+ (0, bun_test_1.expect)(res.body.data).toEqual({ ok: true });
550
+ return [2 /*return*/];
551
+ }
552
+ });
553
+ }); });
554
+ });
555
+ (0, bun_test_1.describe)("validation", function () {
556
+ (0, bun_test_1.it)("passes valid body through ctx", function () { return __awaiter(void 0, void 0, void 0, function () {
557
+ var seenEmail;
558
+ return __generator(this, function (_a) {
559
+ switch (_a.label) {
560
+ case 0:
561
+ mountFoodRouter({
562
+ collectionActions: {
563
+ notify: {
564
+ body: zod_1.z.object({ email: zod_1.z.string().email() }),
565
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
566
+ var body = _b.body;
567
+ return __generator(this, function (_c) {
568
+ seenEmail = body.email;
569
+ return [2 /*return*/, { sent: true }];
570
+ });
571
+ }); },
572
+ method: "POST",
573
+ permissions: [permissions_1.Permissions.IsAny],
574
+ },
575
+ },
576
+ permissions: allPermissions,
577
+ });
578
+ return [4 /*yield*/, server.post("/food/notify").send({ email: "a@b.com" }).expect(200)];
579
+ case 1:
580
+ _a.sent();
581
+ (0, bun_test_1.expect)(seenEmail).toBe("a@b.com");
582
+ return [2 /*return*/];
583
+ }
584
+ });
585
+ }); });
586
+ (0, bun_test_1.it)("returns 400 with meta.fields for invalid body", function () { return __awaiter(void 0, void 0, void 0, function () {
587
+ var res;
588
+ return __generator(this, function (_a) {
589
+ switch (_a.label) {
590
+ case 0:
591
+ mountFoodRouter({
592
+ collectionActions: {
593
+ notify: {
594
+ body: zod_1.z.object({ email: zod_1.z.string().email() }),
595
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
596
+ return [2 /*return*/, ({ sent: true })];
597
+ }); }); },
598
+ method: "POST",
599
+ permissions: [permissions_1.Permissions.IsAny],
600
+ },
601
+ },
602
+ permissions: allPermissions,
603
+ });
604
+ return [4 /*yield*/, server.post("/food/notify").send({ email: "not-an-email" }).expect(400)];
605
+ case 1:
606
+ res = _a.sent();
607
+ (0, bun_test_1.expect)(res.body.title).toBe("Validation failed");
608
+ (0, bun_test_1.expect)(res.body.meta.fields.email).toBeDefined();
609
+ return [2 /*return*/];
610
+ }
611
+ });
612
+ }); });
613
+ (0, bun_test_1.it)("validates query schema into ctx without mutating req.query", function () { return __awaiter(void 0, void 0, void 0, function () {
614
+ var seenQ, originalQ;
615
+ return __generator(this, function (_a) {
616
+ switch (_a.label) {
617
+ case 0:
618
+ mountFoodRouter({
619
+ collectionActions: {
620
+ search: {
621
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
622
+ var query = _b.query, req = _b.req;
623
+ return __generator(this, function (_c) {
624
+ seenQ = query.count;
625
+ originalQ = req.query.count;
626
+ return [2 /*return*/];
627
+ });
628
+ }); },
629
+ method: "GET",
630
+ permissions: [permissions_1.Permissions.IsAny],
631
+ query: zod_1.z.object({ count: zod_1.z.coerce.number() }),
632
+ },
633
+ },
634
+ permissions: allPermissions,
635
+ });
636
+ return [4 /*yield*/, server.get("/food/search?count=5").expect(200)];
637
+ case 1:
638
+ _a.sent();
639
+ (0, bun_test_1.expect)(seenQ).toBe(5);
640
+ (0, bun_test_1.expect)(originalQ).toBe("5");
641
+ return [2 /*return*/];
642
+ }
643
+ });
644
+ }); });
645
+ (0, bun_test_1.it)("coerces body values via zod in ctx", function () { return __awaiter(void 0, void 0, void 0, function () {
646
+ var seenCount, res;
647
+ return __generator(this, function (_a) {
648
+ switch (_a.label) {
649
+ case 0:
650
+ mountFoodRouter({
651
+ collectionActions: {
652
+ tally: {
653
+ body: zod_1.z.object({ count: zod_1.z.coerce.number() }),
654
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
655
+ var parsed;
656
+ var body = _b.body;
657
+ return __generator(this, function (_c) {
658
+ parsed = body;
659
+ seenCount = parsed.count;
660
+ return [2 /*return*/, { count: parsed.count }];
661
+ });
662
+ }); },
663
+ method: "POST",
664
+ permissions: [permissions_1.Permissions.IsAny],
665
+ },
666
+ },
667
+ permissions: allPermissions,
668
+ });
669
+ return [4 /*yield*/, server.post("/food/tally").send({ count: "5" }).expect(200)];
670
+ case 1:
671
+ res = _a.sent();
672
+ (0, bun_test_1.expect)(seenCount).toBe(5);
673
+ (0, bun_test_1.expect)(res.body.data).toEqual({ count: 5 });
674
+ return [2 /*return*/];
675
+ }
676
+ });
677
+ }); });
678
+ (0, bun_test_1.it)("strips unknown body fields by default", function () { return __awaiter(void 0, void 0, void 0, function () {
679
+ var seenBody;
680
+ return __generator(this, function (_a) {
681
+ switch (_a.label) {
682
+ case 0:
683
+ seenBody = {};
684
+ mountFoodRouter({
685
+ collectionActions: {
686
+ strictish: {
687
+ body: zod_1.z.object({ known: zod_1.z.string() }),
688
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
689
+ var body = _b.body;
690
+ return __generator(this, function (_c) {
691
+ seenBody = body;
692
+ return [2 /*return*/, { ok: true }];
693
+ });
694
+ }); },
695
+ method: "POST",
696
+ permissions: [permissions_1.Permissions.IsAny],
697
+ },
698
+ },
699
+ permissions: allPermissions,
700
+ });
701
+ return [4 /*yield*/, server.post("/food/strictish").send({ extra: "x", known: "y" }).expect(200)];
702
+ case 1:
703
+ _a.sent();
704
+ (0, bun_test_1.expect)(seenBody).toEqual({ known: "y" });
705
+ return [2 /*return*/];
706
+ }
707
+ });
708
+ }); });
709
+ });
710
+ (0, bun_test_1.describe)("response shape", function () {
711
+ (0, bun_test_1.it)("wraps handler return in data envelope", function () { return __awaiter(void 0, void 0, void 0, function () {
712
+ var res;
713
+ return __generator(this, function (_a) {
714
+ switch (_a.label) {
715
+ case 0:
716
+ mountFoodRouter({
717
+ collectionActions: {
718
+ echo: {
719
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
720
+ return [2 /*return*/, ({ x: 1 })];
721
+ }); }); },
722
+ method: "POST",
723
+ permissions: [permissions_1.Permissions.IsAny],
724
+ },
725
+ },
726
+ permissions: allPermissions,
727
+ });
728
+ return [4 /*yield*/, server.post("/food/echo").send({}).expect(200)];
729
+ case 1:
730
+ res = _a.sent();
731
+ (0, bun_test_1.expect)(res.body).toEqual({ data: { x: 1 } });
732
+ return [2 /*return*/];
733
+ }
734
+ });
735
+ }); });
736
+ (0, bun_test_1.it)("respects custom status code", function () { return __awaiter(void 0, void 0, void 0, function () {
737
+ var res;
738
+ return __generator(this, function (_a) {
739
+ switch (_a.label) {
740
+ case 0:
741
+ mountFoodRouter({
742
+ collectionActions: {
743
+ queue: {
744
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
745
+ return [2 /*return*/, ({ queued: true })];
746
+ }); }); },
747
+ method: "POST",
748
+ permissions: [permissions_1.Permissions.IsAny],
749
+ status: 202,
750
+ },
751
+ },
752
+ permissions: allPermissions,
753
+ });
754
+ return [4 /*yield*/, server.post("/food/queue").send({}).expect(202)];
755
+ case 1:
756
+ res = _a.sent();
757
+ (0, bun_test_1.expect)(res.body).toEqual({ data: { queued: true } });
758
+ return [2 /*return*/];
759
+ }
760
+ });
761
+ }); });
762
+ (0, bun_test_1.it)("returns data null for undefined handler return", function () { return __awaiter(void 0, void 0, void 0, function () {
763
+ var res;
764
+ return __generator(this, function (_a) {
765
+ switch (_a.label) {
766
+ case 0:
767
+ mountFoodRouter({
768
+ collectionActions: {
769
+ noop: {
770
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
771
+ return [2 /*return*/, undefined];
772
+ }); }); },
773
+ method: "POST",
774
+ permissions: [permissions_1.Permissions.IsAny],
775
+ },
776
+ },
777
+ permissions: allPermissions,
778
+ });
779
+ return [4 /*yield*/, server.post("/food/noop").send({}).expect(200)];
780
+ case 1:
781
+ res = _a.sent();
782
+ (0, bun_test_1.expect)(res.body).toEqual({ data: null });
783
+ return [2 /*return*/];
784
+ }
785
+ });
786
+ }); });
787
+ (0, bun_test_1.it)("skips auto-wrap when res.headersSent", function () { return __awaiter(void 0, void 0, void 0, function () {
788
+ var res;
789
+ return __generator(this, function (_a) {
790
+ switch (_a.label) {
791
+ case 0:
792
+ mountFoodRouter({
793
+ collectionActions: {
794
+ custom: {
795
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
796
+ var res = _b.res;
797
+ return __generator(this, function (_c) {
798
+ res.json({ custom: 1 });
799
+ return [2 /*return*/];
800
+ });
801
+ }); },
802
+ method: "POST",
803
+ permissions: [permissions_1.Permissions.IsAny],
804
+ },
805
+ },
806
+ permissions: allPermissions,
807
+ });
808
+ return [4 /*yield*/, server.post("/food/custom").send({}).expect(200)];
809
+ case 1:
810
+ res = _a.sent();
811
+ (0, bun_test_1.expect)(res.body).toEqual({ custom: 1 });
812
+ return [2 /*return*/];
813
+ }
814
+ });
815
+ }); });
816
+ (0, bun_test_1.it)("allows custom list-style envelope via res.json", function () { return __awaiter(void 0, void 0, void 0, function () {
817
+ var res;
818
+ return __generator(this, function (_a) {
819
+ switch (_a.label) {
820
+ case 0:
821
+ mountFoodRouter({
822
+ collectionActions: {
823
+ paged: {
824
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
825
+ var res = _b.res;
826
+ return __generator(this, function (_c) {
827
+ res.json({ data: [{ id: 1 }], more: false, page: 1, total: 1 });
828
+ return [2 /*return*/];
829
+ });
830
+ }); },
831
+ method: "GET",
832
+ permissions: [permissions_1.Permissions.IsAny],
833
+ },
834
+ },
835
+ permissions: allPermissions,
836
+ });
837
+ return [4 /*yield*/, server.get("/food/paged").expect(200)];
838
+ case 1:
839
+ res = _a.sent();
840
+ (0, bun_test_1.expect)(res.body).toEqual({ data: [{ id: 1 }], more: false, page: 1, total: 1 });
841
+ (0, bun_test_1.expect)(res.body.data).not.toHaveProperty("data");
842
+ return [2 /*return*/];
843
+ }
844
+ });
845
+ }); });
846
+ });
847
+ (0, bun_test_1.describe)("co-registration precedence", function () {
848
+ (0, bun_test_1.it)("instance action wins over endpoints route on same path", function () { return __awaiter(void 0, void 0, void 0, function () {
849
+ var res;
850
+ return __generator(this, function (_a) {
851
+ switch (_a.label) {
852
+ case 0:
853
+ mountFoodRouter({
854
+ endpoints: function (router) {
855
+ router.get("/:id/foo", function (_req, res) {
856
+ res.json({ data: { from: "endpoints" } });
857
+ });
858
+ },
859
+ instanceActions: {
860
+ foo: {
861
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
862
+ return [2 /*return*/, ({ from: "action" })];
863
+ }); }); },
864
+ method: "GET",
865
+ permissions: [permissions_1.Permissions.IsAny],
866
+ },
867
+ },
868
+ permissions: allPermissions,
869
+ });
870
+ return [4 /*yield*/, server.get("/food/".concat(spinach._id, "/foo")).expect(200)];
871
+ case 1:
872
+ res = _a.sent();
873
+ (0, bun_test_1.expect)(res.body.data).toEqual({ from: "action" });
874
+ return [2 /*return*/];
875
+ }
876
+ });
877
+ }); });
878
+ (0, bun_test_1.it)("collection action wins over endpoints route on same path", function () { return __awaiter(void 0, void 0, void 0, function () {
879
+ var res;
880
+ return __generator(this, function (_a) {
881
+ switch (_a.label) {
882
+ case 0:
883
+ mountFoodRouter({
884
+ collectionActions: {
885
+ report: {
886
+ handler: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
887
+ return [2 /*return*/, ({ from: "action" })];
888
+ }); }); },
889
+ method: "GET",
890
+ permissions: [permissions_1.Permissions.IsAny],
891
+ },
892
+ },
893
+ endpoints: function (router) {
894
+ router.get("/report", function (_req, res) {
895
+ res.json({ data: { from: "endpoints" } });
896
+ });
897
+ },
898
+ permissions: allPermissions,
899
+ });
900
+ return [4 /*yield*/, server.get("/food/report").expect(200)];
901
+ case 1:
902
+ res = _a.sent();
903
+ (0, bun_test_1.expect)(res.body.data).toEqual({ from: "action" });
904
+ return [2 /*return*/];
905
+ }
906
+ });
907
+ }); });
908
+ });
909
+ });
910
+ (0, bun_test_1.describe)("defineInstanceAction type ergonomics", function () {
911
+ (0, bun_test_1.it)("preserves handler types at compile time", function () {
912
+ var action = (0, actions_1.defineInstanceAction)({
913
+ body: zod_1.z.object({ notifyUsers: zod_1.z.boolean() }),
914
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
915
+ var _doc, _notify;
916
+ var _c, _d;
917
+ var body = _b.body, doc = _b.doc;
918
+ return __generator(this, function (_e) {
919
+ _doc = doc;
920
+ _notify = body.notifyUsers;
921
+ return [2 /*return*/, { notify: _notify, publishedAt: (_d = (_c = _doc.publishedAt) === null || _c === void 0 ? void 0 : _c.toISOString()) !== null && _d !== void 0 ? _d : null }];
922
+ });
923
+ }); },
924
+ method: "POST",
925
+ permissions: [permissions_1.Permissions.IsAny],
926
+ });
927
+ (0, bun_test_1.expect)(action.method).toBe("POST");
928
+ });
929
+ (0, bun_test_1.it)("defineCollectionAction preserves body types", function () {
930
+ var action = (0, actions_1.defineCollectionAction)({
931
+ body: zod_1.z.object({ ids: zod_1.z.array(zod_1.z.string()) }),
932
+ handler: function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
933
+ var _ids;
934
+ var body = _b.body;
935
+ return __generator(this, function (_c) {
936
+ _ids = body.ids;
937
+ return [2 /*return*/, { count: _ids.length }];
938
+ });
939
+ }); },
940
+ method: "POST",
941
+ permissions: [permissions_1.Permissions.IsAny],
942
+ });
943
+ (0, bun_test_1.expect)(action.method).toBe("POST");
944
+ });
945
+ });
946
+ });