@dyrected/core 2.7.1 → 2.8.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 (43) hide show
  1. package/dist/app-config-7G-3tF_I.d.cts +2259 -0
  2. package/dist/app-config-7G-3tF_I.d.ts +2259 -0
  3. package/dist/app-config-B3ByBjCN.d.cts +2222 -0
  4. package/dist/app-config-B3ByBjCN.d.ts +2222 -0
  5. package/dist/app-config-BIkjQWzU.d.cts +2218 -0
  6. package/dist/app-config-BIkjQWzU.d.ts +2218 -0
  7. package/dist/app-config-BfpeE3bF.d.cts +2100 -0
  8. package/dist/app-config-BfpeE3bF.d.ts +2100 -0
  9. package/dist/app-config-CCe-yq9q.d.cts +2222 -0
  10. package/dist/app-config-CCe-yq9q.d.ts +2222 -0
  11. package/dist/app-config-CPOs7THF.d.cts +2217 -0
  12. package/dist/app-config-CPOs7THF.d.ts +2217 -0
  13. package/dist/app-config-Ctp454zB.d.cts +2214 -0
  14. package/dist/app-config-Ctp454zB.d.ts +2214 -0
  15. package/dist/app-config-DJmNYgYh.d.cts +2305 -0
  16. package/dist/app-config-DJmNYgYh.d.ts +2305 -0
  17. package/dist/app-config-DO3laV58.d.cts +2397 -0
  18. package/dist/app-config-DO3laV58.d.ts +2397 -0
  19. package/dist/app-config-DZf_mmi9.d.cts +2211 -0
  20. package/dist/app-config-DZf_mmi9.d.ts +2211 -0
  21. package/dist/app-config-SkUoN5JU.d.cts +2192 -0
  22. package/dist/app-config-SkUoN5JU.d.ts +2192 -0
  23. package/dist/app-config-oz8H_MOX.d.cts +2224 -0
  24. package/dist/app-config-oz8H_MOX.d.ts +2224 -0
  25. package/dist/chunk-3TRRTN6U.js +3441 -0
  26. package/dist/chunk-5ZIA5COH.js +3414 -0
  27. package/dist/chunk-6TWQ6GTD.js +3480 -0
  28. package/dist/chunk-AQO4HI2Y.js +3204 -0
  29. package/dist/chunk-DFBLNYUH.js +3402 -0
  30. package/dist/chunk-KEKOEAX6.js +3139 -0
  31. package/dist/chunk-PRQ2RF5C.js +3485 -0
  32. package/dist/chunk-PZMDK3MH.js +3203 -0
  33. package/dist/chunk-SM32ND5M.js +3472 -0
  34. package/dist/chunk-XX7N64NL.js +3550 -0
  35. package/dist/index.cjs +446 -9
  36. package/dist/index.d.cts +166 -3
  37. package/dist/index.d.ts +166 -3
  38. package/dist/index.js +53 -1
  39. package/dist/server.cjs +615 -363
  40. package/dist/server.d.cts +26 -7
  41. package/dist/server.d.ts +26 -7
  42. package/dist/server.js +412 -355
  43. package/package.json +1 -1
package/dist/server.cjs CHANGED
@@ -627,6 +627,7 @@ __export(server_exports, {
627
627
  registerRoutes: () => registerRoutes,
628
628
  renderPrometheusMetrics: () => renderPrometheusMetrics,
629
629
  resolveObservabilityConfig: () => resolveObservabilityConfig,
630
+ serializeDetailForApi: () => serializeDetailForApi,
630
631
  shouldSampleRequest: () => shouldSampleRequest
631
632
  });
632
633
  module.exports = __toCommonJS(server_exports);
@@ -1259,9 +1260,42 @@ function round(val, decimals = 0) {
1259
1260
  }
1260
1261
  function clamp(val, minVal, maxVal) {
1261
1262
  const num = Number(val) || 0;
1262
- const min = Number(minVal) || 0;
1263
- const max = Number(maxVal) || 0;
1264
- return Math.min(Math.max(num, min), max);
1263
+ const min2 = Number(minVal) || 0;
1264
+ const max2 = Number(maxVal) || 0;
1265
+ return Math.min(Math.max(num, min2), max2);
1266
+ }
1267
+ function ceil(val) {
1268
+ return Math.ceil(Number(val) || 0);
1269
+ }
1270
+ function floor(val) {
1271
+ return Math.floor(Number(val) || 0);
1272
+ }
1273
+ function abs(val) {
1274
+ return Math.abs(Number(val) || 0);
1275
+ }
1276
+ function min(...args) {
1277
+ const nums = args.flat().map((n) => Number(n) || 0);
1278
+ return nums.length ? Math.min(...nums) : 0;
1279
+ }
1280
+ function max(...args) {
1281
+ const nums = args.flat().map((n) => Number(n) || 0);
1282
+ return nums.length ? Math.max(...nums) : 0;
1283
+ }
1284
+ function pow(base, exp) {
1285
+ return Math.pow(Number(base) || 0, Number(exp) || 0);
1286
+ }
1287
+ function sqrt(val) {
1288
+ return Math.sqrt(Number(val) || 0);
1289
+ }
1290
+ function sum(arr) {
1291
+ if (!Array.isArray(arr)) return Number(arr) || 0;
1292
+ return arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
1293
+ }
1294
+ function count(val) {
1295
+ if (Array.isArray(val)) return val.length;
1296
+ if (typeof val === "string") return val.length;
1297
+ if (val !== void 0 && val !== null && val !== false) return 1;
1298
+ return 0;
1265
1299
  }
1266
1300
  function defaultValue(val, fallback) {
1267
1301
  if (val === void 0 || val === null || val === "") return fallback;
@@ -1280,13 +1314,12 @@ function isEmpty(val) {
1280
1314
  return false;
1281
1315
  }
1282
1316
  function get(obj, path, fallback = null) {
1283
- if (!obj || typeof obj !== "object") return fallback;
1284
- const pathStr = String(path || "").trim();
1285
- if (!pathStr) return fallback;
1286
- const parts = pathStr.split(".");
1317
+ if (obj === void 0 || obj === null || typeof obj !== "object") return fallback;
1318
+ if (typeof path !== "string") return fallback;
1319
+ const parts = path.split(".").filter(Boolean);
1287
1320
  let curr = obj;
1288
1321
  for (const part of parts) {
1289
- if (curr === void 0 || curr === null) return fallback;
1322
+ if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
1290
1323
  curr = curr[part];
1291
1324
  }
1292
1325
  return curr === void 0 || curr === null ? fallback : curr;
@@ -1324,6 +1357,15 @@ function registerJexlHelpers(jexlInstance = import_jexl2.default) {
1324
1357
  // Math
1325
1358
  round,
1326
1359
  clamp,
1360
+ ceil,
1361
+ floor,
1362
+ abs,
1363
+ min,
1364
+ max,
1365
+ pow,
1366
+ sqrt,
1367
+ sum,
1368
+ count,
1327
1369
  // Objects & Logical
1328
1370
  default: defaultValue,
1329
1371
  coalesce,
@@ -1335,9 +1377,33 @@ function registerJexlHelpers(jexlInstance = import_jexl2.default) {
1335
1377
  jexlInstance.addFunction(name, fn);
1336
1378
  } catch {
1337
1379
  }
1380
+ try {
1381
+ jexlInstance.addTransform(name, fn);
1382
+ } catch {
1383
+ }
1384
+ }
1385
+ try {
1386
+ jexlInstance.addBinaryOp("and", 20, (left, right) => Boolean(left && right));
1387
+ jexlInstance.addBinaryOp("or", 10, (left, right) => Boolean(left || right));
1388
+ } catch {
1338
1389
  }
1339
1390
  }
1340
1391
  registerJexlHelpers(import_jexl2.default);
1392
+ async function evaluateJexl(expression, context = {}) {
1393
+ if (!expression || typeof expression !== "string") return void 0;
1394
+ try {
1395
+ const evalContext = {
1396
+ null: null,
1397
+ undefined: void 0,
1398
+ ...context,
1399
+ math: Math,
1400
+ Math
1401
+ };
1402
+ return await import_jexl2.default.eval(expression, evalContext);
1403
+ } catch (_err) {
1404
+ return void 0;
1405
+ }
1406
+ }
1341
1407
  var BUILTIN_JEXL_HELPERS = [
1342
1408
  // Strings
1343
1409
  "slugify",
@@ -1919,7 +1985,7 @@ function collectConfigDiagnostics(config) {
1919
1985
  function formatConfigDiagnostics(source, diagnostics, options = {}) {
1920
1986
  const color = options.color ?? false;
1921
1987
  const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
1922
- const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/features/admin/form-and-field-hooks", 34, color)}`;
1988
+ const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/editor-experience/form-and-field-hooks", 34, color)}`;
1923
1989
  const lines = diagnostics.flatMap((issue, index) => [
1924
1990
  `${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
1925
1991
  ` ${issue.message}`,
@@ -3015,6 +3081,71 @@ async function transitionWorkflow(args) {
3015
3081
 
3016
3082
  // src/controllers/collection.controller.ts
3017
3083
  init_observability();
3084
+
3085
+ // src/detail.ts
3086
+ function normalizeDetailItem(item) {
3087
+ if (typeof item === "string") {
3088
+ return {
3089
+ type: "field",
3090
+ field: item
3091
+ };
3092
+ }
3093
+ return item;
3094
+ }
3095
+ async function evaluateDetailComputed(detail, doc, user, db) {
3096
+ if (!doc || !detail || !Array.isArray(detail)) {
3097
+ return doc;
3098
+ }
3099
+ const computedItems = [];
3100
+ const extractComputed = (items) => {
3101
+ for (const rawItem of items) {
3102
+ if (!rawItem) continue;
3103
+ const item = normalizeDetailItem(rawItem);
3104
+ if (item.type === "computed") {
3105
+ const id = item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_");
3106
+ if (id) {
3107
+ computedItems.push({
3108
+ id,
3109
+ label: item.label,
3110
+ expression: item.expression,
3111
+ handler: item.handler
3112
+ });
3113
+ }
3114
+ } else if (item.type === "section" || item.type === "grid" || item.type === "repeat") {
3115
+ if (item.items && Array.isArray(item.items)) {
3116
+ extractComputed(item.items);
3117
+ }
3118
+ } else if (item.type === "tabs" && item.tabs && Array.isArray(item.tabs)) {
3119
+ for (const tab of item.tabs) {
3120
+ if (tab.items) extractComputed(tab.items);
3121
+ }
3122
+ }
3123
+ }
3124
+ };
3125
+ extractComputed(detail);
3126
+ if (computedItems.length === 0) return doc;
3127
+ const evaluated = {};
3128
+ for (const item of computedItems) {
3129
+ try {
3130
+ if (typeof item.handler === "function") {
3131
+ evaluated[item.id] = await item.handler({ doc, user, db });
3132
+ } else if (item.expression) {
3133
+ evaluated[item.id] = await evaluateJexl(item.expression, { doc, user });
3134
+ }
3135
+ } catch (_err) {
3136
+ evaluated[item.id] = null;
3137
+ }
3138
+ }
3139
+ return {
3140
+ ...doc,
3141
+ _meta: {
3142
+ ...doc._meta || {},
3143
+ computed: evaluated
3144
+ }
3145
+ };
3146
+ }
3147
+
3148
+ // src/controllers/collection.controller.ts
3018
3149
  var CollectionController = class {
3019
3150
  collection;
3020
3151
  constructor(collection) {
@@ -3031,6 +3162,22 @@ var CollectionController = class {
3031
3162
  toHookRequestContext(c) {
3032
3163
  return toHookRequestContext(c.req);
3033
3164
  }
3165
+ sanitizeDoc(doc) {
3166
+ if (!doc || typeof doc !== "object") return doc;
3167
+ if (this.collection.auth) {
3168
+ const {
3169
+ password: _p,
3170
+ salt: _s,
3171
+ resetPasswordToken: _rpt,
3172
+ resetPasswordExpires: _rpe,
3173
+ loginAttempts: _la,
3174
+ lockUntil: _lu,
3175
+ ...safeDoc
3176
+ } = doc;
3177
+ return safeDoc;
3178
+ }
3179
+ return doc;
3180
+ }
3034
3181
  async evaluateAccess(c, action, options = {}) {
3035
3182
  const config = c.get("config");
3036
3183
  return resolveCollectionAccess(
@@ -3263,6 +3410,9 @@ var CollectionController = class {
3263
3410
  depth
3264
3411
  );
3265
3412
  }
3413
+ if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
3414
+ result.docs = result.docs.map((d) => this.sanitizeDoc(d));
3415
+ }
3266
3416
  return c.json(result);
3267
3417
  }
3268
3418
  async findOne(c) {
@@ -3279,32 +3429,51 @@ var CollectionController = class {
3279
3429
  id: id2
3280
3430
  });
3281
3431
  const externalSubject = localDoc?.externalSubject || id2;
3282
- let member = null;
3283
3432
  if (provider.members.get) {
3284
- member = await provider.members.get({ externalSubject, req: hookReq });
3433
+ const member = await provider.members.get({
3434
+ externalSubject,
3435
+ req: hookReq
3436
+ });
3437
+ if (member) {
3438
+ return c.json({
3439
+ ...member,
3440
+ id: localDoc ? localDoc.id : member.id,
3441
+ externalSubject: member.id
3442
+ });
3443
+ }
3285
3444
  } else if (provider.members.list) {
3286
3445
  const listResult = await provider.members.list({ req: hookReq });
3287
- member = listResult.docs.find((m) => m.id === externalSubject) || null;
3446
+ const matched = listResult.docs.find((m) => m.id === externalSubject || m.id === id2);
3447
+ if (matched) {
3448
+ return c.json({
3449
+ ...matched,
3450
+ id: localDoc ? localDoc.id : matched.id,
3451
+ externalSubject: matched.id
3452
+ });
3453
+ }
3288
3454
  }
3289
- if (!member) return c.json({ message: "Not Found" }, 404);
3290
- return c.json({
3291
- ...member,
3292
- id: localDoc ? localDoc.id : member.id,
3293
- externalSubject: member.id
3294
- });
3295
3455
  }
3296
- const readonlyDb = createReadonlyDb(db);
3297
3456
  const id = c.req.param("id");
3298
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
3299
- const user = c.get("user");
3300
3457
  if (!id) return c.json({ message: "Missing ID" }, 400);
3301
- let doc = await db.findOne({ collection: this.collection.slug, id });
3302
- if (!doc) return c.json({ message: "Not Found" }, 404);
3458
+ const depth = Number.parseInt(c.req.query("depth") || "0", 10);
3459
+ const user = c.get("user");
3460
+ const doc = await db.findOne({
3461
+ collection: this.collection.slug,
3462
+ id
3463
+ });
3464
+ if (!doc) {
3465
+ return c.json({ message: "Document not found" }, 404);
3466
+ }
3467
+ let effectiveDoc = doc;
3303
3468
  if (this.collection.workflow) {
3304
- doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
3305
- if (!doc) return c.json({ message: "Not Found" }, 404);
3469
+ effectiveDoc = materializeWorkflowDocument(
3470
+ doc,
3471
+ this.collection.workflow,
3472
+ user
3473
+ );
3474
+ if (!effectiveDoc) return c.json({ message: "Document not found" }, 404);
3306
3475
  }
3307
- const access = await this.evaluateAccess(c, "read", { id, doc });
3476
+ const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
3308
3477
  if (!access.allowed) {
3309
3478
  return c.json(
3310
3479
  {
@@ -3314,7 +3483,11 @@ var CollectionController = class {
3314
3483
  403
3315
3484
  );
3316
3485
  }
3317
- const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
3486
+ const docWithDefaults = DefaultsService.apply(
3487
+ this.collection.fields,
3488
+ effectiveDoc
3489
+ );
3490
+ const readonlyDb = createReadonlyDb(db);
3318
3491
  const docWithCollectionHooks = await runCollectionHooks(
3319
3492
  this.collection.hooks?.afterRead,
3320
3493
  {
@@ -3340,17 +3513,20 @@ var CollectionController = class {
3340
3513
  },
3341
3514
  docWithFieldHooks
3342
3515
  );
3516
+ let finalDoc = docWithFieldAccess;
3343
3517
  if (depth > 0 && docWithFieldAccess) {
3344
3518
  const populationService = new PopulationService(db, config.collections);
3345
- const populatedDoc = await populationService.populate({
3519
+ finalDoc = await populationService.populate({
3346
3520
  data: docWithFieldAccess,
3347
3521
  fields: this.collection.fields,
3348
3522
  currentDepth: 0,
3349
3523
  maxDepth: depth
3350
3524
  });
3351
- return c.json(populatedDoc);
3352
3525
  }
3353
- return c.json(docWithFieldAccess);
3526
+ if (finalDoc && this.collection.detail) {
3527
+ finalDoc = await evaluateDetailComputed(this.collection.detail, finalDoc, user, readonlyDb);
3528
+ }
3529
+ return c.json(this.sanitizeDoc(finalDoc));
3354
3530
  }
3355
3531
  async create(c) {
3356
3532
  const config = c.get("config");
@@ -4122,6 +4298,63 @@ var CollectionController = class {
4122
4298
  201
4123
4299
  );
4124
4300
  }
4301
+ async aggregate(c) {
4302
+ const config = c.get("config");
4303
+ const db = config.db;
4304
+ if (!db) return c.json({ message: "Database not configured" }, 500);
4305
+ const access = await this.evaluateAccess(c, "read");
4306
+ if (!access.allowed) {
4307
+ return c.json(
4308
+ {
4309
+ error: true,
4310
+ message: `Access denied: read on ${this.collection.slug}`
4311
+ },
4312
+ 403
4313
+ );
4314
+ }
4315
+ let body;
4316
+ try {
4317
+ body = await c.req.json();
4318
+ } catch {
4319
+ return c.json({ message: "Invalid JSON body" }, 400);
4320
+ }
4321
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
4322
+ return c.json(
4323
+ { message: "Aggregate request body must be a JSON object" },
4324
+ 400
4325
+ );
4326
+ }
4327
+ const { sanitizeWhereClause: sanitizeWhereClause2 } = await Promise.resolve().then(() => (init_where_sanitizer(), where_sanitizer_exports));
4328
+ const sanitizedAggregates = {};
4329
+ for (const [key, op] of Object.entries(body)) {
4330
+ if (!op || typeof op !== "object" || Array.isArray(op)) {
4331
+ return c.json(
4332
+ {
4333
+ message: `Aggregate operation "${key}" must be an object`
4334
+ },
4335
+ 400
4336
+ );
4337
+ }
4338
+ const operation = op;
4339
+ let opWhere = operation.where && typeof operation.where === "object" ? operation.where : void 0;
4340
+ if (opWhere) {
4341
+ opWhere = sanitizeWhereClause2(opWhere, this.collection.fields);
4342
+ if (Object.keys(opWhere).length === 0) opWhere = void 0;
4343
+ }
4344
+ if (access.constraint) {
4345
+ opWhere = opWhere ? mergeWhereConstraint(opWhere, access.constraint) : access.constraint;
4346
+ }
4347
+ sanitizedAggregates[key] = {
4348
+ ...operation,
4349
+ ...opWhere !== void 0 ? { where: opWhere } : { where: void 0 }
4350
+ };
4351
+ }
4352
+ const result = await db.aggregate({
4353
+ collection: this.collection.slug,
4354
+ aggregates: sanitizedAggregates
4355
+ });
4356
+ return c.json(result);
4357
+ }
4125
4358
  };
4126
4359
 
4127
4360
  // src/controllers/global.controller.ts
@@ -4205,17 +4438,25 @@ var GlobalController = class {
4205
4438
  },
4206
4439
  docWithFieldHooks
4207
4440
  );
4441
+ let finalDoc = docWithFieldAccess;
4208
4442
  if (depth > 0 && docWithFieldAccess) {
4209
4443
  const populationService = new PopulationService(db, config.collections);
4210
- const populatedData = await populationService.populate({
4444
+ finalDoc = await populationService.populate({
4211
4445
  data: docWithFieldAccess,
4212
4446
  fields: this.global.fields,
4213
4447
  currentDepth: 0,
4214
4448
  maxDepth: depth
4215
4449
  });
4216
- return c.json(populatedData);
4217
4450
  }
4218
- return c.json(docWithFieldAccess);
4451
+ if (finalDoc && this.global.detail) {
4452
+ finalDoc = await evaluateDetailComputed(
4453
+ this.global.detail,
4454
+ finalDoc,
4455
+ user,
4456
+ readonlyDb
4457
+ );
4458
+ }
4459
+ return c.json(finalDoc);
4219
4460
  }
4220
4461
  async update(c) {
4221
4462
  const config = c.get("config");
@@ -5668,6 +5909,7 @@ var AdminAuthController = class {
5668
5909
  });
5669
5910
  }
5670
5911
  getProvider(config, id) {
5912
+ if (!id) return void 0;
5671
5913
  if (config.adminAuth?.mode !== "external") return void 0;
5672
5914
  return config.adminAuth.providers.find((provider) => provider.id === id);
5673
5915
  }
@@ -5866,17 +6108,16 @@ var AdminAuthController = class {
5866
6108
  throw new Error("OIDC nonce verification failed.");
5867
6109
  }
5868
6110
  claims = payload;
5869
- } else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
5870
- const userInfo = await fetch(
5871
- provider.userInfoEndpoint || discovery.userinfo_endpoint,
5872
- {
5873
- headers: { Authorization: `Bearer ${tokenBody.access_token}` }
5874
- }
5875
- );
6111
+ } else {
6112
+ const userInfoEndpoint = provider.userInfoEndpoint ?? discovery.userinfo_endpoint;
6113
+ if (!tokenBody.access_token || !userInfoEndpoint) {
6114
+ throw new Error("OIDC provider did not return usable identity claims.");
6115
+ }
6116
+ const userInfo = await fetch(userInfoEndpoint, {
6117
+ headers: { Authorization: `Bearer ${tokenBody.access_token}` }
6118
+ });
5876
6119
  if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
5877
6120
  claims = await userInfo.json();
5878
- } else {
5879
- throw new Error("OIDC provider did not return usable identity claims.");
5880
6121
  }
5881
6122
  return {
5882
6123
  identity: this.mapClaims(claims, provider.claimMapping),
@@ -6791,6 +7032,71 @@ function generateOpenApi(config) {
6791
7032
  responses: { 200: { description: "Deleted and failed document IDs" } }
6792
7033
  }
6793
7034
  };
7035
+ spec.paths[`${path}/aggregate`] = {
7036
+ post: {
7037
+ tags: [collectionTag],
7038
+ summary: `Aggregate ${labels.plural}`,
7039
+ description: `Compute statistical aggregations (count, sum, avg, min, max) across ${labels.plural}.`,
7040
+ requestBody: {
7041
+ required: true,
7042
+ content: {
7043
+ "application/json": {
7044
+ schema: {
7045
+ type: "object",
7046
+ additionalProperties: {
7047
+ type: "object",
7048
+ properties: {
7049
+ count: { type: "string", example: "*" },
7050
+ sum: { type: "string", description: "Field to sum" },
7051
+ avg: { type: "string", description: "Field to average" },
7052
+ min: { type: "string", description: "Field for minimum" },
7053
+ max: { type: "string", description: "Field for maximum" },
7054
+ cast: {
7055
+ type: "string",
7056
+ enum: [
7057
+ "number",
7058
+ "integer",
7059
+ "float",
7060
+ "boolean",
7061
+ "date",
7062
+ "string"
7063
+ ]
7064
+ },
7065
+ where: {
7066
+ type: "object",
7067
+ description: "Per-aggregate filter condition",
7068
+ additionalProperties: true
7069
+ }
7070
+ }
7071
+ }
7072
+ }
7073
+ }
7074
+ }
7075
+ },
7076
+ responses: {
7077
+ 200: {
7078
+ description: "Success",
7079
+ content: {
7080
+ "application/json": {
7081
+ schema: {
7082
+ type: "object",
7083
+ additionalProperties: {
7084
+ type: "number",
7085
+ nullable: true
7086
+ }
7087
+ }
7088
+ }
7089
+ }
7090
+ },
7091
+ 400: {
7092
+ description: "Bad Request - Invalid aggregate input"
7093
+ },
7094
+ 403: {
7095
+ description: "Forbidden - Access denied by read rule"
7096
+ }
7097
+ }
7098
+ }
7099
+ };
6794
7100
  if (collection.audit) {
6795
7101
  spec.paths[`${path}/__audit`] = {
6796
7102
  get: {
@@ -7387,10 +7693,7 @@ function accessGate(config, target, action) {
7387
7693
  req: toHookRequestContext(c.req)
7388
7694
  });
7389
7695
  if (!allowed) {
7390
- return c.json(
7391
- { error: true, message: `Access denied: ${action} on ${target.slug}` },
7392
- 403
7393
- );
7696
+ return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
7394
7697
  }
7395
7698
  await next();
7396
7699
  };
@@ -7430,6 +7733,80 @@ function serializeBlockForApi(block) {
7430
7733
  fields: block.fields?.map(serializeFieldForApi)
7431
7734
  };
7432
7735
  }
7736
+ function serializeDetailItemForApi(item) {
7737
+ if (!item) return item;
7738
+ if (typeof item === "string") {
7739
+ return { type: "field", field: item };
7740
+ }
7741
+ if (item.type === "section") {
7742
+ return {
7743
+ type: "section",
7744
+ title: item.title,
7745
+ items: (item.items || []).map(serializeDetailItemForApi),
7746
+ options: item.options
7747
+ };
7748
+ }
7749
+ if (item.type === "tab") {
7750
+ return {
7751
+ type: "tab",
7752
+ label: item.label,
7753
+ items: (item.items || []).map(serializeDetailItemForApi),
7754
+ options: item.options
7755
+ };
7756
+ }
7757
+ if (item.type === "tabs") {
7758
+ return {
7759
+ type: "tabs",
7760
+ tabs: (item.tabs || []).map(serializeDetailItemForApi),
7761
+ options: item.options
7762
+ };
7763
+ }
7764
+ if (item.type === "grid") {
7765
+ return {
7766
+ type: "grid",
7767
+ columns: item.columns,
7768
+ items: (item.items || []).map(serializeDetailItemForApi),
7769
+ options: item.options
7770
+ };
7771
+ }
7772
+ if (item.type === "repeat") {
7773
+ return {
7774
+ type: "repeat",
7775
+ field: item.field,
7776
+ items: (item.items || []).map(serializeDetailItemForApi),
7777
+ options: item.options
7778
+ };
7779
+ }
7780
+ if (item.type === "computed") {
7781
+ return {
7782
+ type: "computed",
7783
+ id: item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
7784
+ label: item.label,
7785
+ expression: item.expression,
7786
+ options: {
7787
+ id: item.id,
7788
+ span: item.options?.span,
7789
+ format: item.options?.format,
7790
+ currency: item.options?.currency,
7791
+ visible: item.options?.visible
7792
+ }
7793
+ };
7794
+ }
7795
+ if (item.type === "field") {
7796
+ return {
7797
+ type: "field",
7798
+ field: item.field,
7799
+ options: item.options
7800
+ };
7801
+ }
7802
+ return item;
7803
+ }
7804
+ function serializeDetailForApi(detail) {
7805
+ if (detail === false) return false;
7806
+ if (detail === true) return true;
7807
+ if (!detail || !Array.isArray(detail)) return void 0;
7808
+ return detail.map(serializeDetailItemForApi);
7809
+ }
7433
7810
  function registerRoutes(app, config) {
7434
7811
  const optionsCache = /* @__PURE__ */ new Map();
7435
7812
  app.get("/api/schemas", optionalAuth(config), async (c) => {
@@ -7448,15 +7825,12 @@ function registerRoutes(app, config) {
7448
7825
  if (typeof access === "boolean") return access;
7449
7826
  if (access && typeof access === "object" && typeof access.policy === "string") {
7450
7827
  const policy = requestConfig.accessPolicies?.[access.policy];
7451
- if (typeof policy === "string" || typeof policy === "boolean")
7452
- return policy;
7828
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
7453
7829
  }
7454
7830
  return resolveBooleanAccess(requestConfig, access, accessArgs);
7455
7831
  };
7456
7832
  const filteredCollections = await Promise.all(
7457
- collections.filter(
7458
- (col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
7459
- ).map(async (col) => ({
7833
+ collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
7460
7834
  slug: col.slug,
7461
7835
  labels: col.labels,
7462
7836
  access: {
@@ -7494,6 +7868,7 @@ function registerRoutes(app, config) {
7494
7868
  audit: !!col.audit,
7495
7869
  drafts: !!col.drafts,
7496
7870
  admin: col.admin,
7871
+ detail: serializeDetailForApi(col.detail),
7497
7872
  workflow: col.workflow ? {
7498
7873
  initialState: col.workflow.initialState,
7499
7874
  draftState: col.workflow.draftState,
@@ -7512,9 +7887,7 @@ function registerRoutes(app, config) {
7512
7887
  }))
7513
7888
  );
7514
7889
  const filteredGlobals = await Promise.all(
7515
- globals.filter(
7516
- (glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
7517
- ).map(async (glb) => ({
7890
+ globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
7518
7891
  slug: glb.slug,
7519
7892
  label: glb.label,
7520
7893
  access: {
@@ -7544,7 +7917,8 @@ function registerRoutes(app, config) {
7544
7917
  }
7545
7918
  }))
7546
7919
  ),
7547
- admin: glb.admin
7920
+ admin: glb.admin,
7921
+ detail: serializeDetailForApi(glb.detail)
7548
7922
  }))
7549
7923
  );
7550
7924
  return c.json({
@@ -7558,148 +7932,137 @@ function registerRoutes(app, config) {
7558
7932
  adminHealth: {
7559
7933
  emailConfigured: !!requestConfig.email,
7560
7934
  secureAuthSecretConfigured: !!process.env.DYRECTED_JWT_SECRET,
7561
- authCollectionConfigured: requestConfig.collections.some(
7562
- (collection) => !!collection.auth
7563
- ),
7564
- uploadCollectionConfigured: requestConfig.collections.some(
7565
- (collection) => !!collection.upload
7566
- )
7935
+ authCollectionConfigured: requestConfig.collections.some((collection) => !!collection.auth),
7936
+ uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload)
7567
7937
  }
7568
7938
  });
7569
7939
  });
7570
- app.get(
7571
- "/api/dyrected/options/:collection/:field",
7572
- optionalAuth(config),
7573
- async (c) => {
7574
- const { collection: colSlug, field: fieldName } = c.req.param();
7575
- const siteId = c.req.header("X-Site-Id");
7576
- const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
7577
- let collections = [...requestConfig.collections];
7578
- const user = c.get("user");
7579
- let collection = collections.find((col) => col.slug === colSlug);
7580
- let field;
7581
- if (collection) {
7582
- const accessExpr = collection.access?.read;
7583
- if (accessExpr !== void 0 && accessExpr !== null) {
7584
- const allowed = await resolveBooleanAccess(config, accessExpr, {
7585
- user,
7586
- req: toHookRequestContext(c.req)
7587
- });
7588
- if (!allowed) {
7589
- return c.json(
7590
- { error: true, message: `Access denied: read on ${colSlug}` },
7591
- 403
7592
- );
7593
- }
7594
- }
7595
- field = collection.fields.find((f) => f.name === fieldName);
7596
- } else {
7597
- const globals = [...requestConfig.globals];
7598
- const glb = globals.find((g) => g.slug === colSlug);
7599
- if (!glb) {
7600
- return c.json(
7601
- {
7602
- error: true,
7603
- message: `${colSlug} not found as collection or global`
7604
- },
7605
- 404
7606
- );
7607
- }
7608
- const accessExpr = glb.access?.read;
7609
- if (accessExpr !== void 0 && accessExpr !== null) {
7610
- const allowed = await resolveBooleanAccess(config, accessExpr, {
7611
- user,
7612
- req: toHookRequestContext(c.req)
7613
- });
7614
- if (!allowed) {
7615
- return c.json(
7616
- {
7617
- error: true,
7618
- message: `Access denied: read on global ${colSlug}`
7619
- },
7620
- 403
7621
- );
7622
- }
7940
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
7941
+ const { collection: colSlug, field: fieldName } = c.req.param();
7942
+ const siteId = c.req.header("X-Site-Id");
7943
+ const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
7944
+ let collections = [...requestConfig.collections];
7945
+ const user = c.get("user");
7946
+ let collection = collections.find((col) => col.slug === colSlug);
7947
+ let field;
7948
+ if (collection) {
7949
+ const accessExpr = collection.access?.read;
7950
+ if (accessExpr !== void 0 && accessExpr !== null) {
7951
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
7952
+ user,
7953
+ req: toHookRequestContext(c.req)
7954
+ });
7955
+ if (!allowed) {
7956
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
7623
7957
  }
7624
- field = glb.fields.find((f) => f.name === fieldName);
7625
7958
  }
7626
- if (!field) {
7959
+ field = collection.fields.find((f) => f.name === fieldName);
7960
+ } else {
7961
+ const globals = [...requestConfig.globals];
7962
+ const glb = globals.find((g) => g.slug === colSlug);
7963
+ if (!glb) {
7627
7964
  return c.json(
7628
7965
  {
7629
7966
  error: true,
7630
- message: `Field ${fieldName} not found in ${colSlug}`
7967
+ message: `${colSlug} not found as collection or global`
7631
7968
  },
7632
7969
  404
7633
7970
  );
7634
7971
  }
7635
- let resolver;
7636
- let cacheTTL;
7637
- if (typeof field.options === "function") {
7638
- resolver = field.options;
7639
- } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
7640
- resolver = field.options.resolve;
7641
- cacheTTL = field.options.cacheTTL;
7642
- }
7643
- if (!resolver) {
7644
- return c.json(
7645
- {
7646
- error: true,
7647
- message: `Field ${fieldName} in ${colSlug} is not dynamic`
7648
- },
7649
- 400
7650
- );
7651
- }
7652
- try {
7653
- const db = c.get("db") || config.db;
7654
- const queryParams = c.req.query();
7655
- const reqContext = {
7656
- query: queryParams,
7657
- headers: c.req.header(),
7658
- raw: c.req.raw
7659
- };
7660
- const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
7661
- let cacheKey = "";
7662
- if (shouldCache) {
7663
- cacheKey = JSON.stringify([
7664
- siteId ?? "",
7665
- colSlug,
7666
- fieldName,
7667
- user?.id ?? null,
7668
- queryParams
7669
- ]);
7670
- const hit = optionsCache.get(cacheKey);
7671
- if (hit && hit.expires > Date.now()) {
7672
- return c.json(hit.value);
7673
- }
7674
- }
7675
- const result = await resolver({
7676
- db,
7972
+ const accessExpr = glb.access?.read;
7973
+ if (accessExpr !== void 0 && accessExpr !== null) {
7974
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
7677
7975
  user,
7678
- req: reqContext
7976
+ req: toHookRequestContext(c.req)
7679
7977
  });
7680
- if (shouldCache) {
7681
- optionsCache.set(cacheKey, {
7682
- expires: Date.now() + cacheTTL * 1e3,
7683
- value: result
7684
- });
7978
+ if (!allowed) {
7979
+ return c.json(
7980
+ {
7981
+ error: true,
7982
+ message: `Access denied: read on global ${colSlug}`
7983
+ },
7984
+ 403
7985
+ );
7685
7986
  }
7686
- return c.json(result);
7687
- } catch (err) {
7688
- getRequestLogger(c, "router").error({
7689
- err,
7690
- msg: "Failed to resolve dynamic field options",
7691
- fieldName
7987
+ }
7988
+ field = glb.fields.find((f) => f.name === fieldName);
7989
+ }
7990
+ if (!field) {
7991
+ return c.json(
7992
+ {
7993
+ error: true,
7994
+ message: `Field ${fieldName} not found in ${colSlug}`
7995
+ },
7996
+ 404
7997
+ );
7998
+ }
7999
+ let resolver;
8000
+ let cacheTTL;
8001
+ if (typeof field.options === "function") {
8002
+ resolver = field.options;
8003
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
8004
+ resolver = field.options.resolve;
8005
+ cacheTTL = field.options.cacheTTL;
8006
+ }
8007
+ if (!resolver) {
8008
+ return c.json(
8009
+ {
8010
+ error: true,
8011
+ message: `Field ${fieldName} in ${colSlug} is not dynamic`
8012
+ },
8013
+ 400
8014
+ );
8015
+ }
8016
+ try {
8017
+ const db = c.get("db") || config.db;
8018
+ const queryParams = c.req.query();
8019
+ const reqContext = {
8020
+ query: queryParams,
8021
+ headers: c.req.header(),
8022
+ raw: c.req.raw
8023
+ };
8024
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
8025
+ let cacheKey = "";
8026
+ if (shouldCache) {
8027
+ cacheKey = JSON.stringify([
8028
+ siteId ?? "",
8029
+ colSlug,
8030
+ fieldName,
8031
+ user?.id ?? null,
8032
+ queryParams
8033
+ ]);
8034
+ const hit = optionsCache.get(cacheKey);
8035
+ if (hit && hit.expires > Date.now()) {
8036
+ return c.json(hit.value);
8037
+ }
8038
+ }
8039
+ const result = await resolver({
8040
+ db,
8041
+ user,
8042
+ req: reqContext
8043
+ });
8044
+ if (shouldCache) {
8045
+ optionsCache.set(cacheKey, {
8046
+ expires: Date.now() + cacheTTL * 1e3,
8047
+ value: result
7692
8048
  });
7693
- return c.json(
7694
- {
7695
- error: true,
7696
- message: err.message || "Failed to resolve dynamic options"
7697
- },
7698
- 500
7699
- );
7700
8049
  }
8050
+ return c.json(result);
8051
+ } catch (err) {
8052
+ getRequestLogger(c, "router").error({
8053
+ err,
8054
+ msg: "Failed to resolve dynamic field options",
8055
+ fieldName
8056
+ });
8057
+ return c.json(
8058
+ {
8059
+ error: true,
8060
+ message: err.message || "Failed to resolve dynamic options"
8061
+ },
8062
+ 500
8063
+ );
7701
8064
  }
7702
- );
8065
+ });
7703
8066
  app.get("/api/openapi.json", (c) => {
7704
8067
  return c.json(generateOpenApi(config));
7705
8068
  });
@@ -7712,13 +8075,8 @@ function registerRoutes(app, config) {
7712
8075
  const key = c.req.param("key");
7713
8076
  const scope = c.req.query("scope");
7714
8077
  if (!db) return c.json({ message: "Database not configured" }, 500);
7715
- if (!user?.collection || !user.sub)
7716
- return c.json({ error: true, message: "Authentication required." }, 401);
7717
- if (!key)
7718
- return c.json(
7719
- { error: true, message: "Preference key is required." },
7720
- 400
7721
- );
8078
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8079
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7722
8080
  const getGlobalPreference = async () => {
7723
8081
  const globalDoc = await db.findOne({
7724
8082
  collection: "__global_preferences",
@@ -7745,13 +8103,8 @@ function registerRoutes(app, config) {
7745
8103
  const key = c.req.param("key");
7746
8104
  const scope = c.req.query("scope");
7747
8105
  if (!db) return c.json({ message: "Database not configured" }, 500);
7748
- if (!user?.collection || !user.sub)
7749
- return c.json({ error: true, message: "Authentication required." }, 401);
7750
- if (!key)
7751
- return c.json(
7752
- { error: true, message: "Preference key is required." },
7753
- 400
7754
- );
8106
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8107
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7755
8108
  const body = await c.req.json().catch(() => ({}));
7756
8109
  if (scope === "global") {
7757
8110
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
@@ -7799,13 +8152,8 @@ function registerRoutes(app, config) {
7799
8152
  const key = c.req.param("key");
7800
8153
  const scope = c.req.query("scope");
7801
8154
  if (!db) return c.json({ message: "Database not configured" }, 500);
7802
- if (!user?.collection || !user.sub)
7803
- return c.json({ error: true, message: "Authentication required." }, 401);
7804
- if (!key)
7805
- return c.json(
7806
- { error: true, message: "Preference key is required." },
7807
- 400
7808
- );
8155
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8156
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7809
8157
  if (scope === "global") {
7810
8158
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
7811
8159
  if (!isAdminUser) {
@@ -7844,41 +8192,17 @@ function registerRoutes(app, config) {
7844
8192
  for (const col of uploadCollections) {
7845
8193
  const mediaController = new MediaController(col.slug);
7846
8194
  const prefix = `/api/collections/${col.slug}`;
7847
- app.get(
7848
- `${prefix}/media`,
7849
- accessGate(config, col, "read"),
7850
- (c) => mediaController.find(c)
7851
- );
7852
- app.get(
7853
- `${prefix}/media/:filename{.+$}`,
7854
- (c) => mediaController.serve(c)
7855
- );
7856
- app.post(
7857
- `${prefix}/media`,
7858
- accessGate(config, col, "create"),
7859
- (c) => mediaController.upload(c)
7860
- );
7861
- app.delete(
7862
- `${prefix}/media/:id`,
7863
- accessGate(config, col, "delete"),
7864
- (c) => mediaController.delete(c)
7865
- );
8195
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
8196
+ app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
8197
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
8198
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
7866
8199
  }
7867
8200
  }
7868
8201
  const adminAuthController = new AdminAuthController(config);
7869
8202
  app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
7870
- app.get(
7871
- "/api/admin/auth/:provider/start",
7872
- (c) => adminAuthController.start(c)
7873
- );
7874
- app.get(
7875
- "/api/admin/auth/:provider/callback",
7876
- (c) => adminAuthController.callback(c)
7877
- );
7878
- app.post(
7879
- "/api/admin/auth/:provider/exchange",
7880
- (c) => adminAuthController.exchange(c)
7881
- );
8203
+ app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
8204
+ app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
8205
+ app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
7882
8206
  app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
7883
8207
  for (const collection of config.collections) {
7884
8208
  if (!collection.auth) continue;
@@ -7889,21 +8213,10 @@ function registerRoutes(app, config) {
7889
8213
  app.get(`${path}/init`, (c) => authController.init(c));
7890
8214
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
7891
8215
  app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
7892
- app.post(
7893
- `${path}/refresh-token`,
7894
- requireAuth(config),
7895
- (c) => authController.refreshToken(c)
7896
- );
7897
- app.post(
7898
- `${path}/forgot-password`,
7899
- (c) => authController.forgotPassword(c)
7900
- );
8216
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
8217
+ app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
7901
8218
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
7902
- app.post(
7903
- `${path}/invite`,
7904
- requireAuth(config),
7905
- (c) => authController.invite(c)
7906
- );
8219
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
7907
8220
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
7908
8221
  }
7909
8222
  const auditController = new AuditController();
@@ -7914,33 +8227,19 @@ function registerRoutes(app, config) {
7914
8227
  app.post(path, (c) => controller.create(c));
7915
8228
  app.post(`${path}/media`, (c) => controller.create(c));
7916
8229
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
8230
+ app.post(`${path}/aggregate`, (c) => controller.aggregate(c));
7917
8231
  if (collection.audit) {
7918
- app.get(
7919
- `${path}/__audit`,
7920
- (c) => auditController.findForCollection(c, collection)
7921
- );
8232
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
7922
8233
  }
7923
8234
  app.get(`${path}/:id`, (c) => controller.findOne(c));
7924
8235
  app.patch(`${path}/:id`, (c) => controller.update(c));
7925
8236
  app.delete(`${path}/:id`, (c) => controller.delete(c));
7926
8237
  if (collection.auth) {
7927
- app.post(
7928
- `${path}/:id/change-password`,
7929
- requireAuth(config),
7930
- (c) => controller.changePassword(c)
7931
- );
8238
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
7932
8239
  }
7933
8240
  if (collection.workflow) {
7934
- app.post(
7935
- `${path}/:id/transitions/:transition`,
7936
- requireAuth(config),
7937
- (c) => controller.transition(c)
7938
- );
7939
- app.get(
7940
- `${path}/:id/workflow-history`,
7941
- requireAuth(config),
7942
- (c) => controller.workflowHistory(c)
7943
- );
8241
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
8242
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
7944
8243
  }
7945
8244
  }
7946
8245
  for (const global of config.globals) {
@@ -7955,11 +8254,7 @@ function registerRoutes(app, config) {
7955
8254
  });
7956
8255
  }
7957
8256
  const previewController = new PreviewController();
7958
- app.post(
7959
- "/api/preview-token",
7960
- requireAuth(config),
7961
- (c) => previewController.createToken(c)
7962
- );
8257
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
7963
8258
  app.get("/api/preview-data", (c) => previewController.getData(c));
7964
8259
  app.get("/api/audit", (c) => auditController.findAll(c));
7965
8260
  app.get("/api/collections/:slug/__audit", async (c) => {
@@ -7972,81 +8267,49 @@ function registerRoutes(app, config) {
7972
8267
  if (!config2.onSchemaFetch || !siteId) {
7973
8268
  return c.json({ message: `Collection "${slug}" not found` }, 404);
7974
8269
  }
7975
- const requestConfig = mergeDynamicConfig(
7976
- config2,
7977
- await config2.onSchemaFetch(siteId)
7978
- );
7979
- const collection = requestConfig.collections.find(
7980
- (col) => col.slug === slug
7981
- );
8270
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8271
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
7982
8272
  if (!collection?.audit) {
7983
- return c.json(
7984
- { message: `Collection "${slug}" not found or has no audit log` },
7985
- 404
7986
- );
8273
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
7987
8274
  }
7988
8275
  return auditController.findForCollection(c, collection);
7989
8276
  });
7990
- app.post(
7991
- "/api/collections/:slug/:id/transitions/:transition",
7992
- requireAuth(config),
7993
- async (c) => {
7994
- const slug = c.req.param("slug");
7995
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
7996
- const config2 = c.get("config");
7997
- if (config2.collections.some((col) => col.slug === slug)) {
7998
- return c.json({ message: "Not Found" }, 404);
7999
- }
8000
- if (!config2.onSchemaFetch || !siteId) {
8001
- return c.json({ message: `Collection "${slug}" not found` }, 404);
8002
- }
8003
- const requestConfig = mergeDynamicConfig(
8004
- config2,
8005
- await config2.onSchemaFetch(siteId)
8006
- );
8007
- const collection = requestConfig.collections.find(
8008
- (col) => col.slug === slug
8009
- );
8010
- if (!collection?.workflow) {
8011
- return c.json(
8012
- { message: `Collection "${slug}" not found or has no workflow` },
8013
- 404
8014
- );
8015
- }
8016
- const controller = new CollectionController(collection);
8017
- return controller.transition(c);
8277
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
8278
+ const slug = c.req.param("slug");
8279
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8280
+ const config2 = c.get("config");
8281
+ if (config2.collections.some((col) => col.slug === slug)) {
8282
+ return c.json({ message: "Not Found" }, 404);
8018
8283
  }
8019
- );
8020
- app.get(
8021
- "/api/collections/:slug/:id/workflow-history",
8022
- requireAuth(config),
8023
- async (c) => {
8024
- const slug = c.req.param("slug");
8025
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8026
- const config2 = c.get("config");
8027
- if (config2.collections.some((col) => col.slug === slug)) {
8028
- return c.json({ message: "Not Found" }, 404);
8029
- }
8030
- if (!config2.onSchemaFetch || !siteId) {
8031
- return c.json({ message: `Collection "${slug}" not found` }, 404);
8032
- }
8033
- const requestConfig = mergeDynamicConfig(
8034
- config2,
8035
- await config2.onSchemaFetch(siteId)
8036
- );
8037
- const collection = requestConfig.collections.find(
8038
- (col) => col.slug === slug
8039
- );
8040
- if (!collection?.workflow) {
8041
- return c.json(
8042
- { message: `Collection "${slug}" not found or has no workflow` },
8043
- 404
8044
- );
8045
- }
8046
- const controller = new CollectionController(collection);
8047
- return controller.workflowHistory(c);
8284
+ if (!config2.onSchemaFetch || !siteId) {
8285
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
8048
8286
  }
8049
- );
8287
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8288
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
8289
+ if (!collection?.workflow) {
8290
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
8291
+ }
8292
+ const controller = new CollectionController(collection);
8293
+ return controller.transition(c);
8294
+ });
8295
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
8296
+ const slug = c.req.param("slug");
8297
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8298
+ const config2 = c.get("config");
8299
+ if (config2.collections.some((col) => col.slug === slug)) {
8300
+ return c.json({ message: "Not Found" }, 404);
8301
+ }
8302
+ if (!config2.onSchemaFetch || !siteId) {
8303
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
8304
+ }
8305
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8306
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
8307
+ if (!collection?.workflow) {
8308
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
8309
+ }
8310
+ const controller = new CollectionController(collection);
8311
+ return controller.workflowHistory(c);
8312
+ });
8050
8313
  app.all("/api/collections/:slug/:id?", async (c) => {
8051
8314
  const slug = c.req.param("slug");
8052
8315
  const id = c.req.param("id");
@@ -8056,13 +8319,8 @@ function registerRoutes(app, config) {
8056
8319
  return c.json({ message: "Method Not Allowed" }, 405);
8057
8320
  }
8058
8321
  if (config2.onSchemaFetch && siteId) {
8059
- const requestConfig = mergeDynamicConfig(
8060
- config2,
8061
- await config2.onSchemaFetch(siteId)
8062
- );
8063
- let collection = requestConfig.collections.find(
8064
- (col) => col.slug === slug
8065
- );
8322
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8323
+ let collection = requestConfig.collections.find((col) => col.slug === slug);
8066
8324
  if (!collection && slug === "media") {
8067
8325
  collection = {
8068
8326
  slug: "media",
@@ -8075,25 +8333,19 @@ function registerRoutes(app, config) {
8075
8333
  if (collection.auth && id) {
8076
8334
  const authController = new AuthController(collection);
8077
8335
  const method2 = c.req.method;
8078
- if (method2 === "POST" && id === "login")
8079
- return authController.login(c);
8080
- if (method2 === "POST" && id === "logout")
8081
- return authController.logout(c);
8336
+ if (method2 === "POST" && id === "login") return authController.login(c);
8337
+ if (method2 === "POST" && id === "logout") return authController.logout(c);
8082
8338
  if (method2 === "GET" && id === "me") return authController.me(c);
8083
- if (method2 === "POST" && id === "refresh-token")
8084
- return authController.refreshToken(c);
8085
- if (method2 === "POST" && id === "forgot-password")
8086
- return authController.forgotPassword(c);
8087
- if (method2 === "POST" && id === "reset-password")
8088
- return authController.resetPassword(c);
8339
+ if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
8340
+ if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
8341
+ if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
8089
8342
  }
8090
8343
  const controller = new CollectionController(collection);
8091
8344
  const method = c.req.method;
8092
8345
  if (id) {
8093
8346
  if (method === "GET") return controller.findOne(c);
8094
8347
  if (method === "PATCH") return controller.update(c);
8095
- if (method === "DELETE" && id === "delete-many")
8096
- return controller.deleteMany(c);
8348
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
8097
8349
  if (method === "DELETE") return controller.delete(c);
8098
8350
  if (method === "POST" && id === "media") return controller.create(c);
8099
8351
  } else {
@@ -8113,10 +8365,7 @@ function registerRoutes(app, config) {
8113
8365
  return c.json({ message: "Method Not Allowed" }, 405);
8114
8366
  }
8115
8367
  if (config2.onSchemaFetch && siteId) {
8116
- const requestConfig = mergeDynamicConfig(
8117
- config2,
8118
- await config2.onSchemaFetch(siteId)
8119
- );
8368
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8120
8369
  const global = requestConfig.globals.find((glb) => glb.slug === slug);
8121
8370
  if (global) {
8122
8371
  const controller = new GlobalController(global);
@@ -8482,7 +8731,7 @@ function resolveClientIp(req, trustProxy = false) {
8482
8731
  const value = normalizeIp(req.headers[header]);
8483
8732
  if (value) return value;
8484
8733
  }
8485
- if (trustProxy) {
8734
+ if (trustProxy === true || typeof trustProxy === "number") {
8486
8735
  const forwardedFor = pickForwardedFor(req.headers, trustProxy);
8487
8736
  if (forwardedFor) return forwardedFor;
8488
8737
  }
@@ -8494,6 +8743,7 @@ function pickForwardedFor(headers, trustProxy) {
8494
8743
  const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
8495
8744
  if (chain2.length > 0) {
8496
8745
  if (trustProxy === true) return chain2[0];
8746
+ if (trustProxy <= 0) return null;
8497
8747
  const trustedHops2 = Math.max(1, Math.floor(trustProxy));
8498
8748
  const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
8499
8749
  return chain2[candidateIndex2] ?? chain2[0];
@@ -8506,6 +8756,7 @@ function pickForwardedFor(headers, trustProxy) {
8506
8756
  const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
8507
8757
  if (chain.length === 0) return null;
8508
8758
  if (trustProxy === true) return chain[0];
8759
+ if (trustProxy <= 0) return null;
8509
8760
  const trustedHops = Math.max(1, Math.floor(trustProxy));
8510
8761
  const candidateIndex = Math.max(0, chain.length - trustedHops);
8511
8762
  return chain[candidateIndex] ?? chain[0];
@@ -8710,7 +8961,7 @@ async function createDyrectedApp(rawConfig) {
8710
8961
  app.use("*", (0, import_cors.cors)(config.cors ? { origin: config.cors.origins } : {}));
8711
8962
  app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
8712
8963
  if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
8713
- app.get(observability.config.metrics.path, async (c) => {
8964
+ app.get(observability.config.metrics.path, async (_c) => {
8714
8965
  const metrics = await renderPrometheusMetrics(
8715
8966
  observability.prometheusExporter
8716
8967
  );
@@ -8770,5 +9021,6 @@ init_observability();
8770
9021
  registerRoutes,
8771
9022
  renderPrometheusMetrics,
8772
9023
  resolveObservabilityConfig,
9024
+ serializeDetailForApi,
8773
9025
  shouldSampleRequest
8774
9026
  });