@dyrected/core 2.5.13 → 2.5.16

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 (48) hide show
  1. package/dist/app-B2tg7Djj.d.cts +1575 -0
  2. package/dist/app-B2tg7Djj.d.ts +1575 -0
  3. package/dist/app-Bh4_Opv0.d.cts +1522 -0
  4. package/dist/app-Bh4_Opv0.d.ts +1522 -0
  5. package/dist/app-Bv9gaDAN.d.cts +561 -0
  6. package/dist/app-Bv9gaDAN.d.ts +561 -0
  7. package/dist/app-BvG3bRc8.d.cts +419 -0
  8. package/dist/app-BvG3bRc8.d.ts +419 -0
  9. package/dist/app-C3B9N1KR.d.cts +1522 -0
  10. package/dist/app-C3B9N1KR.d.ts +1522 -0
  11. package/dist/app-DDJJa0ep.d.cts +1621 -0
  12. package/dist/app-DDJJa0ep.d.ts +1621 -0
  13. package/dist/app-DO1s9YW1.d.cts +1621 -0
  14. package/dist/app-DO1s9YW1.d.ts +1621 -0
  15. package/dist/app-DTP3-9PJ.d.cts +561 -0
  16. package/dist/app-DTP3-9PJ.d.ts +561 -0
  17. package/dist/app-DbKDGYTI.d.cts +566 -0
  18. package/dist/app-DbKDGYTI.d.ts +566 -0
  19. package/dist/app-DqRO-CMi.d.cts +1457 -0
  20. package/dist/app-DqRO-CMi.d.ts +1457 -0
  21. package/dist/app-DvaFpOtj.d.cts +398 -0
  22. package/dist/app-DvaFpOtj.d.ts +398 -0
  23. package/dist/app-FGzip4XM.d.cts +1563 -0
  24. package/dist/app-FGzip4XM.d.ts +1563 -0
  25. package/dist/app-T0alZAE0.d.cts +383 -0
  26. package/dist/app-T0alZAE0.d.ts +383 -0
  27. package/dist/app-oQt5-9MU.d.cts +1560 -0
  28. package/dist/app-oQt5-9MU.d.ts +1560 -0
  29. package/dist/app-rZj1VFer.d.cts +1621 -0
  30. package/dist/app-rZj1VFer.d.ts +1621 -0
  31. package/dist/app-wo82JRHl.d.cts +445 -0
  32. package/dist/app-wo82JRHl.d.ts +445 -0
  33. package/dist/chunk-23URSKPI.js +2371 -0
  34. package/dist/chunk-2JMA3M5S.js +2475 -0
  35. package/dist/chunk-3FZEUK36.js +2470 -0
  36. package/dist/chunk-DOJHZ7XN.js +2394 -0
  37. package/dist/chunk-PKNFV7KE.js +2469 -0
  38. package/dist/chunk-UBTRANFX.js +2476 -0
  39. package/dist/chunk-W6KURRMW.js +2471 -0
  40. package/dist/index.cjs +457 -48
  41. package/dist/index.d.cts +117 -8
  42. package/dist/index.d.ts +117 -8
  43. package/dist/index.js +9 -3
  44. package/dist/server.cjs +449 -46
  45. package/dist/server.d.cts +57 -15
  46. package/dist/server.d.ts +57 -15
  47. package/dist/server.js +1 -1
  48. package/package.json +1 -1
package/dist/server.cjs CHANGED
@@ -268,6 +268,150 @@ async function verifyPassword(plain, stored) {
268
268
  return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
269
269
  }
270
270
 
271
+ // src/utils/hooks.ts
272
+ async function runCollectionHooks(hooks, args) {
273
+ if (!hooks || hooks.length === 0) {
274
+ return args.data ?? args.doc ?? void 0;
275
+ }
276
+ let currentPayload = args.data ?? args.doc ?? void 0;
277
+ for (const hook of hooks) {
278
+ const result = await hook({
279
+ ...args,
280
+ data: args.data !== void 0 ? currentPayload : void 0,
281
+ doc: args.doc !== void 0 ? currentPayload : void 0
282
+ });
283
+ if (result !== void 0) {
284
+ currentPayload = result;
285
+ }
286
+ }
287
+ return currentPayload;
288
+ }
289
+ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
290
+ if (!data || typeof data !== "object") return data;
291
+ const result = { ...data };
292
+ for (const field of fields) {
293
+ if (!field.name) continue;
294
+ const value = result[field.name];
295
+ const origValue = originalDoc?.[field.name];
296
+ let updatedValue = value;
297
+ if (field.hooks?.beforeChange) {
298
+ for (const hook of field.hooks.beforeChange) {
299
+ updatedValue = await hook({
300
+ value: updatedValue,
301
+ originalDoc: originalDoc ?? void 0,
302
+ data: result,
303
+ user
304
+ });
305
+ }
306
+ result[field.name] = updatedValue;
307
+ }
308
+ if (updatedValue !== void 0 && updatedValue !== null) {
309
+ if (field.type === "object" && field.fields) {
310
+ result[field.name] = await executeFieldBeforeChange(
311
+ field.fields,
312
+ updatedValue,
313
+ origValue,
314
+ user
315
+ );
316
+ } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
317
+ const arrayResult = [];
318
+ for (let i = 0; i < updatedValue.length; i++) {
319
+ const item = updatedValue[i];
320
+ const origItem = Array.isArray(origValue) ? origValue[i] : null;
321
+ arrayResult.push(
322
+ await executeFieldBeforeChange(field.fields, item, origItem, user)
323
+ );
324
+ }
325
+ result[field.name] = arrayResult;
326
+ } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
327
+ const blocksResult = [];
328
+ for (let i = 0; i < updatedValue.length; i++) {
329
+ const blockData = updatedValue[i];
330
+ const origBlock = Array.isArray(origValue) ? origValue[i] : null;
331
+ const blockConfig = field.blocks.find(
332
+ (b) => b.slug === blockData.blockType
333
+ );
334
+ if (blockConfig) {
335
+ blocksResult.push(
336
+ await executeFieldBeforeChange(
337
+ blockConfig.fields,
338
+ blockData,
339
+ origBlock,
340
+ user
341
+ )
342
+ );
343
+ } else {
344
+ blocksResult.push(blockData);
345
+ }
346
+ }
347
+ result[field.name] = blocksResult;
348
+ }
349
+ }
350
+ }
351
+ return result;
352
+ }
353
+ async function executeFieldAfterRead(fields, doc, user) {
354
+ if (!doc || typeof doc !== "object") return doc;
355
+ const result = { ...doc };
356
+ for (const field of fields) {
357
+ if (!field.name) continue;
358
+ const value = result[field.name];
359
+ let updatedValue = value;
360
+ if (field.hooks?.afterRead) {
361
+ for (const hook of field.hooks.afterRead) {
362
+ updatedValue = await hook({
363
+ value: updatedValue,
364
+ doc: result,
365
+ user
366
+ });
367
+ }
368
+ result[field.name] = updatedValue;
369
+ }
370
+ if (updatedValue !== void 0 && updatedValue !== null) {
371
+ if (field.type === "object" && field.fields) {
372
+ result[field.name] = await executeFieldAfterRead(
373
+ field.fields,
374
+ updatedValue,
375
+ user
376
+ );
377
+ } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
378
+ const arrayResult = [];
379
+ for (const item of updatedValue) {
380
+ arrayResult.push(
381
+ await executeFieldAfterRead(
382
+ field.fields,
383
+ item,
384
+ user
385
+ )
386
+ );
387
+ }
388
+ result[field.name] = arrayResult;
389
+ } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
390
+ const blocksResult = [];
391
+ for (const blockData of updatedValue) {
392
+ const typedBlock = blockData;
393
+ const blockConfig = field.blocks.find(
394
+ (b) => b.slug === typedBlock.blockType
395
+ );
396
+ if (blockConfig) {
397
+ blocksResult.push(
398
+ await executeFieldAfterRead(
399
+ blockConfig.fields,
400
+ typedBlock,
401
+ user
402
+ )
403
+ );
404
+ } else {
405
+ blocksResult.push(blockData);
406
+ }
407
+ }
408
+ result[field.name] = blocksResult;
409
+ }
410
+ }
411
+ }
412
+ return result;
413
+ }
414
+
271
415
  // src/controllers/collection.controller.ts
272
416
  var CollectionController = class {
273
417
  collection;
@@ -282,6 +426,7 @@ var CollectionController = class {
282
426
  const page = Number(c.req.query("page")) || 1;
283
427
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 2;
284
428
  const sort = c.req.query("sort") || void 0;
429
+ const user = c.get("user");
285
430
  let where = void 0;
286
431
  const whereRaw = c.req.query("where");
287
432
  if (whereRaw) {
@@ -290,6 +435,14 @@ var CollectionController = class {
290
435
  } catch {
291
436
  }
292
437
  }
438
+ const beforeReadResult = await runCollectionHooks(this.collection.hooks?.beforeRead, {
439
+ req: c.req,
440
+ query: where,
441
+ user
442
+ });
443
+ if (beforeReadResult !== void 0) {
444
+ where = beforeReadResult;
445
+ }
293
446
  let result = await db.find({
294
447
  collection: this.collection.slug,
295
448
  limit,
@@ -311,6 +464,17 @@ var CollectionController = class {
311
464
  });
312
465
  }
313
466
  result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
467
+ const processedDocs = [];
468
+ for (const doc of result.docs) {
469
+ const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
470
+ doc,
471
+ req: c.req,
472
+ user
473
+ });
474
+ const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
475
+ processedDocs.push(docWithFieldHooks);
476
+ }
477
+ result.docs = processedDocs;
314
478
  if (depth > 0) {
315
479
  const populationService = new PopulationService(db, config.collections);
316
480
  result = await populationService.populateResult(result, this.collection.fields, depth);
@@ -323,21 +487,28 @@ var CollectionController = class {
323
487
  if (!db) return c.json({ message: "Database not configured" }, 500);
324
488
  const id = c.req.param("id");
325
489
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
490
+ const user = c.get("user");
326
491
  if (!id) return c.json({ message: "Missing ID" }, 400);
327
492
  const doc = await db.findOne({ collection: this.collection.slug, id });
328
493
  if (!doc) return c.json({ message: "Not Found" }, 404);
329
494
  const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
330
- if (depth > 0 && docWithDefaults) {
495
+ const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
496
+ doc: docWithDefaults,
497
+ req: c.req,
498
+ user
499
+ });
500
+ const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
501
+ if (depth > 0 && docWithFieldHooks) {
331
502
  const populationService = new PopulationService(db, config.collections);
332
503
  const populatedDoc = await populationService.populate({
333
- data: docWithDefaults,
504
+ data: docWithFieldHooks,
334
505
  fields: this.collection.fields,
335
506
  currentDepth: 0,
336
507
  maxDepth: depth
337
508
  });
338
509
  return c.json(populatedDoc);
339
510
  }
340
- return c.json(docWithDefaults);
511
+ return c.json(docWithFieldHooks);
341
512
  }
342
513
  async create(c) {
343
514
  const config = c.get("config");
@@ -350,7 +521,7 @@ var CollectionController = class {
350
521
  const body = await c.req.json();
351
522
  const user = c.get("user");
352
523
  const now = (/* @__PURE__ */ new Date()).toISOString();
353
- const data = {
524
+ let data = {
354
525
  ...body,
355
526
  createdAt: now,
356
527
  updatedAt: now,
@@ -360,6 +531,13 @@ var CollectionController = class {
360
531
  if (this.collection.auth && data.password) {
361
532
  data.password = await hashPassword(data.password);
362
533
  }
534
+ data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
535
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
536
+ data,
537
+ req: c.req,
538
+ user,
539
+ operation: "create"
540
+ });
363
541
  const doc = await db.create({ collection: this.collection.slug, data });
364
542
  if (this.collection.audit && db) {
365
543
  AuditService.log(db, {
@@ -371,7 +549,19 @@ var CollectionController = class {
371
549
  after: doc
372
550
  });
373
551
  }
374
- return c.json(doc, 201);
552
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
553
+ doc,
554
+ user,
555
+ req: c.req,
556
+ operation: "create"
557
+ });
558
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
559
+ doc,
560
+ req: c.req,
561
+ user
562
+ });
563
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
564
+ return c.json(finalDoc, 201);
375
565
  }
376
566
  async upload(c) {
377
567
  const config = c.get("config");
@@ -398,18 +588,38 @@ var CollectionController = class {
398
588
  });
399
589
  const user = c.get("user");
400
590
  const now = (/* @__PURE__ */ new Date()).toISOString();
591
+ let data = {
592
+ ...otherData,
593
+ ...fileData,
594
+ createdAt: now,
595
+ updatedAt: now,
596
+ createdBy: user?.sub ?? null,
597
+ updatedBy: user?.sub ?? null
598
+ };
599
+ data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
600
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
601
+ data,
602
+ req: c.req,
603
+ user,
604
+ operation: "create"
605
+ });
401
606
  const doc = await config.db.create({
402
607
  collection: this.collection.slug,
403
- data: {
404
- ...otherData,
405
- ...fileData,
406
- createdAt: now,
407
- updatedAt: now,
408
- createdBy: user?.sub ?? null,
409
- updatedBy: user?.sub ?? null
410
- }
608
+ data
411
609
  });
412
- return c.json(doc, 201);
610
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
611
+ doc,
612
+ user,
613
+ req: c.req,
614
+ operation: "create"
615
+ });
616
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
617
+ doc,
618
+ req: c.req,
619
+ user
620
+ });
621
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
622
+ return c.json(finalDoc, 201);
413
623
  }
414
624
  async update(c) {
415
625
  const config = c.get("config");
@@ -419,7 +629,7 @@ var CollectionController = class {
419
629
  if (!id) return c.json({ message: "Missing ID" }, 400);
420
630
  const body = await c.req.json();
421
631
  const user = c.get("user");
422
- const data = { ...body };
632
+ let data = { ...body };
423
633
  if (this.collection.auth) {
424
634
  delete data.password;
425
635
  delete data.oldPassword;
@@ -429,10 +639,20 @@ var CollectionController = class {
429
639
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
430
640
  updatedBy: user?.sub ?? null
431
641
  });
642
+ const originalDoc = await db.findOne({ collection: this.collection.slug, id });
643
+ if (!originalDoc) return c.json({ message: "Not Found" }, 404);
432
644
  let before = null;
433
645
  if (this.collection.audit) {
434
- before = await db.findOne({ collection: this.collection.slug, id });
435
- }
646
+ before = originalDoc;
647
+ }
648
+ data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user);
649
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
650
+ data,
651
+ doc: originalDoc,
652
+ req: c.req,
653
+ user,
654
+ operation: "update"
655
+ });
436
656
  const doc = await db.update({ collection: this.collection.slug, id, data });
437
657
  if (this.collection.audit && db) {
438
658
  AuditService.log(db, {
@@ -444,7 +664,20 @@ var CollectionController = class {
444
664
  after: doc
445
665
  });
446
666
  }
447
- return c.json(doc);
667
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
668
+ doc,
669
+ previousDoc: originalDoc,
670
+ user,
671
+ req: c.req,
672
+ operation: "update"
673
+ });
674
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
675
+ doc,
676
+ req: c.req,
677
+ user
678
+ });
679
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
680
+ return c.json(finalDoc);
448
681
  }
449
682
  /**
450
683
  * POST /api/collections/:slug/:id/change-password
@@ -524,10 +757,18 @@ var CollectionController = class {
524
757
  const id = c.req.param("id");
525
758
  if (!id) return c.json({ message: "Missing ID" }, 400);
526
759
  const user = c.get("user");
760
+ const doc = await db.findOne({ collection: this.collection.slug, id });
761
+ if (!doc) return c.json({ message: "Not Found" }, 404);
527
762
  let before = null;
528
763
  if (this.collection.audit) {
529
- before = await db.findOne({ collection: this.collection.slug, id });
764
+ before = doc;
530
765
  }
766
+ await runCollectionHooks(this.collection.hooks?.beforeDelete, {
767
+ id,
768
+ doc,
769
+ user,
770
+ req: c.req
771
+ });
531
772
  await db.delete({ collection: this.collection.slug, id });
532
773
  if (this.collection.audit && db) {
533
774
  AuditService.log(db, {
@@ -539,6 +780,12 @@ var CollectionController = class {
539
780
  after: null
540
781
  });
541
782
  }
783
+ await runCollectionHooks(this.collection.hooks?.afterDelete, {
784
+ id,
785
+ doc,
786
+ user,
787
+ req: c.req
788
+ });
542
789
  return c.json({ message: "Deleted" });
543
790
  }
544
791
  async deleteMany(c) {
@@ -563,10 +810,21 @@ var CollectionController = class {
563
810
  const failed = [];
564
811
  for (const id of ids) {
565
812
  try {
813
+ const doc = await db.findOne({ collection: this.collection.slug, id });
814
+ if (!doc) {
815
+ failed.push({ id, error: "Not Found" });
816
+ continue;
817
+ }
566
818
  let before = null;
567
819
  if (this.collection.audit) {
568
- before = await db.findOne({ collection: this.collection.slug, id });
820
+ before = doc;
569
821
  }
822
+ await runCollectionHooks(this.collection.hooks?.beforeDelete, {
823
+ id,
824
+ doc,
825
+ user,
826
+ req: c.req
827
+ });
570
828
  await db.delete({ collection: this.collection.slug, id });
571
829
  deleted.push(id);
572
830
  if (this.collection.audit) {
@@ -579,6 +837,12 @@ var CollectionController = class {
579
837
  after: null
580
838
  });
581
839
  }
840
+ await runCollectionHooks(this.collection.hooks?.afterDelete, {
841
+ id,
842
+ doc,
843
+ user,
844
+ req: c.req
845
+ });
582
846
  } catch (err) {
583
847
  failed.push({ id, error: err?.message ?? "Unknown error" });
584
848
  }
@@ -623,6 +887,16 @@ var GlobalController = class {
623
887
  const db = config.db;
624
888
  if (!db) return c.json({ message: "Database not configured" }, 500);
625
889
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
890
+ const user = c.get("user");
891
+ let query = void 0;
892
+ const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
893
+ req: c.req,
894
+ query,
895
+ user
896
+ });
897
+ if (beforeReadResult !== void 0) {
898
+ query = beforeReadResult;
899
+ }
626
900
  let data = await db.getGlobal({ slug: this.global.slug });
627
901
  const isEmpty = !data || Object.keys(data).length === 0;
628
902
  if (isEmpty && this.global.initialData) {
@@ -631,24 +905,53 @@ var GlobalController = class {
631
905
  data = this.global.initialData;
632
906
  }
633
907
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
634
- if (depth > 0 && dataWithDefaults) {
908
+ const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
909
+ doc: dataWithDefaults,
910
+ req: c.req,
911
+ user
912
+ });
913
+ const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user);
914
+ if (depth > 0 && docWithFieldHooks) {
635
915
  const populationService = new PopulationService(db, config.collections);
636
916
  const populatedData = await populationService.populate({
637
- data: dataWithDefaults,
917
+ data: docWithFieldHooks,
638
918
  fields: this.global.fields,
639
919
  currentDepth: 0,
640
920
  maxDepth: depth
641
921
  });
642
922
  return c.json(populatedData);
643
923
  }
644
- return c.json(dataWithDefaults);
924
+ return c.json(docWithFieldHooks);
645
925
  }
646
926
  async update(c) {
647
927
  const db = c.get("config").db;
648
928
  if (!db) return c.json({ message: "Database not configured" }, 500);
649
929
  const body = await c.req.json();
650
- const data = await db.updateGlobal({ slug: this.global.slug, data: body });
651
- return c.json(data);
930
+ const user = c.get("user");
931
+ const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
932
+ let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user);
933
+ data = await runCollectionHooks(this.global.hooks?.beforeChange, {
934
+ data,
935
+ doc: originalDoc,
936
+ req: c.req,
937
+ user,
938
+ operation: "update"
939
+ });
940
+ const updated = await db.updateGlobal({ slug: this.global.slug, data });
941
+ await runCollectionHooks(this.global.hooks?.afterChange, {
942
+ doc: updated,
943
+ previousDoc: originalDoc,
944
+ user,
945
+ req: c.req,
946
+ operation: "update"
947
+ });
948
+ const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
949
+ doc: updated,
950
+ req: c.req,
951
+ user
952
+ });
953
+ const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user);
954
+ return c.json(finalDoc);
652
955
  }
653
956
  async seed(c) {
654
957
  const config = c.get("config");
@@ -1557,12 +1860,13 @@ function fieldToSchema(field) {
1557
1860
  schema = { type: "string", format: "date-time" };
1558
1861
  break;
1559
1862
  case "select":
1560
- schema = { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) };
1863
+ case "radio":
1864
+ schema = { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 };
1561
1865
  break;
1562
1866
  case "multiSelect":
1563
1867
  schema = {
1564
1868
  type: "array",
1565
- items: { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) }
1869
+ items: { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 }
1566
1870
  };
1567
1871
  break;
1568
1872
  case "relationship":
@@ -1686,6 +1990,49 @@ function accessGate(target, action) {
1686
1990
  await next();
1687
1991
  };
1688
1992
  }
1993
+ async function checkAccess(access, accessArgs) {
1994
+ if (access === void 0 || access === null) return true;
1995
+ if (typeof access === "function") {
1996
+ try {
1997
+ const result = await access(accessArgs);
1998
+ return typeof result === "boolean" ? result : !!result;
1999
+ } catch (err) {
2000
+ console.error("[dyrected/core] Functional access check failed:", err);
2001
+ return false;
2002
+ }
2003
+ }
2004
+ if (typeof access === "string" || typeof access === "boolean") {
2005
+ return evaluateAccess(access, accessArgs);
2006
+ }
2007
+ return true;
2008
+ }
2009
+ function serializeFieldForApi(f) {
2010
+ if (!f) return f;
2011
+ const serialized = { ...f };
2012
+ if (serialized.admin?.hooks) {
2013
+ const hooks = { ...serialized.admin.hooks };
2014
+ if (typeof hooks.onChange === "function") {
2015
+ hooks.onChange = hooks.onChange.toString();
2016
+ }
2017
+ if (typeof hooks.options === "function") {
2018
+ hooks.options = hooks.options.toString();
2019
+ }
2020
+ serialized.admin = { ...serialized.admin, hooks };
2021
+ }
2022
+ if (typeof serialized.options === "function" || serialized.options && typeof serialized.options === "object" && "resolve" in serialized.options) {
2023
+ serialized.options = { _dynamic: true };
2024
+ }
2025
+ if (serialized.fields) {
2026
+ serialized.fields = serialized.fields.map(serializeFieldForApi);
2027
+ }
2028
+ if (serialized.blocks) {
2029
+ serialized.blocks = serialized.blocks.map((b) => ({
2030
+ ...b,
2031
+ fields: b.fields?.map(serializeFieldForApi)
2032
+ }));
2033
+ }
2034
+ return serialized;
2035
+ }
1689
2036
  function registerRoutes(app, config) {
1690
2037
  app.get("/api/schemas", optionalAuth(), async (c) => {
1691
2038
  const siteId = c.req.header("X-Site-Id");
@@ -1701,26 +2048,10 @@ function registerRoutes(app, config) {
1701
2048
  }
1702
2049
  const user = c.get("user");
1703
2050
  const accessArgs = { user, req: c.req, doc: null };
1704
- const resolveAccess = async (access) => {
1705
- if (access === void 0 || access === null) return true;
1706
- if (typeof access === "function") {
1707
- try {
1708
- const result = await access(accessArgs);
1709
- return typeof result === "boolean" ? result : !!result;
1710
- } catch (err) {
1711
- console.error("[dyrected/core] Functional access check failed:", err);
1712
- return false;
1713
- }
1714
- }
1715
- if (typeof access === "string" || typeof access === "boolean") {
1716
- return evaluateAccess(access, accessArgs);
1717
- }
1718
- return true;
1719
- };
1720
2051
  const serializeAccess = async (access) => {
1721
2052
  if (typeof access === "string") return access;
1722
2053
  if (typeof access === "boolean") return access;
1723
- return resolveAccess(access);
2054
+ return checkAccess(access, accessArgs);
1724
2055
  };
1725
2056
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
1726
2057
  slug: col.slug,
@@ -1731,7 +2062,7 @@ function registerRoutes(app, config) {
1731
2062
  update: await serializeAccess(col.access?.update),
1732
2063
  delete: await serializeAccess(col.access?.delete)
1733
2064
  },
1734
- fields: await Promise.all(col.fields.map(async (f) => ({
2065
+ fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
1735
2066
  name: f.name,
1736
2067
  type: f.type,
1737
2068
  label: f.label,
@@ -1759,7 +2090,7 @@ function registerRoutes(app, config) {
1759
2090
  read: await serializeAccess(glb.access?.read),
1760
2091
  update: await serializeAccess(glb.access?.update)
1761
2092
  },
1762
- fields: await Promise.all(glb.fields.map(async (f) => ({
2093
+ fields: await Promise.all(glb.fields.map(serializeFieldForApi).map(async (f) => ({
1763
2094
  name: f.name,
1764
2095
  type: f.type,
1765
2096
  label: f.label,
@@ -1784,6 +2115,78 @@ function registerRoutes(app, config) {
1784
2115
  admin: config.admin || {}
1785
2116
  });
1786
2117
  });
2118
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
2119
+ const { collection: colSlug, field: fieldName } = c.req.param();
2120
+ const siteId = c.req.header("X-Site-Id");
2121
+ let collections = [...config.collections];
2122
+ if (siteId && config.onSchemaFetch) {
2123
+ const dynamic = await config.onSchemaFetch(siteId);
2124
+ if (dynamic.collections) collections = [...collections, ...dynamic.collections];
2125
+ }
2126
+ const user = c.get("user");
2127
+ let collection = collections.find((col) => col.slug === colSlug);
2128
+ let field;
2129
+ if (collection) {
2130
+ const accessExpr = collection.access?.read;
2131
+ if (accessExpr !== void 0 && accessExpr !== null) {
2132
+ const accessArgs = { user, req: c.req, doc: null };
2133
+ const allowed = await checkAccess(accessExpr, accessArgs);
2134
+ if (!allowed) {
2135
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
2136
+ }
2137
+ }
2138
+ field = collection.fields.find((f) => f.name === fieldName);
2139
+ } else {
2140
+ let globals = [...config.globals];
2141
+ if (siteId && config.onSchemaFetch) {
2142
+ const dynamic = await config.onSchemaFetch(siteId);
2143
+ if (dynamic.globals) globals = [...globals, ...dynamic.globals];
2144
+ }
2145
+ const glb = globals.find((g) => g.slug === colSlug);
2146
+ if (!glb) {
2147
+ return c.json({ error: true, message: `${colSlug} not found as collection or global` }, 404);
2148
+ }
2149
+ const accessExpr = glb.access?.read;
2150
+ if (accessExpr !== void 0 && accessExpr !== null) {
2151
+ const accessArgs = { user, req: c.req, doc: null };
2152
+ const allowed = await checkAccess(accessExpr, accessArgs);
2153
+ if (!allowed) {
2154
+ return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
2155
+ }
2156
+ }
2157
+ field = glb.fields.find((f) => f.name === fieldName);
2158
+ }
2159
+ if (!field) {
2160
+ return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
2161
+ }
2162
+ let resolver;
2163
+ if (typeof field.options === "function") {
2164
+ resolver = field.options;
2165
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
2166
+ resolver = field.options.resolve;
2167
+ }
2168
+ if (!resolver) {
2169
+ return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
2170
+ }
2171
+ try {
2172
+ const db = c.get("db") || config.db;
2173
+ const queryParams = c.req.query();
2174
+ const reqContext = {
2175
+ query: queryParams,
2176
+ headers: c.req.header(),
2177
+ raw: c.req.raw
2178
+ };
2179
+ const result = await resolver({
2180
+ db,
2181
+ user,
2182
+ req: reqContext
2183
+ });
2184
+ return c.json(result);
2185
+ } catch (err) {
2186
+ console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
2187
+ return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
2188
+ }
2189
+ });
1787
2190
  app.get("/api/openapi.json", (c) => {
1788
2191
  return c.json(generateOpenApi(config));
1789
2192
  });