@terreno/api 0.0.17 → 0.1.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 (77) hide show
  1. package/.claude/CLAUDE.local.md +204 -0
  2. package/.cursor/rules/00-root.mdc +338 -0
  3. package/.github/copilot-instructions.md +333 -0
  4. package/AGENTS.md +333 -0
  5. package/README.md +76 -7
  6. package/biome.jsonc +1 -1
  7. package/dist/api.d.ts +68 -1
  8. package/dist/api.js +140 -5
  9. package/dist/api.query.test.js +1 -1
  10. package/dist/api.test.js +222 -484
  11. package/dist/auth.js +3 -1
  12. package/dist/errors.js +15 -12
  13. package/dist/example.js +7 -7
  14. package/dist/expressServer.d.ts +8 -2
  15. package/dist/expressServer.js +8 -1
  16. package/dist/githubAuth.d.ts +64 -0
  17. package/dist/githubAuth.js +293 -0
  18. package/dist/githubAuth.test.d.ts +1 -0
  19. package/dist/githubAuth.test.js +351 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/logger.js +1 -1
  23. package/dist/middleware.js +1 -1
  24. package/dist/notifiers/googleChatNotifier.js +1 -1
  25. package/dist/notifiers/googleChatNotifier.test.js +1 -1
  26. package/dist/notifiers/slackNotifier.js +1 -1
  27. package/dist/notifiers/slackNotifier.test.js +1 -1
  28. package/dist/notifiers/zoomNotifier.js +1 -1
  29. package/dist/notifiers/zoomNotifier.test.js +1 -1
  30. package/dist/openApi.test.js +8 -5
  31. package/dist/openApiBuilder.d.ts +69 -1
  32. package/dist/openApiBuilder.js +109 -5
  33. package/dist/openApiValidator.d.ts +296 -0
  34. package/dist/openApiValidator.js +698 -0
  35. package/dist/openApiValidator.test.d.ts +1 -0
  36. package/dist/openApiValidator.test.js +346 -0
  37. package/dist/permissions.js +1 -1
  38. package/dist/plugins.test.js +3 -3
  39. package/dist/terrenoPlugin.d.ts +4 -0
  40. package/dist/terrenoPlugin.js +2 -0
  41. package/dist/tests/bunSetup.js +2 -2
  42. package/dist/tests.js +34 -24
  43. package/package.json +7 -2
  44. package/src/__snapshots__/openApi.test.ts.snap +399 -0
  45. package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
  46. package/src/api.query.test.ts +1 -1
  47. package/src/api.test.ts +161 -374
  48. package/src/api.ts +210 -4
  49. package/src/auth.ts +3 -1
  50. package/src/errors.ts +15 -12
  51. package/src/example.ts +7 -7
  52. package/src/expressServer.ts +18 -2
  53. package/src/githubAuth.test.ts +223 -0
  54. package/src/githubAuth.ts +335 -0
  55. package/src/index.ts +3 -0
  56. package/src/logger.ts +1 -1
  57. package/src/middleware.ts +1 -1
  58. package/src/notifiers/googleChatNotifier.test.ts +1 -1
  59. package/src/notifiers/googleChatNotifier.ts +1 -1
  60. package/src/notifiers/slackNotifier.test.ts +1 -1
  61. package/src/notifiers/slackNotifier.ts +1 -1
  62. package/src/notifiers/zoomNotifier.test.ts +1 -1
  63. package/src/notifiers/zoomNotifier.ts +1 -1
  64. package/src/openApi.test.ts +8 -5
  65. package/src/openApiBuilder.ts +188 -15
  66. package/src/openApiValidator.test.ts +241 -0
  67. package/src/openApiValidator.ts +860 -0
  68. package/src/permissions.ts +1 -1
  69. package/src/plugins.test.ts +3 -3
  70. package/src/terrenoPlugin.ts +5 -0
  71. package/src/tests/bunSetup.ts +2 -2
  72. package/src/tests.ts +34 -24
  73. package/CLAUDE.md +0 -107
  74. package/dist/response.d.ts +0 -0
  75. package/dist/response.js +0 -1
  76. package/index.ts +0 -1
  77. package/src/response.ts +0 -0
package/dist/api.test.js CHANGED
@@ -728,6 +728,7 @@ var transformers_1 = require("./transformers");
728
728
  (0, bun_test_1.describe)("transformer errors", function () {
729
729
  var admin;
730
730
  var spinach;
731
+ var agent;
731
732
  (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
732
733
  var _a;
733
734
  return __generator(this, function (_b) {
@@ -832,6 +833,41 @@ var transformers_1 = require("./transformers");
832
833
  }
833
834
  });
834
835
  }); });
836
+ (0, bun_test_1.it)("preDelete hook throwing APIError is re-thrown", function () { return __awaiter(void 0, void 0, void 0, function () {
837
+ var res;
838
+ return __generator(this, function (_a) {
839
+ switch (_a.label) {
840
+ case 0:
841
+ app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
842
+ allowAnonymous: true,
843
+ permissions: {
844
+ create: [permissions_1.Permissions.IsAny],
845
+ delete: [permissions_1.Permissions.IsAny],
846
+ list: [permissions_1.Permissions.IsAny],
847
+ read: [permissions_1.Permissions.IsAny],
848
+ update: [permissions_1.Permissions.IsAny],
849
+ },
850
+ preDelete: function () {
851
+ throw new errors_1.APIError({
852
+ disableExternalErrorTracking: true,
853
+ status: 400,
854
+ title: "Custom preDelete APIError",
855
+ });
856
+ },
857
+ }));
858
+ server = (0, supertest_1.default)(app);
859
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
860
+ case 1:
861
+ agent = _a.sent();
862
+ return [4 /*yield*/, agent.delete("/food/".concat(spinach._id)).expect(400)];
863
+ case 2:
864
+ res = _a.sent();
865
+ (0, bun_test_1.expect)(res.body.title).toBe("Custom preDelete APIError");
866
+ (0, bun_test_1.expect)(res.body.disableExternalErrorTracking).toBe(true);
867
+ return [2 /*return*/];
868
+ }
869
+ });
870
+ }); });
835
871
  });
836
872
  (0, bun_test_1.describe)("addPopulateToQuery", function () {
837
873
  (0, bun_test_1.it)("returns query unchanged with no populate paths", function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -1109,8 +1145,12 @@ var transformers_1 = require("./transformers");
1109
1145
  case 1:
1110
1146
  mongoose = _a.sent();
1111
1147
  softDeleteSchema = new mongoose.Schema({
1112
- deleted: { default: false, type: Boolean },
1113
- name: String,
1148
+ deleted: {
1149
+ default: false,
1150
+ description: "Whether this item has been soft deleted",
1151
+ type: Boolean,
1152
+ },
1153
+ name: { description: "The name of the item", type: String },
1114
1154
  });
1115
1155
  try {
1116
1156
  SoftDeleteModel = mongoose.model("SoftDeleteTest");
@@ -1192,10 +1232,8 @@ var transformers_1 = require("./transformers");
1192
1232
  });
1193
1233
  }); });
1194
1234
  });
1195
- (0, bun_test_1.describe)("transformer errors", function () {
1235
+ (0, bun_test_1.describe)("special query params", function () {
1196
1236
  var admin;
1197
- var spinach;
1198
- var agent;
1199
1237
  (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1200
1238
  var _a;
1201
1239
  return __generator(this, function (_b) {
@@ -1209,12 +1247,9 @@ var transformers_1 = require("./transformers");
1209
1247
  hidden: false,
1210
1248
  name: "Spinach",
1211
1249
  ownerId: admin._id,
1212
- source: {
1213
- name: "Brand",
1214
- },
1215
1250
  })];
1216
1251
  case 2:
1217
- spinach = _b.sent();
1252
+ _b.sent();
1218
1253
  app = (0, tests_1.getBaseServer)();
1219
1254
  (0, auth_1.setupAuth)(app, tests_1.UserModel);
1220
1255
  (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
@@ -1222,7 +1257,7 @@ var transformers_1 = require("./transformers");
1222
1257
  }
1223
1258
  });
1224
1259
  }); });
1225
- (0, bun_test_1.it)("transform error in create is handled", function () { return __awaiter(void 0, void 0, void 0, function () {
1260
+ (0, bun_test_1.it)("period query param is stripped from query", function () { return __awaiter(void 0, void 0, void 0, function () {
1226
1261
  var res;
1227
1262
  return __generator(this, function (_a) {
1228
1263
  switch (_a.label) {
@@ -1236,25 +1271,41 @@ var transformers_1 = require("./transformers");
1236
1271
  read: [permissions_1.Permissions.IsAny],
1237
1272
  update: [permissions_1.Permissions.IsAny],
1238
1273
  },
1239
- transformer: (0, transformers_1.AdminOwnerTransformer)({
1240
- // Only allow 'name' to be written, so 'calories' will throw
1241
- anonWriteFields: ["name"],
1242
- }),
1274
+ queryFields: ["name", "period"],
1275
+ queryFilter: function (_user, query) {
1276
+ // Simulate a queryFilter that accepts and processes period
1277
+ if (query === null || query === void 0 ? void 0 : query.period) {
1278
+ // Period is processed but shouldn't be passed to mongo
1279
+ return query;
1280
+ }
1281
+ return query !== null && query !== void 0 ? query : {};
1282
+ },
1243
1283
  }));
1244
1284
  server = (0, supertest_1.default)(app);
1245
- return [4 /*yield*/, server.post("/food").send({ calories: 15, name: "Broccoli" }).expect(400)];
1285
+ return [4 /*yield*/, server.get("/food?period=weekly").expect(200)];
1246
1286
  case 1:
1247
1287
  res = _a.sent();
1248
- (0, bun_test_1.expect)(res.body.title).toContain("cannot write fields");
1288
+ (0, bun_test_1.expect)(res.body.data).toBeDefined();
1249
1289
  return [2 /*return*/];
1250
1290
  }
1251
1291
  });
1252
1292
  }); });
1253
- (0, bun_test_1.it)("transform error in patch is handled", function () { return __awaiter(void 0, void 0, void 0, function () {
1293
+ (0, bun_test_1.it)("query with false value", function () { return __awaiter(void 0, void 0, void 0, function () {
1254
1294
  var res;
1255
1295
  return __generator(this, function (_a) {
1256
1296
  switch (_a.label) {
1257
- case 0:
1297
+ case 0:
1298
+ // Create a food that is hidden
1299
+ return [4 /*yield*/, tests_1.FoodModel.create({
1300
+ calories: 50,
1301
+ created: new Date("2021-12-04T00:00:20.000Z"),
1302
+ hidden: true,
1303
+ name: "HiddenFood",
1304
+ ownerId: admin._id,
1305
+ })];
1306
+ case 1:
1307
+ // Create a food that is hidden
1308
+ _a.sent();
1258
1309
  app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1259
1310
  allowAnonymous: true,
1260
1311
  permissions: {
@@ -1264,28 +1315,25 @@ var transformers_1 = require("./transformers");
1264
1315
  read: [permissions_1.Permissions.IsAny],
1265
1316
  update: [permissions_1.Permissions.IsAny],
1266
1317
  },
1267
- transformer: (0, transformers_1.AdminOwnerTransformer)({
1268
- // Only allow 'name' to be written, so 'calories' will throw
1269
- anonWriteFields: ["name"],
1270
- }),
1318
+ queryFields: ["name", "hidden"],
1271
1319
  }));
1272
1320
  server = (0, supertest_1.default)(app);
1273
- return [4 /*yield*/, server.patch("/food/".concat(spinach._id)).send({ calories: 100 }).expect(403)];
1274
- case 1:
1321
+ return [4 /*yield*/, server.get("/food?hidden=false").expect(200)];
1322
+ case 2:
1275
1323
  res = _a.sent();
1276
- (0, bun_test_1.expect)(res.body.title).toContain("cannot write fields");
1324
+ (0, bun_test_1.expect)(res.body.data.every(function (f) { return f.hidden === false; })).toBe(true);
1277
1325
  return [2 /*return*/];
1278
1326
  }
1279
1327
  });
1280
1328
  }); });
1281
- (0, bun_test_1.it)("model.create validation error is handled", function () { return __awaiter(void 0, void 0, void 0, function () {
1282
- var RequiredModel, res;
1329
+ (0, bun_test_1.it)("$search query triggers special handling code path", function () { return __awaiter(void 0, void 0, void 0, function () {
1330
+ var res;
1283
1331
  return __generator(this, function (_a) {
1284
1332
  switch (_a.label) {
1285
- case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("./tests")); })];
1286
- case 1:
1287
- RequiredModel = (_a.sent()).RequiredModel;
1288
- app.use("/required", (0, api_1.modelRouter)(RequiredModel, {
1333
+ case 0:
1334
+ // The $search code path just accesses the collection but doesn't do anything with it
1335
+ // This test verifies the code path is exercised
1336
+ app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1289
1337
  allowAnonymous: true,
1290
1338
  permissions: {
1291
1339
  create: [permissions_1.Permissions.IsAny],
@@ -1294,17 +1342,21 @@ var transformers_1 = require("./transformers");
1294
1342
  read: [permissions_1.Permissions.IsAny],
1295
1343
  update: [permissions_1.Permissions.IsAny],
1296
1344
  },
1345
+ // Need to include $search in queryFields for it to pass validation
1346
+ queryFields: ["name", "$search"],
1297
1347
  }));
1298
1348
  server = (0, supertest_1.default)(app);
1299
- return [4 /*yield*/, server.post("/required").send({ about: "test" }).expect(400)];
1300
- case 2:
1349
+ return [4 /*yield*/, server.get("/food?$search=test")];
1350
+ case 1:
1301
1351
  res = _a.sent();
1302
- (0, bun_test_1.expect)(res.body.title).toContain("Required");
1352
+ // May return 500 because $search is passed to Mongo which doesn't support it without Atlas
1353
+ // The important thing is we've exercised the code path
1354
+ (0, bun_test_1.expect)(res.status === 200 || res.status === 500).toBe(true);
1303
1355
  return [2 /*return*/];
1304
1356
  }
1305
1357
  });
1306
1358
  }); });
1307
- (0, bun_test_1.it)("preDelete hook throwing APIError is re-thrown", function () { return __awaiter(void 0, void 0, void 0, function () {
1359
+ (0, bun_test_1.it)("$autocomplete query triggers special handling code path", function () { return __awaiter(void 0, void 0, void 0, function () {
1308
1360
  var res;
1309
1361
  return __generator(this, function (_a) {
1310
1362
  switch (_a.label) {
@@ -1318,30 +1370,22 @@ var transformers_1 = require("./transformers");
1318
1370
  read: [permissions_1.Permissions.IsAny],
1319
1371
  update: [permissions_1.Permissions.IsAny],
1320
1372
  },
1321
- preDelete: function () {
1322
- throw new errors_1.APIError({
1323
- disableExternalErrorTracking: true,
1324
- status: 400,
1325
- title: "Custom preDelete APIError",
1326
- });
1327
- },
1373
+ queryFields: ["name", "$autocomplete"],
1328
1374
  }));
1329
1375
  server = (0, supertest_1.default)(app);
1330
- return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
1376
+ return [4 /*yield*/, server.get("/food?$autocomplete=test")];
1331
1377
  case 1:
1332
- agent = _a.sent();
1333
- return [4 /*yield*/, agent.delete("/food/".concat(spinach._id)).expect(400)];
1334
- case 2:
1335
1378
  res = _a.sent();
1336
- (0, bun_test_1.expect)(res.body.title).toBe("Custom preDelete APIError");
1337
- (0, bun_test_1.expect)(res.body.disableExternalErrorTracking).toBe(true);
1379
+ (0, bun_test_1.expect)(res.status === 200 || res.status === 500).toBe(true);
1338
1380
  return [2 /*return*/];
1339
1381
  }
1340
1382
  });
1341
1383
  }); });
1342
1384
  });
1343
- (0, bun_test_1.describe)("special query params", function () {
1385
+ (0, bun_test_1.describe)("array operation with undefined preUpdate return", function () {
1344
1386
  var admin;
1387
+ var apple;
1388
+ var agent;
1345
1389
  (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1346
1390
  var _a;
1347
1391
  return __generator(this, function (_b) {
@@ -1350,14 +1394,19 @@ var transformers_1 = require("./transformers");
1350
1394
  case 1:
1351
1395
  _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
1352
1396
  return [4 /*yield*/, tests_1.FoodModel.create({
1353
- calories: 1,
1354
- created: new Date("2021-12-03T00:00:20.000Z"),
1397
+ calories: 100,
1398
+ categories: [
1399
+ { name: "Fruit", show: true },
1400
+ { name: "Popular", show: false },
1401
+ ],
1402
+ created: new Date("2021-12-03T00:00:30.000Z"),
1355
1403
  hidden: false,
1356
- name: "Spinach",
1404
+ name: "Apple",
1357
1405
  ownerId: admin._id,
1406
+ tags: ["healthy", "cheap"],
1358
1407
  })];
1359
1408
  case 2:
1360
- _b.sent();
1409
+ apple = _b.sent();
1361
1410
  app = (0, tests_1.getBaseServer)();
1362
1411
  (0, auth_1.setupAuth)(app, tests_1.UserModel);
1363
1412
  (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
@@ -1365,7 +1414,7 @@ var transformers_1 = require("./transformers");
1365
1414
  }
1366
1415
  });
1367
1416
  }); });
1368
- (0, bun_test_1.it)("period query param is stripped from query", function () { return __awaiter(void 0, void 0, void 0, function () {
1417
+ (0, bun_test_1.it)("array operation preUpdate returning undefined for array POST throws error", function () { return __awaiter(void 0, void 0, void 0, function () {
1369
1418
  var res;
1370
1419
  return __generator(this, function (_a) {
1371
1420
  switch (_a.label) {
@@ -1373,98 +1422,89 @@ var transformers_1 = require("./transformers");
1373
1422
  app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1374
1423
  allowAnonymous: true,
1375
1424
  permissions: {
1376
- create: [permissions_1.Permissions.IsAny],
1377
- delete: [permissions_1.Permissions.IsAny],
1378
- list: [permissions_1.Permissions.IsAny],
1379
- read: [permissions_1.Permissions.IsAny],
1380
- update: [permissions_1.Permissions.IsAny],
1381
- },
1382
- queryFields: ["name", "period"],
1383
- queryFilter: function (_user, query) {
1384
- // Simulate a queryFilter that accepts and processes period
1385
- if (query === null || query === void 0 ? void 0 : query.period) {
1386
- // Period is processed but shouldn't be passed to mongo
1387
- return query;
1388
- }
1389
- return query !== null && query !== void 0 ? query : {};
1425
+ create: [permissions_1.Permissions.IsAdmin],
1426
+ delete: [permissions_1.Permissions.IsAdmin],
1427
+ list: [permissions_1.Permissions.IsAdmin],
1428
+ read: [permissions_1.Permissions.IsAdmin],
1429
+ update: [permissions_1.Permissions.IsAdmin],
1390
1430
  },
1431
+ preUpdate: function () { return undefined; },
1391
1432
  }));
1392
1433
  server = (0, supertest_1.default)(app);
1393
- return [4 /*yield*/, server.get("/food?period=weekly").expect(200)];
1434
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
1394
1435
  case 1:
1436
+ agent = _a.sent();
1437
+ return [4 /*yield*/, agent.post("/food/".concat(apple._id, "/tags")).send({ tags: "organic" }).expect(403)];
1438
+ case 2:
1395
1439
  res = _a.sent();
1396
- (0, bun_test_1.expect)(res.body.data).toBeDefined();
1440
+ (0, bun_test_1.expect)(res.body.title).toBe("Update not allowed");
1441
+ (0, bun_test_1.expect)(res.body.detail).toBe("A body must be returned from preUpdate");
1397
1442
  return [2 /*return*/];
1398
1443
  }
1399
1444
  });
1400
1445
  }); });
1401
- (0, bun_test_1.it)("query with false value", function () { return __awaiter(void 0, void 0, void 0, function () {
1446
+ (0, bun_test_1.it)("array operation preUpdate returning null for array PATCH throws error", function () { return __awaiter(void 0, void 0, void 0, function () {
1402
1447
  var res;
1403
1448
  return __generator(this, function (_a) {
1404
1449
  switch (_a.label) {
1405
- case 0:
1406
- // Create a food that is hidden
1407
- return [4 /*yield*/, tests_1.FoodModel.create({
1408
- calories: 50,
1409
- created: new Date("2021-12-04T00:00:20.000Z"),
1410
- hidden: true,
1411
- name: "HiddenFood",
1412
- ownerId: admin._id,
1413
- })];
1414
- case 1:
1415
- // Create a food that is hidden
1416
- _a.sent();
1450
+ case 0:
1417
1451
  app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1418
1452
  allowAnonymous: true,
1419
1453
  permissions: {
1420
- create: [permissions_1.Permissions.IsAny],
1421
- delete: [permissions_1.Permissions.IsAny],
1422
- list: [permissions_1.Permissions.IsAny],
1423
- read: [permissions_1.Permissions.IsAny],
1424
- update: [permissions_1.Permissions.IsAny],
1454
+ create: [permissions_1.Permissions.IsAdmin],
1455
+ delete: [permissions_1.Permissions.IsAdmin],
1456
+ list: [permissions_1.Permissions.IsAdmin],
1457
+ read: [permissions_1.Permissions.IsAdmin],
1458
+ update: [permissions_1.Permissions.IsAdmin],
1425
1459
  },
1426
- queryFields: ["name", "hidden"],
1460
+ preUpdate: function () { return null; },
1427
1461
  }));
1428
1462
  server = (0, supertest_1.default)(app);
1429
- return [4 /*yield*/, server.get("/food?hidden=false").expect(200)];
1463
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
1464
+ case 1:
1465
+ agent = _a.sent();
1466
+ return [4 /*yield*/, agent
1467
+ .patch("/food/".concat(apple._id, "/tags/healthy"))
1468
+ .send({ tags: "unhealthy" })
1469
+ .expect(403)];
1430
1470
  case 2:
1431
1471
  res = _a.sent();
1432
- (0, bun_test_1.expect)(res.body.data.every(function (f) { return f.hidden === false; })).toBe(true);
1472
+ (0, bun_test_1.expect)(res.body.title).toBe("Update not allowed");
1433
1473
  return [2 /*return*/];
1434
1474
  }
1435
1475
  });
1436
1476
  }); });
1437
- (0, bun_test_1.it)("$search query triggers special handling code path", function () { return __awaiter(void 0, void 0, void 0, function () {
1477
+ (0, bun_test_1.it)("array operation preUpdate error for array DELETE is handled", function () { return __awaiter(void 0, void 0, void 0, function () {
1438
1478
  var res;
1439
1479
  return __generator(this, function (_a) {
1440
1480
  switch (_a.label) {
1441
1481
  case 0:
1442
- // The $search code path just accesses the collection but doesn't do anything with it
1443
- // This test verifies the code path is exercised
1444
1482
  app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1445
1483
  allowAnonymous: true,
1446
1484
  permissions: {
1447
- create: [permissions_1.Permissions.IsAny],
1448
- delete: [permissions_1.Permissions.IsAny],
1449
- list: [permissions_1.Permissions.IsAny],
1450
- read: [permissions_1.Permissions.IsAny],
1451
- update: [permissions_1.Permissions.IsAny],
1485
+ create: [permissions_1.Permissions.IsAdmin],
1486
+ delete: [permissions_1.Permissions.IsAdmin],
1487
+ list: [permissions_1.Permissions.IsAdmin],
1488
+ read: [permissions_1.Permissions.IsAdmin],
1489
+ update: [permissions_1.Permissions.IsAdmin],
1490
+ },
1491
+ preUpdate: function () {
1492
+ throw new Error("preUpdate error during delete");
1452
1493
  },
1453
- // Need to include $search in queryFields for it to pass validation
1454
- queryFields: ["name", "$search"],
1455
1494
  }));
1456
1495
  server = (0, supertest_1.default)(app);
1457
- return [4 /*yield*/, server.get("/food?$search=test")];
1496
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
1458
1497
  case 1:
1498
+ agent = _a.sent();
1499
+ return [4 /*yield*/, agent.delete("/food/".concat(apple._id, "/tags/healthy")).expect(400)];
1500
+ case 2:
1459
1501
  res = _a.sent();
1460
- // May return 500 because $search is passed to Mongo which doesn't support it without Atlas
1461
- // The important thing is we've exercised the code path
1462
- (0, bun_test_1.expect)(res.status === 200 || res.status === 500).toBe(true);
1502
+ (0, bun_test_1.expect)(res.body.title).toContain("preUpdate hook error");
1463
1503
  return [2 /*return*/];
1464
1504
  }
1465
1505
  });
1466
1506
  }); });
1467
- (0, bun_test_1.it)("$autocomplete query triggers special handling code path", function () { return __awaiter(void 0, void 0, void 0, function () {
1507
+ (0, bun_test_1.it)("array operation postUpdate error is handled", function () { return __awaiter(void 0, void 0, void 0, function () {
1468
1508
  var res;
1469
1509
  return __generator(this, function (_a) {
1470
1510
  switch (_a.label) {
@@ -1472,391 +1512,57 @@ var transformers_1 = require("./transformers");
1472
1512
  app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1473
1513
  allowAnonymous: true,
1474
1514
  permissions: {
1475
- create: [permissions_1.Permissions.IsAny],
1476
- delete: [permissions_1.Permissions.IsAny],
1477
- list: [permissions_1.Permissions.IsAny],
1478
- read: [permissions_1.Permissions.IsAny],
1479
- update: [permissions_1.Permissions.IsAny],
1515
+ create: [permissions_1.Permissions.IsAdmin],
1516
+ delete: [permissions_1.Permissions.IsAdmin],
1517
+ list: [permissions_1.Permissions.IsAdmin],
1518
+ read: [permissions_1.Permissions.IsAdmin],
1519
+ update: [permissions_1.Permissions.IsAdmin],
1480
1520
  },
1481
- queryFields: ["name", "$autocomplete"],
1482
- }));
1483
- server = (0, supertest_1.default)(app);
1484
- return [4 /*yield*/, server.get("/food?$autocomplete=test")];
1485
- case 1:
1486
- res = _a.sent();
1487
- (0, bun_test_1.expect)(res.status === 200 || res.status === 500).toBe(true);
1488
- return [2 /*return*/];
1489
- }
1490
- });
1491
- }); });
1492
- });
1493
- (0, bun_test_1.describe)("addPopulateToQuery", function () {
1494
- (0, bun_test_1.it)("returns query unchanged with no populate paths", function () { return __awaiter(void 0, void 0, void 0, function () {
1495
- var query, result;
1496
- return __generator(this, function (_a) {
1497
- switch (_a.label) {
1498
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1499
- case 1:
1500
- _a.sent();
1501
- query = tests_1.FoodModel.find({});
1502
- result = (0, api_1.addPopulateToQuery)(query, undefined);
1503
- (0, bun_test_1.expect)(result).toBe(query);
1504
- return [2 /*return*/];
1505
- }
1506
- });
1507
- }); });
1508
- (0, bun_test_1.it)("returns query unchanged with empty populate paths", function () { return __awaiter(void 0, void 0, void 0, function () {
1509
- var query, result;
1510
- return __generator(this, function (_a) {
1511
- switch (_a.label) {
1512
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1513
- case 1:
1514
- _a.sent();
1515
- query = tests_1.FoodModel.find({});
1516
- result = (0, api_1.addPopulateToQuery)(query, []);
1517
- (0, bun_test_1.expect)(result).toBe(query);
1518
- return [2 /*return*/];
1519
- }
1520
- });
1521
- }); });
1522
- (0, bun_test_1.it)("applies multiple populate paths", function () { return __awaiter(void 0, void 0, void 0, function () {
1523
- var query, result;
1524
- return __generator(this, function (_a) {
1525
- switch (_a.label) {
1526
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1527
- case 1:
1528
- _a.sent();
1529
- query = tests_1.FoodModel.find({});
1530
- result = (0, api_1.addPopulateToQuery)(query, [
1531
- { fields: ["email"], path: "ownerId" },
1532
- { fields: ["name"], path: "eatenBy" },
1533
- ]);
1534
- // The result should be a query with populate applied
1535
- (0, bun_test_1.expect)(result).toBeDefined();
1536
- return [2 /*return*/];
1537
- }
1538
- });
1539
- }); });
1540
- });
1541
- (0, bun_test_1.describe)("soft delete with isDeleted plugin", function () {
1542
- var admin;
1543
- var agent;
1544
- (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1545
- var _a;
1546
- return __generator(this, function (_b) {
1547
- switch (_b.label) {
1548
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1549
- case 1:
1550
- _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
1551
- app = (0, tests_1.getBaseServer)();
1552
- (0, auth_1.setupAuth)(app, tests_1.UserModel);
1553
- (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
1554
- return [2 /*return*/];
1555
- }
1556
- });
1557
- }); });
1558
- (0, bun_test_1.it)("soft deletes user with deleted field", function () { return __awaiter(void 0, void 0, void 0, function () {
1559
- var res, deletedUser;
1560
- return __generator(this, function (_a) {
1561
- switch (_a.label) {
1562
- case 0:
1563
- // UserModel has the isDisabledPlugin which adds a 'disabled' field,
1564
- // but we need to test the 'deleted' field check.
1565
- // Let's use a model that has the deleted field.
1566
- app.use("/users", (0, api_1.modelRouter)(tests_1.UserModel, {
1567
- allowAnonymous: true,
1568
- permissions: {
1569
- create: [permissions_1.Permissions.IsAny],
1570
- delete: [permissions_1.Permissions.IsAny],
1571
- list: [permissions_1.Permissions.IsAny],
1572
- read: [permissions_1.Permissions.IsAny],
1573
- update: [permissions_1.Permissions.IsAny],
1521
+ postUpdate: function () {
1522
+ throw new Error("postUpdate array failed");
1574
1523
  },
1575
1524
  }));
1576
1525
  server = (0, supertest_1.default)(app);
1577
- return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
1526
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
1578
1527
  case 1:
1579
1528
  agent = _a.sent();
1580
- return [4 /*yield*/, agent.delete("/users/".concat(admin._id)).expect(204)];
1529
+ return [4 /*yield*/, agent.post("/food/".concat(apple._id, "/tags")).send({ tags: "organic" }).expect(400)];
1581
1530
  case 2:
1582
1531
  res = _a.sent();
1583
- (0, bun_test_1.expect)(res.body).toEqual({});
1584
- return [4 /*yield*/, tests_1.UserModel.findById(admin._id)];
1585
- case 3:
1586
- deletedUser = _a.sent();
1587
- (0, bun_test_1.expect)(deletedUser).toBeNull();
1588
- return [2 /*return*/];
1589
- }
1590
- });
1591
- }); });
1592
- });
1593
- (0, bun_test_1.describe)("populate in create", function () {
1594
- var admin;
1595
- (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1596
- var _a;
1597
- return __generator(this, function (_b) {
1598
- switch (_b.label) {
1599
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1600
- case 1:
1601
- _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
1602
- return [4 /*yield*/, tests_1.FoodModel.create({
1603
- calories: 1,
1604
- created: new Date("2021-12-03T00:00:20.000Z"),
1605
- hidden: false,
1606
- name: "Spinach",
1607
- ownerId: admin._id,
1608
- })];
1609
- case 2:
1610
- _b.sent();
1611
- app = (0, tests_1.getBaseServer)();
1612
- (0, auth_1.setupAuth)(app, tests_1.UserModel);
1613
- (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
1614
- return [2 /*return*/];
1615
- }
1616
- });
1617
- }); });
1618
- (0, bun_test_1.it)("handles populate with valid path in create", function () { return __awaiter(void 0, void 0, void 0, function () {
1619
- var res;
1620
- return __generator(this, function (_a) {
1621
- switch (_a.label) {
1622
- case 0:
1623
- // Test that valid populate works in create flow
1624
- app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1625
- allowAnonymous: true,
1626
- permissions: {
1627
- create: [permissions_1.Permissions.IsAny],
1628
- delete: [permissions_1.Permissions.IsAny],
1629
- list: [permissions_1.Permissions.IsAny],
1630
- read: [permissions_1.Permissions.IsAny],
1631
- update: [permissions_1.Permissions.IsAny],
1632
- },
1633
- populatePaths: [{ fields: ["email"], path: "ownerId" }],
1634
- }));
1635
- server = (0, supertest_1.default)(app);
1636
- return [4 /*yield*/, server
1637
- .post("/food")
1638
- .send({ calories: 15, name: "Broccoli", ownerId: admin._id })
1639
- .expect(201)];
1640
- case 1:
1641
- res = _a.sent();
1642
- (0, bun_test_1.expect)(res.body.data.name).toBe("Broccoli");
1643
- // Verify populate worked - ownerId should be an object with email
1644
- (0, bun_test_1.expect)(res.body.data.ownerId.email).toBe(admin.email);
1645
- return [2 /*return*/];
1646
- }
1647
- });
1648
- }); });
1649
- });
1650
- (0, bun_test_1.describe)("save error handling", function () {
1651
- var admin;
1652
- var spinach;
1653
- (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1654
- var _a;
1655
- return __generator(this, function (_b) {
1656
- switch (_b.label) {
1657
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1658
- case 1:
1659
- _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
1660
- return [4 /*yield*/, tests_1.FoodModel.create({
1661
- calories: 1,
1662
- created: new Date("2021-12-03T00:00:20.000Z"),
1663
- hidden: false,
1664
- name: "Spinach",
1665
- ownerId: admin._id,
1666
- source: {
1667
- name: "Brand",
1668
- },
1669
- })];
1670
- case 2:
1671
- spinach = _b.sent();
1672
- app = (0, tests_1.getBaseServer)();
1673
- (0, auth_1.setupAuth)(app, tests_1.UserModel);
1674
- (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
1675
- return [2 /*return*/];
1676
- }
1677
- });
1678
- }); });
1679
- (0, bun_test_1.it)("handles patch save error with validation failure", function () { return __awaiter(void 0, void 0, void 0, function () {
1680
- var res;
1681
- return __generator(this, function (_a) {
1682
- switch (_a.label) {
1683
- case 0:
1684
- // The FoodModel has strict: "throw" which will cause validation errors for unknown fields
1685
- app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1686
- allowAnonymous: true,
1687
- permissions: {
1688
- create: [permissions_1.Permissions.IsAny],
1689
- delete: [permissions_1.Permissions.IsAny],
1690
- list: [permissions_1.Permissions.IsAny],
1691
- read: [permissions_1.Permissions.IsAny],
1692
- update: [permissions_1.Permissions.IsAny],
1693
- },
1694
- }));
1695
- server = (0, supertest_1.default)(app);
1696
- return [4 /*yield*/, server
1697
- .patch("/food/".concat(spinach._id))
1698
- .send({ invalidField: "value" })
1699
- .expect(400)];
1700
- case 1:
1701
- res = _a.sent();
1702
- (0, bun_test_1.expect)(res.body.title).toContain("preUpdate hook save error");
1532
+ (0, bun_test_1.expect)(res.body.title).toContain("PATCH Post Update error");
1703
1533
  return [2 /*return*/];
1704
1534
  }
1705
1535
  });
1706
1536
  }); });
1707
- });
1708
- (0, bun_test_1.describe)("body undefined after transform without preCreate", function () {
1709
- (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1710
- return __generator(this, function (_a) {
1711
- switch (_a.label) {
1712
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1713
- case 1:
1714
- _a.sent();
1715
- app = (0, tests_1.getBaseServer)();
1716
- (0, auth_1.setupAuth)(app, tests_1.UserModel);
1717
- (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
1718
- return [2 /*return*/];
1719
- }
1720
- });
1721
- }); });
1722
- (0, bun_test_1.it)("handles undefined body after transform when no preCreate", function () { return __awaiter(void 0, void 0, void 0, function () {
1537
+ (0, bun_test_1.it)("array operation denied without update permission", function () { return __awaiter(void 0, void 0, void 0, function () {
1723
1538
  var res;
1724
1539
  return __generator(this, function (_a) {
1725
1540
  switch (_a.label) {
1726
1541
  case 0:
1727
- // Create a transformer that returns undefined
1728
1542
  app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1729
1543
  allowAnonymous: true,
1730
1544
  permissions: {
1731
- create: [permissions_1.Permissions.IsAny],
1732
- delete: [permissions_1.Permissions.IsAny],
1733
- list: [permissions_1.Permissions.IsAny],
1734
- read: [permissions_1.Permissions.IsAny],
1735
- update: [permissions_1.Permissions.IsAny],
1736
- },
1737
- transformer: {
1738
- transform: function () { return undefined; },
1739
- },
1740
- }));
1741
- server = (0, supertest_1.default)(app);
1742
- return [4 /*yield*/, server.post("/food").send({ calories: 15, name: "Broccoli" }).expect(400)];
1743
- case 1:
1744
- res = _a.sent();
1745
- (0, bun_test_1.expect)(res.body.title).toBe("Invalid request body");
1746
- (0, bun_test_1.expect)(res.body.detail).toBe("Body is undefined");
1747
- return [2 /*return*/];
1748
- }
1749
- });
1750
- }); });
1751
- });
1752
- (0, bun_test_1.describe)("soft delete with deleted field", function () {
1753
- var agent;
1754
- (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1755
- return __generator(this, function (_a) {
1756
- switch (_a.label) {
1757
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1758
- case 1:
1759
- _a.sent();
1760
- app = (0, tests_1.getBaseServer)();
1761
- (0, auth_1.setupAuth)(app, tests_1.UserModel);
1762
- (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
1763
- return [2 /*return*/];
1764
- }
1765
- });
1766
- }); });
1767
- (0, bun_test_1.it)("soft deletes document with deleted field using isDeletedPlugin", function () { return __awaiter(void 0, void 0, void 0, function () {
1768
- var mongoose, softDeleteSchema, SoftDeleteModel, testDoc, softDeleted;
1769
- return __generator(this, function (_a) {
1770
- switch (_a.label) {
1771
- case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("mongoose")); })];
1772
- case 1:
1773
- mongoose = _a.sent();
1774
- softDeleteSchema = new mongoose.Schema({
1775
- deleted: { default: false, type: Boolean },
1776
- name: String,
1777
- });
1778
- try {
1779
- SoftDeleteModel = mongoose.model("SoftDeleteTest");
1780
- }
1781
- catch (_b) {
1782
- SoftDeleteModel = mongoose.model("SoftDeleteTest", softDeleteSchema);
1783
- }
1784
- // Clean up any existing documents
1785
- return [4 /*yield*/, SoftDeleteModel.deleteMany({})];
1786
- case 2:
1787
- // Clean up any existing documents
1788
- _a.sent();
1789
- return [4 /*yield*/, SoftDeleteModel.create({ name: "TestItem" })];
1790
- case 3:
1791
- testDoc = _a.sent();
1792
- app.use("/softdelete", (0, api_1.modelRouter)(SoftDeleteModel, {
1793
- allowAnonymous: true,
1794
- permissions: {
1795
- create: [permissions_1.Permissions.IsAny],
1796
- delete: [permissions_1.Permissions.IsAny],
1545
+ create: [permissions_1.Permissions.IsAdmin],
1546
+ delete: [permissions_1.Permissions.IsAdmin],
1797
1547
  list: [permissions_1.Permissions.IsAny],
1798
1548
  read: [permissions_1.Permissions.IsAny],
1799
- update: [permissions_1.Permissions.IsAny],
1549
+ update: [permissions_1.Permissions.IsAdmin],
1800
1550
  },
1801
1551
  }));
1802
1552
  server = (0, supertest_1.default)(app);
1803
1553
  return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
1804
- case 4:
1805
- agent = _a.sent();
1806
- // Delete should soft delete (set deleted: true) instead of hard delete
1807
- return [4 /*yield*/, agent.delete("/softdelete/".concat(testDoc._id)).expect(204)];
1808
- case 5:
1809
- // Delete should soft delete (set deleted: true) instead of hard delete
1810
- _a.sent();
1811
- return [4 /*yield*/, SoftDeleteModel.findById(testDoc._id)];
1812
- case 6:
1813
- softDeleted = _a.sent();
1814
- (0, bun_test_1.expect)(softDeleted).not.toBeNull();
1815
- (0, bun_test_1.expect)(softDeleted === null || softDeleted === void 0 ? void 0 : softDeleted.deleted).toBe(true);
1816
- // Clean up
1817
- return [4 /*yield*/, SoftDeleteModel.deleteMany({})];
1818
- case 7:
1819
- // Clean up
1820
- _a.sent();
1821
- return [2 /*return*/];
1822
- }
1823
- });
1824
- }); });
1825
- });
1826
- (0, bun_test_1.describe)("array operation with undefined preUpdate return", function () {
1827
- var admin;
1828
- var apple;
1829
- var agent;
1830
- (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1831
- var _a;
1832
- return __generator(this, function (_b) {
1833
- switch (_b.label) {
1834
- case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
1835
1554
  case 1:
1836
- _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
1837
- return [4 /*yield*/, tests_1.FoodModel.create({
1838
- calories: 100,
1839
- categories: [
1840
- { name: "Fruit", show: true },
1841
- { name: "Popular", show: false },
1842
- ],
1843
- created: new Date("2021-12-03T00:00:30.000Z"),
1844
- hidden: false,
1845
- name: "Apple",
1846
- ownerId: admin._id,
1847
- tags: ["healthy", "cheap"],
1848
- })];
1555
+ agent = _a.sent();
1556
+ return [4 /*yield*/, agent.post("/food/".concat(apple._id, "/tags")).send({ tags: "organic" }).expect(405)];
1849
1557
  case 2:
1850
- apple = _b.sent();
1851
- app = (0, tests_1.getBaseServer)();
1852
- (0, auth_1.setupAuth)(app, tests_1.UserModel);
1853
- (0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
1558
+ res = _a.sent();
1559
+ (0, bun_test_1.expect)(res.body.title).toContain("Access to PATCH");
1854
1560
  return [2 /*return*/];
1855
1561
  }
1856
1562
  });
1857
1563
  }); });
1858
- (0, bun_test_1.it)("array operation preUpdate returning undefined for array POST throws error", function () { return __awaiter(void 0, void 0, void 0, function () {
1859
- var res;
1564
+ (0, bun_test_1.it)("array operation on non-existent document returns 404", function () { return __awaiter(void 0, void 0, void 0, function () {
1565
+ var fakeId, res;
1860
1566
  return __generator(this, function (_a) {
1861
1567
  switch (_a.label) {
1862
1568
  case 0:
@@ -1869,53 +1575,50 @@ var transformers_1 = require("./transformers");
1869
1575
  read: [permissions_1.Permissions.IsAdmin],
1870
1576
  update: [permissions_1.Permissions.IsAdmin],
1871
1577
  },
1872
- preUpdate: function () { return undefined; },
1873
1578
  }));
1874
1579
  server = (0, supertest_1.default)(app);
1875
1580
  return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
1876
1581
  case 1:
1877
1582
  agent = _a.sent();
1878
- return [4 /*yield*/, agent.post("/food/".concat(apple._id, "/tags")).send({ tags: "organic" }).expect(403)];
1583
+ fakeId = "000000000000000000000000";
1584
+ return [4 /*yield*/, agent.post("/food/".concat(fakeId, "/tags")).send({ tags: "organic" }).expect(404)];
1879
1585
  case 2:
1880
1586
  res = _a.sent();
1881
- (0, bun_test_1.expect)(res.body.title).toBe("Update not allowed");
1882
- (0, bun_test_1.expect)(res.body.detail).toBe("A body must be returned from preUpdate");
1587
+ (0, bun_test_1.expect)(res.body.title).toContain("Could not find document to PATCH");
1883
1588
  return [2 /*return*/];
1884
1589
  }
1885
1590
  });
1886
1591
  }); });
1887
- (0, bun_test_1.it)("array operation preUpdate returning null for array PATCH throws error", function () { return __awaiter(void 0, void 0, void 0, function () {
1592
+ (0, bun_test_1.it)("array operation denied when user cannot update specific doc", function () { return __awaiter(void 0, void 0, void 0, function () {
1888
1593
  var res;
1889
1594
  return __generator(this, function (_a) {
1890
1595
  switch (_a.label) {
1891
1596
  case 0:
1597
+ // Create food owned by admin, then try to update as notAdmin
1892
1598
  app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1893
1599
  allowAnonymous: true,
1894
1600
  permissions: {
1895
- create: [permissions_1.Permissions.IsAdmin],
1896
- delete: [permissions_1.Permissions.IsAdmin],
1897
- list: [permissions_1.Permissions.IsAdmin],
1898
- read: [permissions_1.Permissions.IsAdmin],
1899
- update: [permissions_1.Permissions.IsAdmin],
1601
+ create: [permissions_1.Permissions.IsAuthenticated],
1602
+ delete: [permissions_1.Permissions.IsAuthenticated],
1603
+ list: [permissions_1.Permissions.IsAuthenticated],
1604
+ read: [permissions_1.Permissions.IsAuthenticated],
1605
+ update: [permissions_1.Permissions.IsOwner],
1900
1606
  },
1901
- preUpdate: function () { return null; },
1902
1607
  }));
1903
1608
  server = (0, supertest_1.default)(app);
1904
- return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
1609
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
1905
1610
  case 1:
1611
+ // Login as notAdmin and try to update admin's food (apple)
1906
1612
  agent = _a.sent();
1907
- return [4 /*yield*/, agent
1908
- .patch("/food/".concat(apple._id, "/tags/healthy"))
1909
- .send({ tags: "unhealthy" })
1910
- .expect(403)];
1613
+ return [4 /*yield*/, agent.post("/food/".concat(apple._id, "/tags")).send({ tags: "organic" }).expect(403)];
1911
1614
  case 2:
1912
1615
  res = _a.sent();
1913
- (0, bun_test_1.expect)(res.body.title).toBe("Update not allowed");
1616
+ (0, bun_test_1.expect)(res.body.title).toContain("Patch not allowed");
1914
1617
  return [2 /*return*/];
1915
1618
  }
1916
1619
  });
1917
1620
  }); });
1918
- (0, bun_test_1.it)("array operation preUpdate error for array DELETE is handled", function () { return __awaiter(void 0, void 0, void 0, function () {
1621
+ (0, bun_test_1.it)("array operation transform error is handled", function () { return __awaiter(void 0, void 0, void 0, function () {
1919
1622
  var res;
1920
1623
  return __generator(this, function (_a) {
1921
1624
  switch (_a.label) {
@@ -1929,18 +1632,18 @@ var transformers_1 = require("./transformers");
1929
1632
  read: [permissions_1.Permissions.IsAdmin],
1930
1633
  update: [permissions_1.Permissions.IsAdmin],
1931
1634
  },
1932
- preUpdate: function () {
1933
- throw new Error("preUpdate error during delete");
1934
- },
1635
+ transformer: (0, transformers_1.AdminOwnerTransformer)({
1636
+ adminWriteFields: ["name"],
1637
+ }),
1935
1638
  }));
1936
1639
  server = (0, supertest_1.default)(app);
1937
1640
  return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
1938
1641
  case 1:
1939
1642
  agent = _a.sent();
1940
- return [4 /*yield*/, agent.delete("/food/".concat(apple._id, "/tags/healthy")).expect(400)];
1643
+ return [4 /*yield*/, agent.post("/food/".concat(apple._id, "/tags")).send({ tags: "organic" }).expect(403)];
1941
1644
  case 2:
1942
1645
  res = _a.sent();
1943
- (0, bun_test_1.expect)(res.body.title).toContain("preUpdate hook error");
1646
+ (0, bun_test_1.expect)(res.body.title).toContain("cannot write fields");
1944
1647
  return [2 /*return*/];
1945
1648
  }
1946
1649
  });
@@ -1949,7 +1652,7 @@ var transformers_1 = require("./transformers");
1949
1652
  (0, bun_test_1.describe)("transformer errors", function () {
1950
1653
  var admin;
1951
1654
  var spinach;
1952
- var _agent;
1655
+ var agent;
1953
1656
  (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
1954
1657
  var _a;
1955
1658
  return __generator(this, function (_b) {
@@ -2058,6 +1761,41 @@ var transformers_1 = require("./transformers");
2058
1761
  }
2059
1762
  });
2060
1763
  }); });
1764
+ (0, bun_test_1.it)("preDelete hook throwing APIError is re-thrown", function () { return __awaiter(void 0, void 0, void 0, function () {
1765
+ var res;
1766
+ return __generator(this, function (_a) {
1767
+ switch (_a.label) {
1768
+ case 0:
1769
+ app.use("/food", (0, api_1.modelRouter)(tests_1.FoodModel, {
1770
+ allowAnonymous: true,
1771
+ permissions: {
1772
+ create: [permissions_1.Permissions.IsAny],
1773
+ delete: [permissions_1.Permissions.IsAny],
1774
+ list: [permissions_1.Permissions.IsAny],
1775
+ read: [permissions_1.Permissions.IsAny],
1776
+ update: [permissions_1.Permissions.IsAny],
1777
+ },
1778
+ preDelete: function () {
1779
+ throw new errors_1.APIError({
1780
+ disableExternalErrorTracking: true,
1781
+ status: 400,
1782
+ title: "Custom preDelete APIError",
1783
+ });
1784
+ },
1785
+ }));
1786
+ server = (0, supertest_1.default)(app);
1787
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "notAdmin")];
1788
+ case 1:
1789
+ agent = _a.sent();
1790
+ return [4 /*yield*/, agent.delete("/food/".concat(spinach._id)).expect(400)];
1791
+ case 2:
1792
+ res = _a.sent();
1793
+ (0, bun_test_1.expect)(res.body.title).toBe("Custom preDelete APIError");
1794
+ (0, bun_test_1.expect)(res.body.disableExternalErrorTracking).toBe(true);
1795
+ return [2 /*return*/];
1796
+ }
1797
+ });
1798
+ }); });
2061
1799
  });
2062
1800
  (0, bun_test_1.describe)("special query params", function () {
2063
1801
  var admin;