@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/index.cjs CHANGED
@@ -34,11 +34,14 @@ __export(index_exports, {
34
34
  defineCollection: () => defineCollection,
35
35
  defineConfig: () => defineConfig,
36
36
  defineGlobal: () => defineGlobal,
37
+ executeFieldAfterRead: () => executeFieldAfterRead,
38
+ executeFieldBeforeChange: () => executeFieldBeforeChange,
37
39
  generateAIPrompt: () => generateAIPrompt,
38
40
  generateFreshSetupPrompt: () => generateFreshSetupPrompt,
39
41
  normalizeConfig: () => normalizeConfig,
40
42
  parseMongoWhere: () => parseMongoWhere,
41
- parseSqlWhere: () => parseSqlWhere
43
+ parseSqlWhere: () => parseSqlWhere,
44
+ runCollectionHooks: () => runCollectionHooks
42
45
  });
43
46
  module.exports = __toCommonJS(index_exports);
44
47
 
@@ -1166,6 +1169,158 @@ function parseMongoWhere(where) {
1166
1169
  return buildClause(where);
1167
1170
  }
1168
1171
 
1172
+ // src/utils/hooks.ts
1173
+ async function runCollectionHooks(hooks, args, options = {}) {
1174
+ if (!hooks || hooks.length === 0) {
1175
+ return args.data ?? args.doc ?? void 0;
1176
+ }
1177
+ let currentPayload = args.data ?? args.doc ?? void 0;
1178
+ for (const hook of hooks) {
1179
+ try {
1180
+ const result = await hook({
1181
+ ...args,
1182
+ data: args.data !== void 0 ? currentPayload : void 0,
1183
+ doc: args.doc !== void 0 ? currentPayload : void 0
1184
+ });
1185
+ if (result !== void 0) {
1186
+ currentPayload = result;
1187
+ }
1188
+ } catch (err) {
1189
+ if (options.isolated) {
1190
+ console.error("[dyrected/core] Side-effect hook failed (error isolated \u2014 DB write was successful):", err);
1191
+ } else {
1192
+ throw err;
1193
+ }
1194
+ }
1195
+ }
1196
+ return currentPayload;
1197
+ }
1198
+ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
1199
+ if (!data || typeof data !== "object") return data;
1200
+ const result = { ...data };
1201
+ for (const field of fields) {
1202
+ if (!field.name) continue;
1203
+ const value = result[field.name];
1204
+ const origValue = originalDoc?.[field.name];
1205
+ let updatedValue = value;
1206
+ if (field.hooks?.beforeChange) {
1207
+ for (const hook of field.hooks.beforeChange) {
1208
+ updatedValue = await hook({
1209
+ value: updatedValue,
1210
+ originalDoc: originalDoc ?? void 0,
1211
+ data: result,
1212
+ user
1213
+ });
1214
+ }
1215
+ result[field.name] = updatedValue;
1216
+ }
1217
+ if (updatedValue !== void 0 && updatedValue !== null) {
1218
+ if (field.type === "object" && field.fields) {
1219
+ result[field.name] = await executeFieldBeforeChange(
1220
+ field.fields,
1221
+ updatedValue,
1222
+ origValue,
1223
+ user
1224
+ );
1225
+ } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
1226
+ const arrayResult = [];
1227
+ for (let i = 0; i < updatedValue.length; i++) {
1228
+ const item = updatedValue[i];
1229
+ const origItem = Array.isArray(origValue) ? origValue[i] : null;
1230
+ arrayResult.push(
1231
+ await executeFieldBeforeChange(field.fields, item, origItem, user)
1232
+ );
1233
+ }
1234
+ result[field.name] = arrayResult;
1235
+ } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
1236
+ const blocksResult = [];
1237
+ for (let i = 0; i < updatedValue.length; i++) {
1238
+ const blockData = updatedValue[i];
1239
+ const origBlock = Array.isArray(origValue) ? origValue[i] : null;
1240
+ const blockConfig = field.blocks.find(
1241
+ (b) => b.slug === blockData.blockType
1242
+ );
1243
+ if (blockConfig) {
1244
+ blocksResult.push(
1245
+ await executeFieldBeforeChange(
1246
+ blockConfig.fields,
1247
+ blockData,
1248
+ origBlock,
1249
+ user
1250
+ )
1251
+ );
1252
+ } else {
1253
+ blocksResult.push(blockData);
1254
+ }
1255
+ }
1256
+ result[field.name] = blocksResult;
1257
+ }
1258
+ }
1259
+ }
1260
+ return result;
1261
+ }
1262
+ async function executeFieldAfterRead(fields, doc, user) {
1263
+ if (!doc || typeof doc !== "object") return doc;
1264
+ const result = { ...doc };
1265
+ for (const field of fields) {
1266
+ if (!field.name) continue;
1267
+ const value = result[field.name];
1268
+ let updatedValue = value;
1269
+ if (field.hooks?.afterRead) {
1270
+ for (const hook of field.hooks.afterRead) {
1271
+ updatedValue = await hook({
1272
+ value: updatedValue,
1273
+ doc: result,
1274
+ user
1275
+ });
1276
+ }
1277
+ result[field.name] = updatedValue;
1278
+ }
1279
+ if (updatedValue !== void 0 && updatedValue !== null) {
1280
+ if (field.type === "object" && field.fields) {
1281
+ result[field.name] = await executeFieldAfterRead(
1282
+ field.fields,
1283
+ updatedValue,
1284
+ user
1285
+ );
1286
+ } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
1287
+ const arrayResult = [];
1288
+ for (const item of updatedValue) {
1289
+ arrayResult.push(
1290
+ await executeFieldAfterRead(
1291
+ field.fields,
1292
+ item,
1293
+ user
1294
+ )
1295
+ );
1296
+ }
1297
+ result[field.name] = arrayResult;
1298
+ } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
1299
+ const blocksResult = [];
1300
+ for (const blockData of updatedValue) {
1301
+ const typedBlock = blockData;
1302
+ const blockConfig = field.blocks.find(
1303
+ (b) => b.slug === typedBlock.blockType
1304
+ );
1305
+ if (blockConfig) {
1306
+ blocksResult.push(
1307
+ await executeFieldAfterRead(
1308
+ blockConfig.fields,
1309
+ typedBlock,
1310
+ user
1311
+ )
1312
+ );
1313
+ } else {
1314
+ blocksResult.push(blockData);
1315
+ }
1316
+ }
1317
+ result[field.name] = blocksResult;
1318
+ }
1319
+ }
1320
+ }
1321
+ return result;
1322
+ }
1323
+
1169
1324
  // src/app.ts
1170
1325
  var import_hono = require("hono");
1171
1326
  var import_cors = require("hono/cors");
@@ -1408,6 +1563,7 @@ var CollectionController = class {
1408
1563
  const page = Number(c.req.query("page")) || 1;
1409
1564
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 2;
1410
1565
  const sort = c.req.query("sort") || void 0;
1566
+ const user = c.get("user");
1411
1567
  let where = void 0;
1412
1568
  const whereRaw = c.req.query("where");
1413
1569
  if (whereRaw) {
@@ -1416,6 +1572,14 @@ var CollectionController = class {
1416
1572
  } catch {
1417
1573
  }
1418
1574
  }
1575
+ const beforeReadResult = await runCollectionHooks(this.collection.hooks?.beforeRead, {
1576
+ req: c.req,
1577
+ query: where,
1578
+ user
1579
+ });
1580
+ if (beforeReadResult !== void 0) {
1581
+ where = beforeReadResult;
1582
+ }
1419
1583
  let result = await db.find({
1420
1584
  collection: this.collection.slug,
1421
1585
  limit,
@@ -1437,6 +1601,17 @@ var CollectionController = class {
1437
1601
  });
1438
1602
  }
1439
1603
  result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
1604
+ const processedDocs = [];
1605
+ for (const doc of result.docs) {
1606
+ const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
1607
+ doc,
1608
+ req: c.req,
1609
+ user
1610
+ });
1611
+ const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
1612
+ processedDocs.push(docWithFieldHooks);
1613
+ }
1614
+ result.docs = processedDocs;
1440
1615
  if (depth > 0) {
1441
1616
  const populationService = new PopulationService(db, config.collections);
1442
1617
  result = await populationService.populateResult(result, this.collection.fields, depth);
@@ -1449,21 +1624,28 @@ var CollectionController = class {
1449
1624
  if (!db) return c.json({ message: "Database not configured" }, 500);
1450
1625
  const id = c.req.param("id");
1451
1626
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
1627
+ const user = c.get("user");
1452
1628
  if (!id) return c.json({ message: "Missing ID" }, 400);
1453
1629
  const doc = await db.findOne({ collection: this.collection.slug, id });
1454
1630
  if (!doc) return c.json({ message: "Not Found" }, 404);
1455
1631
  const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
1456
- if (depth > 0 && docWithDefaults) {
1632
+ const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
1633
+ doc: docWithDefaults,
1634
+ req: c.req,
1635
+ user
1636
+ });
1637
+ const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
1638
+ if (depth > 0 && docWithFieldHooks) {
1457
1639
  const populationService = new PopulationService(db, config.collections);
1458
1640
  const populatedDoc = await populationService.populate({
1459
- data: docWithDefaults,
1641
+ data: docWithFieldHooks,
1460
1642
  fields: this.collection.fields,
1461
1643
  currentDepth: 0,
1462
1644
  maxDepth: depth
1463
1645
  });
1464
1646
  return c.json(populatedDoc);
1465
1647
  }
1466
- return c.json(docWithDefaults);
1648
+ return c.json(docWithFieldHooks);
1467
1649
  }
1468
1650
  async create(c) {
1469
1651
  const config = c.get("config");
@@ -1476,7 +1658,7 @@ var CollectionController = class {
1476
1658
  const body = await c.req.json();
1477
1659
  const user = c.get("user");
1478
1660
  const now = (/* @__PURE__ */ new Date()).toISOString();
1479
- const data = {
1661
+ let data = {
1480
1662
  ...body,
1481
1663
  createdAt: now,
1482
1664
  updatedAt: now,
@@ -1486,6 +1668,13 @@ var CollectionController = class {
1486
1668
  if (this.collection.auth && data.password) {
1487
1669
  data.password = await hashPassword(data.password);
1488
1670
  }
1671
+ data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
1672
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1673
+ data,
1674
+ req: c.req,
1675
+ user,
1676
+ operation: "create"
1677
+ });
1489
1678
  const doc = await db.create({ collection: this.collection.slug, data });
1490
1679
  if (this.collection.audit && db) {
1491
1680
  AuditService.log(db, {
@@ -1497,7 +1686,19 @@ var CollectionController = class {
1497
1686
  after: doc
1498
1687
  });
1499
1688
  }
1500
- return c.json(doc, 201);
1689
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
1690
+ doc,
1691
+ user,
1692
+ req: c.req,
1693
+ operation: "create"
1694
+ }, { isolated: true });
1695
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
1696
+ doc,
1697
+ req: c.req,
1698
+ user
1699
+ });
1700
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
1701
+ return c.json(finalDoc, 201);
1501
1702
  }
1502
1703
  async upload(c) {
1503
1704
  const config = c.get("config");
@@ -1524,18 +1725,38 @@ var CollectionController = class {
1524
1725
  });
1525
1726
  const user = c.get("user");
1526
1727
  const now = (/* @__PURE__ */ new Date()).toISOString();
1728
+ let data = {
1729
+ ...otherData,
1730
+ ...fileData,
1731
+ createdAt: now,
1732
+ updatedAt: now,
1733
+ createdBy: user?.sub ?? null,
1734
+ updatedBy: user?.sub ?? null
1735
+ };
1736
+ data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
1737
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1738
+ data,
1739
+ req: c.req,
1740
+ user,
1741
+ operation: "create"
1742
+ });
1527
1743
  const doc = await config.db.create({
1528
1744
  collection: this.collection.slug,
1529
- data: {
1530
- ...otherData,
1531
- ...fileData,
1532
- createdAt: now,
1533
- updatedAt: now,
1534
- createdBy: user?.sub ?? null,
1535
- updatedBy: user?.sub ?? null
1536
- }
1745
+ data
1537
1746
  });
1538
- return c.json(doc, 201);
1747
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
1748
+ doc,
1749
+ user,
1750
+ req: c.req,
1751
+ operation: "create"
1752
+ }, { isolated: true });
1753
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
1754
+ doc,
1755
+ req: c.req,
1756
+ user
1757
+ });
1758
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
1759
+ return c.json(finalDoc, 201);
1539
1760
  }
1540
1761
  async update(c) {
1541
1762
  const config = c.get("config");
@@ -1545,7 +1766,7 @@ var CollectionController = class {
1545
1766
  if (!id) return c.json({ message: "Missing ID" }, 400);
1546
1767
  const body = await c.req.json();
1547
1768
  const user = c.get("user");
1548
- const data = { ...body };
1769
+ let data = { ...body };
1549
1770
  if (this.collection.auth) {
1550
1771
  delete data.password;
1551
1772
  delete data.oldPassword;
@@ -1555,10 +1776,20 @@ var CollectionController = class {
1555
1776
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1556
1777
  updatedBy: user?.sub ?? null
1557
1778
  });
1779
+ const originalDoc = await db.findOne({ collection: this.collection.slug, id });
1780
+ if (!originalDoc) return c.json({ message: "Not Found" }, 404);
1558
1781
  let before = null;
1559
1782
  if (this.collection.audit) {
1560
- before = await db.findOne({ collection: this.collection.slug, id });
1561
- }
1783
+ before = originalDoc;
1784
+ }
1785
+ data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user);
1786
+ data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1787
+ data,
1788
+ doc: originalDoc,
1789
+ req: c.req,
1790
+ user,
1791
+ operation: "update"
1792
+ });
1562
1793
  const doc = await db.update({ collection: this.collection.slug, id, data });
1563
1794
  if (this.collection.audit && db) {
1564
1795
  AuditService.log(db, {
@@ -1570,7 +1801,20 @@ var CollectionController = class {
1570
1801
  after: doc
1571
1802
  });
1572
1803
  }
1573
- return c.json(doc);
1804
+ await runCollectionHooks(this.collection.hooks?.afterChange, {
1805
+ doc,
1806
+ previousDoc: originalDoc,
1807
+ user,
1808
+ req: c.req,
1809
+ operation: "update"
1810
+ }, { isolated: true });
1811
+ const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
1812
+ doc,
1813
+ req: c.req,
1814
+ user
1815
+ });
1816
+ const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
1817
+ return c.json(finalDoc);
1574
1818
  }
1575
1819
  /**
1576
1820
  * POST /api/collections/:slug/:id/change-password
@@ -1650,10 +1894,18 @@ var CollectionController = class {
1650
1894
  const id = c.req.param("id");
1651
1895
  if (!id) return c.json({ message: "Missing ID" }, 400);
1652
1896
  const user = c.get("user");
1897
+ const doc = await db.findOne({ collection: this.collection.slug, id });
1898
+ if (!doc) return c.json({ message: "Not Found" }, 404);
1653
1899
  let before = null;
1654
1900
  if (this.collection.audit) {
1655
- before = await db.findOne({ collection: this.collection.slug, id });
1901
+ before = doc;
1656
1902
  }
1903
+ await runCollectionHooks(this.collection.hooks?.beforeDelete, {
1904
+ id,
1905
+ doc,
1906
+ user,
1907
+ req: c.req
1908
+ });
1657
1909
  await db.delete({ collection: this.collection.slug, id });
1658
1910
  if (this.collection.audit && db) {
1659
1911
  AuditService.log(db, {
@@ -1665,6 +1917,12 @@ var CollectionController = class {
1665
1917
  after: null
1666
1918
  });
1667
1919
  }
1920
+ await runCollectionHooks(this.collection.hooks?.afterDelete, {
1921
+ id,
1922
+ doc,
1923
+ user,
1924
+ req: c.req
1925
+ }, { isolated: true });
1668
1926
  return c.json({ message: "Deleted" });
1669
1927
  }
1670
1928
  async deleteMany(c) {
@@ -1689,10 +1947,21 @@ var CollectionController = class {
1689
1947
  const failed = [];
1690
1948
  for (const id of ids) {
1691
1949
  try {
1950
+ const doc = await db.findOne({ collection: this.collection.slug, id });
1951
+ if (!doc) {
1952
+ failed.push({ id, error: "Not Found" });
1953
+ continue;
1954
+ }
1692
1955
  let before = null;
1693
1956
  if (this.collection.audit) {
1694
- before = await db.findOne({ collection: this.collection.slug, id });
1957
+ before = doc;
1695
1958
  }
1959
+ await runCollectionHooks(this.collection.hooks?.beforeDelete, {
1960
+ id,
1961
+ doc,
1962
+ user,
1963
+ req: c.req
1964
+ });
1696
1965
  await db.delete({ collection: this.collection.slug, id });
1697
1966
  deleted.push(id);
1698
1967
  if (this.collection.audit) {
@@ -1705,6 +1974,12 @@ var CollectionController = class {
1705
1974
  after: null
1706
1975
  });
1707
1976
  }
1977
+ await runCollectionHooks(this.collection.hooks?.afterDelete, {
1978
+ id,
1979
+ doc,
1980
+ user,
1981
+ req: c.req
1982
+ }, { isolated: true });
1708
1983
  } catch (err) {
1709
1984
  failed.push({ id, error: err?.message ?? "Unknown error" });
1710
1985
  }
@@ -1749,6 +2024,16 @@ var GlobalController = class {
1749
2024
  const db = config.db;
1750
2025
  if (!db) return c.json({ message: "Database not configured" }, 500);
1751
2026
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
2027
+ const user = c.get("user");
2028
+ let query = void 0;
2029
+ const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
2030
+ req: c.req,
2031
+ query,
2032
+ user
2033
+ });
2034
+ if (beforeReadResult !== void 0) {
2035
+ query = beforeReadResult;
2036
+ }
1752
2037
  let data = await db.getGlobal({ slug: this.global.slug });
1753
2038
  const isEmpty = !data || Object.keys(data).length === 0;
1754
2039
  if (isEmpty && this.global.initialData) {
@@ -1757,24 +2042,53 @@ var GlobalController = class {
1757
2042
  data = this.global.initialData;
1758
2043
  }
1759
2044
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
1760
- if (depth > 0 && dataWithDefaults) {
2045
+ const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
2046
+ doc: dataWithDefaults,
2047
+ req: c.req,
2048
+ user
2049
+ });
2050
+ const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user);
2051
+ if (depth > 0 && docWithFieldHooks) {
1761
2052
  const populationService = new PopulationService(db, config.collections);
1762
2053
  const populatedData = await populationService.populate({
1763
- data: dataWithDefaults,
2054
+ data: docWithFieldHooks,
1764
2055
  fields: this.global.fields,
1765
2056
  currentDepth: 0,
1766
2057
  maxDepth: depth
1767
2058
  });
1768
2059
  return c.json(populatedData);
1769
2060
  }
1770
- return c.json(dataWithDefaults);
2061
+ return c.json(docWithFieldHooks);
1771
2062
  }
1772
2063
  async update(c) {
1773
2064
  const db = c.get("config").db;
1774
2065
  if (!db) return c.json({ message: "Database not configured" }, 500);
1775
2066
  const body = await c.req.json();
1776
- const data = await db.updateGlobal({ slug: this.global.slug, data: body });
1777
- return c.json(data);
2067
+ const user = c.get("user");
2068
+ const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
2069
+ let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user);
2070
+ data = await runCollectionHooks(this.global.hooks?.beforeChange, {
2071
+ data,
2072
+ doc: originalDoc,
2073
+ req: c.req,
2074
+ user,
2075
+ operation: "update"
2076
+ });
2077
+ const updated = await db.updateGlobal({ slug: this.global.slug, data });
2078
+ await runCollectionHooks(this.global.hooks?.afterChange, {
2079
+ doc: updated,
2080
+ previousDoc: originalDoc,
2081
+ user,
2082
+ req: c.req,
2083
+ operation: "update"
2084
+ }, { isolated: true });
2085
+ const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
2086
+ doc: updated,
2087
+ req: c.req,
2088
+ user
2089
+ });
2090
+ const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user);
2091
+ return c.json(finalDoc);
1778
2092
  }
1779
2093
  async seed(c) {
1780
2094
  const config = c.get("config");
@@ -2683,12 +2997,13 @@ function fieldToSchema(field) {
2683
2997
  schema = { type: "string", format: "date-time" };
2684
2998
  break;
2685
2999
  case "select":
2686
- schema = { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) };
3000
+ case "radio":
3001
+ schema = { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 };
2687
3002
  break;
2688
3003
  case "multiSelect":
2689
3004
  schema = {
2690
3005
  type: "array",
2691
- items: { type: "string", enum: field.options?.map((o) => typeof o === "string" ? o : o.value) }
3006
+ items: { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 }
2692
3007
  };
2693
3008
  break;
2694
3009
  case "relationship":
@@ -2812,6 +3127,49 @@ function accessGate(target, action) {
2812
3127
  await next();
2813
3128
  };
2814
3129
  }
3130
+ async function checkAccess(access, accessArgs) {
3131
+ if (access === void 0 || access === null) return true;
3132
+ if (typeof access === "function") {
3133
+ try {
3134
+ const result = await access(accessArgs);
3135
+ return typeof result === "boolean" ? result : !!result;
3136
+ } catch (err) {
3137
+ console.error("[dyrected/core] Functional access check failed:", err);
3138
+ return false;
3139
+ }
3140
+ }
3141
+ if (typeof access === "string" || typeof access === "boolean") {
3142
+ return evaluateAccess(access, accessArgs);
3143
+ }
3144
+ return true;
3145
+ }
3146
+ function serializeFieldForApi(f) {
3147
+ if (!f) return f;
3148
+ const serialized = { ...f };
3149
+ if (serialized.admin?.hooks) {
3150
+ const hooks = { ...serialized.admin.hooks };
3151
+ if (typeof hooks.onChange === "function") {
3152
+ hooks.onChange = hooks.onChange.toString();
3153
+ }
3154
+ if (typeof hooks.options === "function") {
3155
+ hooks.options = hooks.options.toString();
3156
+ }
3157
+ serialized.admin = { ...serialized.admin, hooks };
3158
+ }
3159
+ if (typeof serialized.options === "function" || serialized.options && typeof serialized.options === "object" && "resolve" in serialized.options) {
3160
+ serialized.options = { _dynamic: true };
3161
+ }
3162
+ if (serialized.fields) {
3163
+ serialized.fields = serialized.fields.map(serializeFieldForApi);
3164
+ }
3165
+ if (serialized.blocks) {
3166
+ serialized.blocks = serialized.blocks.map((b) => ({
3167
+ ...b,
3168
+ fields: b.fields?.map(serializeFieldForApi)
3169
+ }));
3170
+ }
3171
+ return serialized;
3172
+ }
2815
3173
  function registerRoutes(app, config) {
2816
3174
  app.get("/api/schemas", optionalAuth(), async (c) => {
2817
3175
  const siteId = c.req.header("X-Site-Id");
@@ -2827,26 +3185,10 @@ function registerRoutes(app, config) {
2827
3185
  }
2828
3186
  const user = c.get("user");
2829
3187
  const accessArgs = { user, req: c.req, doc: null };
2830
- const resolveAccess = async (access) => {
2831
- if (access === void 0 || access === null) return true;
2832
- if (typeof access === "function") {
2833
- try {
2834
- const result = await access(accessArgs);
2835
- return typeof result === "boolean" ? result : !!result;
2836
- } catch (err) {
2837
- console.error("[dyrected/core] Functional access check failed:", err);
2838
- return false;
2839
- }
2840
- }
2841
- if (typeof access === "string" || typeof access === "boolean") {
2842
- return evaluateAccess(access, accessArgs);
2843
- }
2844
- return true;
2845
- };
2846
3188
  const serializeAccess = async (access) => {
2847
3189
  if (typeof access === "string") return access;
2848
3190
  if (typeof access === "boolean") return access;
2849
- return resolveAccess(access);
3191
+ return checkAccess(access, accessArgs);
2850
3192
  };
2851
3193
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
2852
3194
  slug: col.slug,
@@ -2857,7 +3199,7 @@ function registerRoutes(app, config) {
2857
3199
  update: await serializeAccess(col.access?.update),
2858
3200
  delete: await serializeAccess(col.access?.delete)
2859
3201
  },
2860
- fields: await Promise.all(col.fields.map(async (f) => ({
3202
+ fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
2861
3203
  name: f.name,
2862
3204
  type: f.type,
2863
3205
  label: f.label,
@@ -2885,7 +3227,7 @@ function registerRoutes(app, config) {
2885
3227
  read: await serializeAccess(glb.access?.read),
2886
3228
  update: await serializeAccess(glb.access?.update)
2887
3229
  },
2888
- fields: await Promise.all(glb.fields.map(async (f) => ({
3230
+ fields: await Promise.all(glb.fields.map(serializeFieldForApi).map(async (f) => ({
2889
3231
  name: f.name,
2890
3232
  type: f.type,
2891
3233
  label: f.label,
@@ -2910,6 +3252,78 @@ function registerRoutes(app, config) {
2910
3252
  admin: config.admin || {}
2911
3253
  });
2912
3254
  });
3255
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
3256
+ const { collection: colSlug, field: fieldName } = c.req.param();
3257
+ const siteId = c.req.header("X-Site-Id");
3258
+ let collections = [...config.collections];
3259
+ if (siteId && config.onSchemaFetch) {
3260
+ const dynamic = await config.onSchemaFetch(siteId);
3261
+ if (dynamic.collections) collections = [...collections, ...dynamic.collections];
3262
+ }
3263
+ const user = c.get("user");
3264
+ let collection = collections.find((col) => col.slug === colSlug);
3265
+ let field;
3266
+ if (collection) {
3267
+ const accessExpr = collection.access?.read;
3268
+ if (accessExpr !== void 0 && accessExpr !== null) {
3269
+ const accessArgs = { user, req: c.req, doc: null };
3270
+ const allowed = await checkAccess(accessExpr, accessArgs);
3271
+ if (!allowed) {
3272
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
3273
+ }
3274
+ }
3275
+ field = collection.fields.find((f) => f.name === fieldName);
3276
+ } else {
3277
+ let globals = [...config.globals];
3278
+ if (siteId && config.onSchemaFetch) {
3279
+ const dynamic = await config.onSchemaFetch(siteId);
3280
+ if (dynamic.globals) globals = [...globals, ...dynamic.globals];
3281
+ }
3282
+ const glb = globals.find((g) => g.slug === colSlug);
3283
+ if (!glb) {
3284
+ return c.json({ error: true, message: `${colSlug} not found as collection or global` }, 404);
3285
+ }
3286
+ const accessExpr = glb.access?.read;
3287
+ if (accessExpr !== void 0 && accessExpr !== null) {
3288
+ const accessArgs = { user, req: c.req, doc: null };
3289
+ const allowed = await checkAccess(accessExpr, accessArgs);
3290
+ if (!allowed) {
3291
+ return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
3292
+ }
3293
+ }
3294
+ field = glb.fields.find((f) => f.name === fieldName);
3295
+ }
3296
+ if (!field) {
3297
+ return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
3298
+ }
3299
+ let resolver;
3300
+ if (typeof field.options === "function") {
3301
+ resolver = field.options;
3302
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
3303
+ resolver = field.options.resolve;
3304
+ }
3305
+ if (!resolver) {
3306
+ return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
3307
+ }
3308
+ try {
3309
+ const db = c.get("db") || config.db;
3310
+ const queryParams = c.req.query();
3311
+ const reqContext = {
3312
+ query: queryParams,
3313
+ headers: c.req.header(),
3314
+ raw: c.req.raw
3315
+ };
3316
+ const result = await resolver({
3317
+ db,
3318
+ user,
3319
+ req: reqContext
3320
+ });
3321
+ return c.json(result);
3322
+ } catch (err) {
3323
+ console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
3324
+ return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
3325
+ }
3326
+ });
2913
3327
  app.get("/api/openapi.json", (c) => {
2914
3328
  return c.json(generateOpenApi(config));
2915
3329
  });
@@ -3097,9 +3511,12 @@ function defineConfig(config) {
3097
3511
  defineCollection,
3098
3512
  defineConfig,
3099
3513
  defineGlobal,
3514
+ executeFieldAfterRead,
3515
+ executeFieldBeforeChange,
3100
3516
  generateAIPrompt,
3101
3517
  generateFreshSetupPrompt,
3102
3518
  normalizeConfig,
3103
3519
  parseMongoWhere,
3104
- parseSqlWhere
3520
+ parseSqlWhere,
3521
+ runCollectionHooks
3105
3522
  });