@dyrected/core 2.5.14 → 2.5.17

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 (52) hide show
  1. package/dist/app-B2tg7Djj.d.cts +1575 -0
  2. package/dist/app-B2tg7Djj.d.ts +1575 -0
  3. package/dist/app-BElen1tP.d.cts +1690 -0
  4. package/dist/app-BElen1tP.d.ts +1690 -0
  5. package/dist/app-Bh4_Opv0.d.cts +1522 -0
  6. package/dist/app-Bh4_Opv0.d.ts +1522 -0
  7. package/dist/app-Bv9gaDAN.d.cts +561 -0
  8. package/dist/app-Bv9gaDAN.d.ts +561 -0
  9. package/dist/app-BvG3bRc8.d.cts +419 -0
  10. package/dist/app-BvG3bRc8.d.ts +419 -0
  11. package/dist/app-C3B9N1KR.d.cts +1522 -0
  12. package/dist/app-C3B9N1KR.d.ts +1522 -0
  13. package/dist/app-DDJJa0ep.d.cts +1621 -0
  14. package/dist/app-DDJJa0ep.d.ts +1621 -0
  15. package/dist/app-DO1s9YW1.d.cts +1621 -0
  16. package/dist/app-DO1s9YW1.d.ts +1621 -0
  17. package/dist/app-DTP3-9PJ.d.cts +561 -0
  18. package/dist/app-DTP3-9PJ.d.ts +561 -0
  19. package/dist/app-DbKDGYTI.d.cts +566 -0
  20. package/dist/app-DbKDGYTI.d.ts +566 -0
  21. package/dist/app-DqRO-CMi.d.cts +1457 -0
  22. package/dist/app-DqRO-CMi.d.ts +1457 -0
  23. package/dist/app-DvaFpOtj.d.cts +398 -0
  24. package/dist/app-DvaFpOtj.d.ts +398 -0
  25. package/dist/app-FGzip4XM.d.cts +1563 -0
  26. package/dist/app-FGzip4XM.d.ts +1563 -0
  27. package/dist/app-T0alZAE0.d.cts +383 -0
  28. package/dist/app-T0alZAE0.d.ts +383 -0
  29. package/dist/app-oQt5-9MU.d.cts +1560 -0
  30. package/dist/app-oQt5-9MU.d.ts +1560 -0
  31. package/dist/app-rZj1VFer.d.cts +1621 -0
  32. package/dist/app-rZj1VFer.d.ts +1621 -0
  33. package/dist/app-wo82JRHl.d.cts +445 -0
  34. package/dist/app-wo82JRHl.d.ts +445 -0
  35. package/dist/chunk-23URSKPI.js +2371 -0
  36. package/dist/chunk-2JMA3M5S.js +2475 -0
  37. package/dist/chunk-3FZEUK36.js +2470 -0
  38. package/dist/chunk-DOJHZ7XN.js +2394 -0
  39. package/dist/chunk-EXXOPW3I.js +2483 -0
  40. package/dist/chunk-PKNFV7KE.js +2469 -0
  41. package/dist/chunk-UBTRANFX.js +2476 -0
  42. package/dist/chunk-W6KURRMW.js +2471 -0
  43. package/dist/chunk-YNJ3YC4N.js +2483 -0
  44. package/dist/index.cjs +465 -48
  45. package/dist/index.d.cts +124 -8
  46. package/dist/index.d.ts +124 -8
  47. package/dist/index.js +9 -3
  48. package/dist/server.cjs +457 -46
  49. package/dist/server.d.cts +57 -15
  50. package/dist/server.d.ts +57 -15
  51. package/dist/server.js +1 -1
  52. package/package.json +1 -1
package/dist/server.cjs CHANGED
@@ -268,6 +268,158 @@ 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, options = {}) {
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
+ try {
279
+ const result = await hook({
280
+ ...args,
281
+ data: args.data !== void 0 ? currentPayload : void 0,
282
+ doc: args.doc !== void 0 ? currentPayload : void 0
283
+ });
284
+ if (result !== void 0) {
285
+ currentPayload = result;
286
+ }
287
+ } catch (err) {
288
+ if (options.isolated) {
289
+ console.error("[dyrected/core] Side-effect hook failed (error isolated \u2014 DB write was successful):", err);
290
+ } else {
291
+ throw err;
292
+ }
293
+ }
294
+ }
295
+ return currentPayload;
296
+ }
297
+ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
298
+ if (!data || typeof data !== "object") return data;
299
+ const result = { ...data };
300
+ for (const field of fields) {
301
+ if (!field.name) continue;
302
+ const value = result[field.name];
303
+ const origValue = originalDoc?.[field.name];
304
+ let updatedValue = value;
305
+ if (field.hooks?.beforeChange) {
306
+ for (const hook of field.hooks.beforeChange) {
307
+ updatedValue = await hook({
308
+ value: updatedValue,
309
+ originalDoc: originalDoc ?? void 0,
310
+ data: result,
311
+ user
312
+ });
313
+ }
314
+ result[field.name] = updatedValue;
315
+ }
316
+ if (updatedValue !== void 0 && updatedValue !== null) {
317
+ if (field.type === "object" && field.fields) {
318
+ result[field.name] = await executeFieldBeforeChange(
319
+ field.fields,
320
+ updatedValue,
321
+ origValue,
322
+ user
323
+ );
324
+ } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
325
+ const arrayResult = [];
326
+ for (let i = 0; i < updatedValue.length; i++) {
327
+ const item = updatedValue[i];
328
+ const origItem = Array.isArray(origValue) ? origValue[i] : null;
329
+ arrayResult.push(
330
+ await executeFieldBeforeChange(field.fields, item, origItem, user)
331
+ );
332
+ }
333
+ result[field.name] = arrayResult;
334
+ } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
335
+ const blocksResult = [];
336
+ for (let i = 0; i < updatedValue.length; i++) {
337
+ const blockData = updatedValue[i];
338
+ const origBlock = Array.isArray(origValue) ? origValue[i] : null;
339
+ const blockConfig = field.blocks.find(
340
+ (b) => b.slug === blockData.blockType
341
+ );
342
+ if (blockConfig) {
343
+ blocksResult.push(
344
+ await executeFieldBeforeChange(
345
+ blockConfig.fields,
346
+ blockData,
347
+ origBlock,
348
+ user
349
+ )
350
+ );
351
+ } else {
352
+ blocksResult.push(blockData);
353
+ }
354
+ }
355
+ result[field.name] = blocksResult;
356
+ }
357
+ }
358
+ }
359
+ return result;
360
+ }
361
+ async function executeFieldAfterRead(fields, doc, user) {
362
+ if (!doc || typeof doc !== "object") return doc;
363
+ const result = { ...doc };
364
+ for (const field of fields) {
365
+ if (!field.name) continue;
366
+ const value = result[field.name];
367
+ let updatedValue = value;
368
+ if (field.hooks?.afterRead) {
369
+ for (const hook of field.hooks.afterRead) {
370
+ updatedValue = await hook({
371
+ value: updatedValue,
372
+ doc: result,
373
+ user
374
+ });
375
+ }
376
+ result[field.name] = updatedValue;
377
+ }
378
+ if (updatedValue !== void 0 && updatedValue !== null) {
379
+ if (field.type === "object" && field.fields) {
380
+ result[field.name] = await executeFieldAfterRead(
381
+ field.fields,
382
+ updatedValue,
383
+ user
384
+ );
385
+ } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
386
+ const arrayResult = [];
387
+ for (const item of updatedValue) {
388
+ arrayResult.push(
389
+ await executeFieldAfterRead(
390
+ field.fields,
391
+ item,
392
+ user
393
+ )
394
+ );
395
+ }
396
+ result[field.name] = arrayResult;
397
+ } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
398
+ const blocksResult = [];
399
+ for (const blockData of updatedValue) {
400
+ const typedBlock = blockData;
401
+ const blockConfig = field.blocks.find(
402
+ (b) => b.slug === typedBlock.blockType
403
+ );
404
+ if (blockConfig) {
405
+ blocksResult.push(
406
+ await executeFieldAfterRead(
407
+ blockConfig.fields,
408
+ typedBlock,
409
+ user
410
+ )
411
+ );
412
+ } else {
413
+ blocksResult.push(blockData);
414
+ }
415
+ }
416
+ result[field.name] = blocksResult;
417
+ }
418
+ }
419
+ }
420
+ return result;
421
+ }
422
+
271
423
  // src/controllers/collection.controller.ts
272
424
  var CollectionController = class {
273
425
  collection;
@@ -282,6 +434,7 @@ var CollectionController = class {
282
434
  const page = Number(c.req.query("page")) || 1;
283
435
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 2;
284
436
  const sort = c.req.query("sort") || void 0;
437
+ const user = c.get("user");
285
438
  let where = void 0;
286
439
  const whereRaw = c.req.query("where");
287
440
  if (whereRaw) {
@@ -290,6 +443,14 @@ var CollectionController = class {
290
443
  } catch {
291
444
  }
292
445
  }
446
+ const beforeReadResult = await runCollectionHooks(this.collection.hooks?.beforeRead, {
447
+ req: c.req,
448
+ query: where,
449
+ user
450
+ });
451
+ if (beforeReadResult !== void 0) {
452
+ where = beforeReadResult;
453
+ }
293
454
  let result = await db.find({
294
455
  collection: this.collection.slug,
295
456
  limit,
@@ -311,6 +472,17 @@ var CollectionController = class {
311
472
  });
312
473
  }
313
474
  result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
475
+ const processedDocs = [];
476
+ for (const doc of result.docs) {
477
+ const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
478
+ doc,
479
+ req: c.req,
480
+ user
481
+ });
482
+ const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
483
+ processedDocs.push(docWithFieldHooks);
484
+ }
485
+ result.docs = processedDocs;
314
486
  if (depth > 0) {
315
487
  const populationService = new PopulationService(db, config.collections);
316
488
  result = await populationService.populateResult(result, this.collection.fields, depth);
@@ -323,21 +495,28 @@ var CollectionController = class {
323
495
  if (!db) return c.json({ message: "Database not configured" }, 500);
324
496
  const id = c.req.param("id");
325
497
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
498
+ const user = c.get("user");
326
499
  if (!id) return c.json({ message: "Missing ID" }, 400);
327
500
  const doc = await db.findOne({ collection: this.collection.slug, id });
328
501
  if (!doc) return c.json({ message: "Not Found" }, 404);
329
502
  const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
330
- if (depth > 0 && docWithDefaults) {
503
+ const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
504
+ doc: docWithDefaults,
505
+ req: c.req,
506
+ user
507
+ });
508
+ const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
509
+ if (depth > 0 && docWithFieldHooks) {
331
510
  const populationService = new PopulationService(db, config.collections);
332
511
  const populatedDoc = await populationService.populate({
333
- data: docWithDefaults,
512
+ data: docWithFieldHooks,
334
513
  fields: this.collection.fields,
335
514
  currentDepth: 0,
336
515
  maxDepth: depth
337
516
  });
338
517
  return c.json(populatedDoc);
339
518
  }
340
- return c.json(docWithDefaults);
519
+ return c.json(docWithFieldHooks);
341
520
  }
342
521
  async create(c) {
343
522
  const config = c.get("config");
@@ -350,7 +529,7 @@ var CollectionController = class {
350
529
  const body = await c.req.json();
351
530
  const user = c.get("user");
352
531
  const now = (/* @__PURE__ */ new Date()).toISOString();
353
- const data = {
532
+ let data = {
354
533
  ...body,
355
534
  createdAt: now,
356
535
  updatedAt: now,
@@ -360,6 +539,13 @@ var CollectionController = class {
360
539
  if (this.collection.auth && data.password) {
361
540
  data.password = await hashPassword(data.password);
362
541
  }
542
+ data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
543
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
544
+ data,
545
+ req: c.req,
546
+ user,
547
+ operation: "create"
548
+ });
363
549
  const doc = await db.create({ collection: this.collection.slug, data });
364
550
  if (this.collection.audit && db) {
365
551
  AuditService.log(db, {
@@ -371,7 +557,19 @@ var CollectionController = class {
371
557
  after: doc
372
558
  });
373
559
  }
374
- return c.json(doc, 201);
560
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
561
+ doc,
562
+ user,
563
+ req: c.req,
564
+ operation: "create"
565
+ }, { isolated: true });
566
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
567
+ doc,
568
+ req: c.req,
569
+ user
570
+ });
571
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
572
+ return c.json(finalDoc, 201);
375
573
  }
376
574
  async upload(c) {
377
575
  const config = c.get("config");
@@ -398,18 +596,38 @@ var CollectionController = class {
398
596
  });
399
597
  const user = c.get("user");
400
598
  const now = (/* @__PURE__ */ new Date()).toISOString();
599
+ let data = {
600
+ ...otherData,
601
+ ...fileData,
602
+ createdAt: now,
603
+ updatedAt: now,
604
+ createdBy: user?.sub ?? null,
605
+ updatedBy: user?.sub ?? null
606
+ };
607
+ data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
608
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
609
+ data,
610
+ req: c.req,
611
+ user,
612
+ operation: "create"
613
+ });
401
614
  const doc = await config.db.create({
402
615
  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
- }
616
+ data
411
617
  });
412
- return c.json(doc, 201);
618
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
619
+ doc,
620
+ user,
621
+ req: c.req,
622
+ operation: "create"
623
+ }, { isolated: true });
624
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
625
+ doc,
626
+ req: c.req,
627
+ user
628
+ });
629
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
630
+ return c.json(finalDoc, 201);
413
631
  }
414
632
  async update(c) {
415
633
  const config = c.get("config");
@@ -419,7 +637,7 @@ var CollectionController = class {
419
637
  if (!id) return c.json({ message: "Missing ID" }, 400);
420
638
  const body = await c.req.json();
421
639
  const user = c.get("user");
422
- const data = { ...body };
640
+ let data = { ...body };
423
641
  if (this.collection.auth) {
424
642
  delete data.password;
425
643
  delete data.oldPassword;
@@ -429,10 +647,20 @@ var CollectionController = class {
429
647
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
430
648
  updatedBy: user?.sub ?? null
431
649
  });
650
+ const originalDoc = await db.findOne({ collection: this.collection.slug, id });
651
+ if (!originalDoc) return c.json({ message: "Not Found" }, 404);
432
652
  let before = null;
433
653
  if (this.collection.audit) {
434
- before = await db.findOne({ collection: this.collection.slug, id });
435
- }
654
+ before = originalDoc;
655
+ }
656
+ data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user);
657
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
658
+ data,
659
+ doc: originalDoc,
660
+ req: c.req,
661
+ user,
662
+ operation: "update"
663
+ });
436
664
  const doc = await db.update({ collection: this.collection.slug, id, data });
437
665
  if (this.collection.audit && db) {
438
666
  AuditService.log(db, {
@@ -444,7 +672,20 @@ var CollectionController = class {
444
672
  after: doc
445
673
  });
446
674
  }
447
- return c.json(doc);
675
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
676
+ doc,
677
+ previousDoc: originalDoc,
678
+ user,
679
+ req: c.req,
680
+ operation: "update"
681
+ }, { isolated: true });
682
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
683
+ doc,
684
+ req: c.req,
685
+ user
686
+ });
687
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
688
+ return c.json(finalDoc);
448
689
  }
449
690
  /**
450
691
  * POST /api/collections/:slug/:id/change-password
@@ -524,10 +765,18 @@ var CollectionController = class {
524
765
  const id = c.req.param("id");
525
766
  if (!id) return c.json({ message: "Missing ID" }, 400);
526
767
  const user = c.get("user");
768
+ const doc = await db.findOne({ collection: this.collection.slug, id });
769
+ if (!doc) return c.json({ message: "Not Found" }, 404);
527
770
  let before = null;
528
771
  if (this.collection.audit) {
529
- before = await db.findOne({ collection: this.collection.slug, id });
772
+ before = doc;
530
773
  }
774
+ await runCollectionHooks(this.collection.hooks?.beforeDelete, {
775
+ id,
776
+ doc,
777
+ user,
778
+ req: c.req
779
+ });
531
780
  await db.delete({ collection: this.collection.slug, id });
532
781
  if (this.collection.audit && db) {
533
782
  AuditService.log(db, {
@@ -539,6 +788,12 @@ var CollectionController = class {
539
788
  after: null
540
789
  });
541
790
  }
791
+ await runCollectionHooks(this.collection.hooks?.afterDelete, {
792
+ id,
793
+ doc,
794
+ user,
795
+ req: c.req
796
+ }, { isolated: true });
542
797
  return c.json({ message: "Deleted" });
543
798
  }
544
799
  async deleteMany(c) {
@@ -563,10 +818,21 @@ var CollectionController = class {
563
818
  const failed = [];
564
819
  for (const id of ids) {
565
820
  try {
821
+ const doc = await db.findOne({ collection: this.collection.slug, id });
822
+ if (!doc) {
823
+ failed.push({ id, error: "Not Found" });
824
+ continue;
825
+ }
566
826
  let before = null;
567
827
  if (this.collection.audit) {
568
- before = await db.findOne({ collection: this.collection.slug, id });
828
+ before = doc;
569
829
  }
830
+ await runCollectionHooks(this.collection.hooks?.beforeDelete, {
831
+ id,
832
+ doc,
833
+ user,
834
+ req: c.req
835
+ });
570
836
  await db.delete({ collection: this.collection.slug, id });
571
837
  deleted.push(id);
572
838
  if (this.collection.audit) {
@@ -579,6 +845,12 @@ var CollectionController = class {
579
845
  after: null
580
846
  });
581
847
  }
848
+ await runCollectionHooks(this.collection.hooks?.afterDelete, {
849
+ id,
850
+ doc,
851
+ user,
852
+ req: c.req
853
+ }, { isolated: true });
582
854
  } catch (err) {
583
855
  failed.push({ id, error: err?.message ?? "Unknown error" });
584
856
  }
@@ -623,6 +895,16 @@ var GlobalController = class {
623
895
  const db = config.db;
624
896
  if (!db) return c.json({ message: "Database not configured" }, 500);
625
897
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
898
+ const user = c.get("user");
899
+ let query = void 0;
900
+ const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
901
+ req: c.req,
902
+ query,
903
+ user
904
+ });
905
+ if (beforeReadResult !== void 0) {
906
+ query = beforeReadResult;
907
+ }
626
908
  let data = await db.getGlobal({ slug: this.global.slug });
627
909
  const isEmpty = !data || Object.keys(data).length === 0;
628
910
  if (isEmpty && this.global.initialData) {
@@ -631,24 +913,53 @@ var GlobalController = class {
631
913
  data = this.global.initialData;
632
914
  }
633
915
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
634
- if (depth > 0 && dataWithDefaults) {
916
+ const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
917
+ doc: dataWithDefaults,
918
+ req: c.req,
919
+ user
920
+ });
921
+ const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user);
922
+ if (depth > 0 && docWithFieldHooks) {
635
923
  const populationService = new PopulationService(db, config.collections);
636
924
  const populatedData = await populationService.populate({
637
- data: dataWithDefaults,
925
+ data: docWithFieldHooks,
638
926
  fields: this.global.fields,
639
927
  currentDepth: 0,
640
928
  maxDepth: depth
641
929
  });
642
930
  return c.json(populatedData);
643
931
  }
644
- return c.json(dataWithDefaults);
932
+ return c.json(docWithFieldHooks);
645
933
  }
646
934
  async update(c) {
647
935
  const db = c.get("config").db;
648
936
  if (!db) return c.json({ message: "Database not configured" }, 500);
649
937
  const body = await c.req.json();
650
- const data = await db.updateGlobal({ slug: this.global.slug, data: body });
651
- return c.json(data);
938
+ const user = c.get("user");
939
+ const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
940
+ let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user);
941
+ data = await runCollectionHooks(this.global.hooks?.beforeChange, {
942
+ data,
943
+ doc: originalDoc,
944
+ req: c.req,
945
+ user,
946
+ operation: "update"
947
+ });
948
+ const updated = await db.updateGlobal({ slug: this.global.slug, data });
949
+ await runCollectionHooks(this.global.hooks?.afterChange, {
950
+ doc: updated,
951
+ previousDoc: originalDoc,
952
+ user,
953
+ req: c.req,
954
+ operation: "update"
955
+ }, { isolated: true });
956
+ const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
957
+ doc: updated,
958
+ req: c.req,
959
+ user
960
+ });
961
+ const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user);
962
+ return c.json(finalDoc);
652
963
  }
653
964
  async seed(c) {
654
965
  const config = c.get("config");
@@ -1557,12 +1868,13 @@ function fieldToSchema(field) {
1557
1868
  schema = { type: "string", format: "date-time" };
1558
1869
  break;
1559
1870
  case "select":
1560
- schema = { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) };
1871
+ case "radio":
1872
+ schema = { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 };
1561
1873
  break;
1562
1874
  case "multiSelect":
1563
1875
  schema = {
1564
1876
  type: "array",
1565
- items: { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) }
1877
+ items: { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 }
1566
1878
  };
1567
1879
  break;
1568
1880
  case "relationship":
@@ -1686,6 +1998,49 @@ function accessGate(target, action) {
1686
1998
  await next();
1687
1999
  };
1688
2000
  }
2001
+ async function checkAccess(access, accessArgs) {
2002
+ if (access === void 0 || access === null) return true;
2003
+ if (typeof access === "function") {
2004
+ try {
2005
+ const result = await access(accessArgs);
2006
+ return typeof result === "boolean" ? result : !!result;
2007
+ } catch (err) {
2008
+ console.error("[dyrected/core] Functional access check failed:", err);
2009
+ return false;
2010
+ }
2011
+ }
2012
+ if (typeof access === "string" || typeof access === "boolean") {
2013
+ return evaluateAccess(access, accessArgs);
2014
+ }
2015
+ return true;
2016
+ }
2017
+ function serializeFieldForApi(f) {
2018
+ if (!f) return f;
2019
+ const serialized = { ...f };
2020
+ if (serialized.admin?.hooks) {
2021
+ const hooks = { ...serialized.admin.hooks };
2022
+ if (typeof hooks.onChange === "function") {
2023
+ hooks.onChange = hooks.onChange.toString();
2024
+ }
2025
+ if (typeof hooks.options === "function") {
2026
+ hooks.options = hooks.options.toString();
2027
+ }
2028
+ serialized.admin = { ...serialized.admin, hooks };
2029
+ }
2030
+ if (typeof serialized.options === "function" || serialized.options && typeof serialized.options === "object" && "resolve" in serialized.options) {
2031
+ serialized.options = { _dynamic: true };
2032
+ }
2033
+ if (serialized.fields) {
2034
+ serialized.fields = serialized.fields.map(serializeFieldForApi);
2035
+ }
2036
+ if (serialized.blocks) {
2037
+ serialized.blocks = serialized.blocks.map((b) => ({
2038
+ ...b,
2039
+ fields: b.fields?.map(serializeFieldForApi)
2040
+ }));
2041
+ }
2042
+ return serialized;
2043
+ }
1689
2044
  function registerRoutes(app, config) {
1690
2045
  app.get("/api/schemas", optionalAuth(), async (c) => {
1691
2046
  const siteId = c.req.header("X-Site-Id");
@@ -1701,26 +2056,10 @@ function registerRoutes(app, config) {
1701
2056
  }
1702
2057
  const user = c.get("user");
1703
2058
  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
2059
  const serializeAccess = async (access) => {
1721
2060
  if (typeof access === "string") return access;
1722
2061
  if (typeof access === "boolean") return access;
1723
- return resolveAccess(access);
2062
+ return checkAccess(access, accessArgs);
1724
2063
  };
1725
2064
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
1726
2065
  slug: col.slug,
@@ -1731,7 +2070,7 @@ function registerRoutes(app, config) {
1731
2070
  update: await serializeAccess(col.access?.update),
1732
2071
  delete: await serializeAccess(col.access?.delete)
1733
2072
  },
1734
- fields: await Promise.all(col.fields.map(async (f) => ({
2073
+ fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
1735
2074
  name: f.name,
1736
2075
  type: f.type,
1737
2076
  label: f.label,
@@ -1759,7 +2098,7 @@ function registerRoutes(app, config) {
1759
2098
  read: await serializeAccess(glb.access?.read),
1760
2099
  update: await serializeAccess(glb.access?.update)
1761
2100
  },
1762
- fields: await Promise.all(glb.fields.map(async (f) => ({
2101
+ fields: await Promise.all(glb.fields.map(serializeFieldForApi).map(async (f) => ({
1763
2102
  name: f.name,
1764
2103
  type: f.type,
1765
2104
  label: f.label,
@@ -1784,6 +2123,78 @@ function registerRoutes(app, config) {
1784
2123
  admin: config.admin || {}
1785
2124
  });
1786
2125
  });
2126
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
2127
+ const { collection: colSlug, field: fieldName } = c.req.param();
2128
+ const siteId = c.req.header("X-Site-Id");
2129
+ let collections = [...config.collections];
2130
+ if (siteId && config.onSchemaFetch) {
2131
+ const dynamic = await config.onSchemaFetch(siteId);
2132
+ if (dynamic.collections) collections = [...collections, ...dynamic.collections];
2133
+ }
2134
+ const user = c.get("user");
2135
+ let collection = collections.find((col) => col.slug === colSlug);
2136
+ let field;
2137
+ if (collection) {
2138
+ const accessExpr = collection.access?.read;
2139
+ if (accessExpr !== void 0 && accessExpr !== null) {
2140
+ const accessArgs = { user, req: c.req, doc: null };
2141
+ const allowed = await checkAccess(accessExpr, accessArgs);
2142
+ if (!allowed) {
2143
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
2144
+ }
2145
+ }
2146
+ field = collection.fields.find((f) => f.name === fieldName);
2147
+ } else {
2148
+ let globals = [...config.globals];
2149
+ if (siteId && config.onSchemaFetch) {
2150
+ const dynamic = await config.onSchemaFetch(siteId);
2151
+ if (dynamic.globals) globals = [...globals, ...dynamic.globals];
2152
+ }
2153
+ const glb = globals.find((g) => g.slug === colSlug);
2154
+ if (!glb) {
2155
+ return c.json({ error: true, message: `${colSlug} not found as collection or global` }, 404);
2156
+ }
2157
+ const accessExpr = glb.access?.read;
2158
+ if (accessExpr !== void 0 && accessExpr !== null) {
2159
+ const accessArgs = { user, req: c.req, doc: null };
2160
+ const allowed = await checkAccess(accessExpr, accessArgs);
2161
+ if (!allowed) {
2162
+ return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
2163
+ }
2164
+ }
2165
+ field = glb.fields.find((f) => f.name === fieldName);
2166
+ }
2167
+ if (!field) {
2168
+ return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
2169
+ }
2170
+ let resolver;
2171
+ if (typeof field.options === "function") {
2172
+ resolver = field.options;
2173
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
2174
+ resolver = field.options.resolve;
2175
+ }
2176
+ if (!resolver) {
2177
+ return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
2178
+ }
2179
+ try {
2180
+ const db = c.get("db") || config.db;
2181
+ const queryParams = c.req.query();
2182
+ const reqContext = {
2183
+ query: queryParams,
2184
+ headers: c.req.header(),
2185
+ raw: c.req.raw
2186
+ };
2187
+ const result = await resolver({
2188
+ db,
2189
+ user,
2190
+ req: reqContext
2191
+ });
2192
+ return c.json(result);
2193
+ } catch (err) {
2194
+ console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
2195
+ return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
2196
+ }
2197
+ });
1787
2198
  app.get("/api/openapi.json", (c) => {
1788
2199
  return c.json(generateOpenApi(config));
1789
2200
  });