@beechcms/core 0.7.0 → 0.8.1

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 (370) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/antivirus/antivirus.interface.d.ts +12 -0
  3. package/dist/antivirus/antivirus.interface.d.ts.map +1 -0
  4. package/dist/antivirus/antivirus.interface.js +3 -0
  5. package/dist/antivirus/noop-antivirus.provider.d.ts +6 -0
  6. package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -0
  7. package/dist/antivirus/noop-antivirus.provider.js +8 -0
  8. package/dist/antivirus/virustotal-antivirus.provider.d.ts +8 -0
  9. package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -0
  10. package/dist/antivirus/virustotal-antivirus.provider.js +48 -0
  11. package/dist/automations-grammar-words.d.ts +2 -0
  12. package/dist/automations-grammar-words.d.ts.map +1 -0
  13. package/dist/automations-grammar-words.js +8 -0
  14. package/dist/automations.repository.interface.d.ts +19 -0
  15. package/dist/automations.repository.interface.d.ts.map +1 -0
  16. package/dist/automations.repository.interface.js +3 -0
  17. package/dist/automations.runner.interface.d.ts +10 -0
  18. package/dist/automations.runner.interface.d.ts.map +1 -0
  19. package/dist/automations.runner.interface.js +3 -0
  20. package/dist/automations.runner.stub.d.ts +5 -0
  21. package/dist/automations.runner.stub.d.ts.map +1 -0
  22. package/dist/automations.runner.stub.js +7 -0
  23. package/dist/automations.types.d.ts +75 -0
  24. package/dist/automations.types.d.ts.map +1 -0
  25. package/dist/automations.types.js +3 -0
  26. package/dist/clock.d.ts +25 -0
  27. package/dist/clock.d.ts.map +1 -0
  28. package/dist/clock.js +11 -0
  29. package/dist/common/canonical-json.d.ts +30 -0
  30. package/dist/common/canonical-json.d.ts.map +1 -0
  31. package/dist/common/canonical-json.js +89 -0
  32. package/dist/common/hooks.d.ts +3 -0
  33. package/dist/common/hooks.d.ts.map +1 -1
  34. package/dist/content/content.repository.d.ts +2 -2
  35. package/dist/content/content.repository.d.ts.map +1 -1
  36. package/dist/content/deletion-ledger.d.ts +39 -0
  37. package/dist/content/deletion-ledger.d.ts.map +1 -0
  38. package/dist/content/deletion-ledger.js +3 -0
  39. package/dist/content/richtext/richtext-render.d.ts.map +1 -1
  40. package/dist/content/richtext/richtext-render.js +14 -1
  41. package/dist/content/seed.repository.d.ts +22 -0
  42. package/dist/content/seed.repository.d.ts.map +1 -1
  43. package/dist/content-scan.repository.d.ts +9 -0
  44. package/dist/content-scan.repository.d.ts.map +1 -0
  45. package/dist/content-scan.repository.js +3 -0
  46. package/dist/content.repository.d.ts +247 -0
  47. package/dist/content.repository.d.ts.map +1 -0
  48. package/dist/content.repository.js +59 -0
  49. package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
  50. package/dist/dashboard-layout/seed-layout.js +13 -6
  51. package/dist/dashboard-layout.d.ts +154 -0
  52. package/dist/dashboard-layout.d.ts.map +1 -0
  53. package/dist/dashboard-layout.js +186 -0
  54. package/dist/dashboard-layout.repository.d.ts +18 -0
  55. package/dist/dashboard-layout.repository.d.ts.map +1 -0
  56. package/dist/dashboard-layout.repository.js +3 -0
  57. package/dist/dashboard-permissions.d.ts +3 -0
  58. package/dist/dashboard-permissions.d.ts.map +1 -0
  59. package/dist/dashboard-permissions.js +11 -0
  60. package/dist/dashboard-scopes.d.ts +12 -0
  61. package/dist/dashboard-scopes.d.ts.map +1 -0
  62. package/dist/dashboard-scopes.js +30 -0
  63. package/dist/define-seed.d.ts +3 -0
  64. package/dist/define-seed.d.ts.map +1 -0
  65. package/dist/define-seed.js +5 -0
  66. package/dist/demo-data.repository.d.ts +5 -0
  67. package/dist/demo-data.repository.d.ts.map +1 -0
  68. package/dist/demo-data.repository.js +3 -0
  69. package/dist/email.provider.d.ts +32 -0
  70. package/dist/email.provider.d.ts.map +1 -0
  71. package/dist/email.provider.js +4 -0
  72. package/dist/engine/ddl/ddl.d.ts +216 -0
  73. package/dist/engine/ddl/ddl.d.ts.map +1 -0
  74. package/dist/engine/ddl/ddl.js +574 -0
  75. package/dist/engine/ddl/schema-mutator.d.ts +26 -0
  76. package/dist/engine/ddl/schema-mutator.d.ts.map +1 -0
  77. package/dist/engine/ddl/schema-mutator.js +3 -0
  78. package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
  79. package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
  80. package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
  81. package/dist/engine/ddl/seed-ddl.d.ts +43 -0
  82. package/dist/engine/ddl/seed-ddl.d.ts.map +1 -0
  83. package/dist/engine/ddl/seed-ddl.js +115 -0
  84. package/dist/engine/ddl.d.ts +15 -0
  85. package/dist/engine/ddl.d.ts.map +1 -1
  86. package/dist/engine/ddl.js +30 -1
  87. package/dist/engine/introspection/introspection.d.ts +101 -0
  88. package/dist/engine/introspection/introspection.d.ts.map +1 -0
  89. package/dist/engine/introspection/introspection.js +127 -0
  90. package/dist/engine/introspection/schema-fingerprint.d.ts +73 -0
  91. package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -0
  92. package/dist/engine/introspection/schema-fingerprint.js +97 -0
  93. package/dist/engine/introspection.d.ts +101 -0
  94. package/dist/engine/introspection.d.ts.map +1 -0
  95. package/dist/engine/introspection.js +127 -0
  96. package/dist/engine/localization/localization.d.ts +127 -0
  97. package/dist/engine/localization/localization.d.ts.map +1 -0
  98. package/dist/engine/localization/localization.js +212 -0
  99. package/dist/engine/localization.d.ts +127 -0
  100. package/dist/engine/localization.d.ts.map +1 -0
  101. package/dist/engine/localization.js +212 -0
  102. package/dist/engine/policies.d.ts.map +1 -1
  103. package/dist/engine/policies.js +5 -0
  104. package/dist/engine/privacy/policies.d.ts +58 -0
  105. package/dist/engine/privacy/policies.d.ts.map +1 -0
  106. package/dist/engine/privacy/policies.js +185 -0
  107. package/dist/engine/privacy/privacy.service.d.ts +65 -0
  108. package/dist/engine/privacy/privacy.service.d.ts.map +1 -0
  109. package/dist/engine/privacy/privacy.service.js +114 -0
  110. package/dist/engine/privacy.service.d.ts.map +1 -1
  111. package/dist/engine/query/query.d.ts +23 -0
  112. package/dist/engine/query/query.d.ts.map +1 -0
  113. package/dist/engine/query/query.js +301 -0
  114. package/dist/engine/query/relations.d.ts +28 -0
  115. package/dist/engine/query/relations.d.ts.map +1 -0
  116. package/dist/engine/query/relations.js +25 -0
  117. package/dist/engine/query/serialize.d.ts +22 -0
  118. package/dist/engine/query/serialize.d.ts.map +1 -0
  119. package/dist/engine/query/serialize.js +175 -0
  120. package/dist/engine/schema-fingerprint.d.ts +73 -0
  121. package/dist/engine/schema-fingerprint.d.ts.map +1 -0
  122. package/dist/engine/schema-fingerprint.js +97 -0
  123. package/dist/engine/seed-registry.d.ts.map +1 -1
  124. package/dist/engine/seed-registry.js +23 -4
  125. package/dist/engine/seed-types-generator.d.ts.map +1 -1
  126. package/dist/engine/seed-types-generator.js +8 -5
  127. package/dist/engine/seed-validation.d.ts +7 -0
  128. package/dist/engine/seed-validation.d.ts.map +1 -1
  129. package/dist/engine/seed-validation.js +59 -1
  130. package/dist/engine/seeds/define-seed.d.ts +3 -0
  131. package/dist/engine/seeds/define-seed.d.ts.map +1 -0
  132. package/dist/engine/seeds/define-seed.js +5 -0
  133. package/dist/engine/seeds/seed-registry.d.ts +71 -0
  134. package/dist/engine/seeds/seed-registry.d.ts.map +1 -0
  135. package/dist/engine/seeds/seed-registry.js +167 -0
  136. package/dist/engine/seeds/seed-types-generator.d.ts +25 -0
  137. package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -0
  138. package/dist/engine/seeds/seed-types-generator.js +117 -0
  139. package/dist/engine/seeds/seed-validation.d.ts +30 -0
  140. package/dist/engine/seeds/seed-validation.d.ts.map +1 -0
  141. package/dist/engine/seeds/seed-validation.js +364 -0
  142. package/dist/engine/seeds/seeds.d.ts +20 -0
  143. package/dist/engine/seeds/seeds.d.ts.map +1 -0
  144. package/dist/engine/seeds/seeds.js +21 -0
  145. package/dist/engine/types.d.ts +7 -0
  146. package/dist/engine/types.d.ts.map +1 -1
  147. package/dist/engine/validation/emptiness.d.ts +12 -0
  148. package/dist/engine/validation/emptiness.d.ts.map +1 -0
  149. package/dist/engine/validation/emptiness.js +33 -0
  150. package/dist/engine/validation/index.d.ts +1 -1
  151. package/dist/engine/validation/index.d.ts.map +1 -1
  152. package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
  153. package/dist/engine/validation/richtext-sanitizer.js +2 -1
  154. package/dist/engine.d.ts +144 -0
  155. package/dist/engine.d.ts.map +1 -0
  156. package/dist/engine.js +741 -0
  157. package/dist/file-types.d.ts +13 -0
  158. package/dist/file-types.d.ts.map +1 -0
  159. package/dist/file-types.js +85 -0
  160. package/dist/hooks.d.ts +29 -0
  161. package/dist/hooks.d.ts.map +1 -0
  162. package/dist/hooks.js +3 -0
  163. package/dist/id-generator.d.ts +26 -0
  164. package/dist/id-generator.d.ts.map +1 -0
  165. package/dist/id-generator.js +12 -0
  166. package/dist/idempotency.repository.d.ts +13 -0
  167. package/dist/idempotency.repository.d.ts.map +1 -0
  168. package/dist/idempotency.repository.js +3 -0
  169. package/dist/index.d.ts +16 -1
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +16 -1
  172. package/dist/kanban-position.repository.d.ts +19 -0
  173. package/dist/kanban-position.repository.d.ts.map +1 -0
  174. package/dist/kanban-position.repository.js +3 -0
  175. package/dist/kanban.d.ts +75 -0
  176. package/dist/kanban.d.ts.map +1 -0
  177. package/dist/kanban.js +82 -0
  178. package/dist/layout-permissions.d.ts +3 -0
  179. package/dist/layout-permissions.d.ts.map +1 -0
  180. package/dist/layout-permissions.js +11 -0
  181. package/dist/media/file-types.d.ts +103 -9
  182. package/dist/media/file-types.d.ts.map +1 -1
  183. package/dist/media/file-types.js +336 -52
  184. package/dist/media/image-transformer.d.ts +34 -0
  185. package/dist/media/image-transformer.d.ts.map +1 -0
  186. package/dist/media/image-transformer.js +3 -0
  187. package/dist/media/magic-bytes.d.ts +21 -0
  188. package/dist/media/magic-bytes.d.ts.map +1 -0
  189. package/dist/media/magic-bytes.js +66 -0
  190. package/dist/media/media-transform.d.ts +88 -0
  191. package/dist/media/media-transform.d.ts.map +1 -0
  192. package/dist/media/media-transform.js +193 -0
  193. package/dist/media.repository.d.ts +61 -0
  194. package/dist/media.repository.d.ts.map +1 -0
  195. package/dist/media.repository.js +3 -0
  196. package/dist/notifications/notification-service.d.ts +1 -1
  197. package/dist/oauth/authorization-code.repository.d.ts +33 -0
  198. package/dist/oauth/authorization-code.repository.d.ts.map +1 -0
  199. package/dist/oauth/authorization-code.repository.js +3 -0
  200. package/dist/oauth/client.repository.d.ts +17 -0
  201. package/dist/oauth/client.repository.d.ts.map +1 -0
  202. package/dist/oauth/client.repository.js +3 -0
  203. package/dist/oauth/consent.repository.d.ts +25 -0
  204. package/dist/oauth/consent.repository.d.ts.map +1 -0
  205. package/dist/oauth/consent.repository.js +3 -0
  206. package/dist/oauth/pkce.d.ts +21 -0
  207. package/dist/oauth/pkce.d.ts.map +1 -0
  208. package/dist/oauth/pkce.js +47 -0
  209. package/dist/oauth/role-guard.d.ts +32 -0
  210. package/dist/oauth/role-guard.d.ts.map +1 -0
  211. package/dist/oauth/role-guard.js +14 -0
  212. package/dist/oauth/scopes.d.ts +27 -0
  213. package/dist/oauth/scopes.d.ts.map +1 -0
  214. package/dist/oauth/scopes.js +41 -0
  215. package/dist/oauth/token.repository.d.ts +46 -0
  216. package/dist/oauth/token.repository.d.ts.map +1 -0
  217. package/dist/oauth/token.repository.js +3 -0
  218. package/dist/observability/activity-logger.d.ts +1 -1
  219. package/dist/observability/activity-logger.d.ts.map +1 -1
  220. package/dist/policies.d.ts +10 -0
  221. package/dist/policies.d.ts.map +1 -0
  222. package/dist/policies.js +33 -0
  223. package/dist/queue/queue.stub.d.ts.map +1 -1
  224. package/dist/queue/scheduler.stub.d.ts.map +1 -1
  225. package/dist/queue.interface.d.ts +39 -0
  226. package/dist/queue.interface.d.ts.map +1 -0
  227. package/dist/queue.interface.js +3 -0
  228. package/dist/queue.stub.d.ts +6 -0
  229. package/dist/queue.stub.d.ts.map +1 -0
  230. package/dist/queue.stub.js +8 -0
  231. package/dist/rate-limit/rate-limiter.d.ts +13 -5
  232. package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
  233. package/dist/rate-limit/token-bucket-rate-limiter.d.ts +20 -0
  234. package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -0
  235. package/dist/rate-limit/token-bucket-rate-limiter.js +63 -0
  236. package/dist/rbac/evaluate.d.ts +57 -0
  237. package/dist/rbac/evaluate.d.ts.map +1 -0
  238. package/dist/rbac/evaluate.js +113 -0
  239. package/dist/rbac/invitation.repository.d.ts +66 -0
  240. package/dist/rbac/invitation.repository.d.ts.map +1 -0
  241. package/dist/rbac/invitation.repository.js +3 -0
  242. package/dist/rbac/permissions.d.ts +39 -0
  243. package/dist/rbac/permissions.d.ts.map +1 -0
  244. package/dist/rbac/permissions.js +43 -0
  245. package/dist/rbac/types.d.ts +118 -0
  246. package/dist/rbac/types.d.ts.map +1 -0
  247. package/dist/rbac/types.js +3 -0
  248. package/dist/relations.d.ts +28 -0
  249. package/dist/relations.d.ts.map +1 -0
  250. package/dist/relations.js +25 -0
  251. package/dist/richtext-render.d.ts +11 -0
  252. package/dist/richtext-render.d.ts.map +1 -0
  253. package/dist/richtext-render.js +87 -0
  254. package/dist/richtext.d.ts +11 -0
  255. package/dist/richtext.d.ts.map +1 -0
  256. package/dist/richtext.js +13 -0
  257. package/dist/scheduler.interface.d.ts +4 -0
  258. package/dist/scheduler.interface.d.ts.map +1 -0
  259. package/dist/scheduler.interface.js +3 -0
  260. package/dist/scheduler.stub.d.ts +5 -0
  261. package/dist/scheduler.stub.d.ts.map +1 -0
  262. package/dist/scheduler.stub.js +7 -0
  263. package/dist/schema/canonical.d.ts +15 -0
  264. package/dist/schema/canonical.d.ts.map +1 -0
  265. package/dist/schema/canonical.js +37 -0
  266. package/dist/schema/define.d.ts +42 -0
  267. package/dist/schema/define.d.ts.map +1 -0
  268. package/dist/schema/define.js +56 -0
  269. package/dist/schema/emit.d.ts +14 -0
  270. package/dist/schema/emit.d.ts.map +1 -0
  271. package/dist/schema/emit.js +43 -0
  272. package/dist/schema/index.d.ts +12 -0
  273. package/dist/schema/index.d.ts.map +1 -0
  274. package/dist/schema/index.js +13 -0
  275. package/dist/schema/manifest-seeds.d.ts +12 -0
  276. package/dist/schema/manifest-seeds.d.ts.map +1 -0
  277. package/dist/schema/manifest-seeds.js +48 -0
  278. package/dist/schema/manifest-validation.d.ts +13 -0
  279. package/dist/schema/manifest-validation.d.ts.map +1 -0
  280. package/dist/schema/manifest-validation.js +41 -0
  281. package/dist/schema/manifest.types.d.ts +54 -0
  282. package/dist/schema/manifest.types.d.ts.map +1 -0
  283. package/dist/schema/manifest.types.js +7 -0
  284. package/dist/schema-mutator.d.ts +26 -0
  285. package/dist/schema-mutator.d.ts.map +1 -0
  286. package/dist/schema-mutator.js +3 -0
  287. package/dist/search/vector-extractor.d.ts +11 -0
  288. package/dist/search/vector-extractor.d.ts.map +1 -0
  289. package/dist/search/vector-extractor.js +25 -0
  290. package/dist/search/vector.repository.d.ts +13 -0
  291. package/dist/search/vector.repository.d.ts.map +1 -0
  292. package/dist/search/vector.repository.js +3 -0
  293. package/dist/security/time-trap-token.repository.d.ts +9 -0
  294. package/dist/security/time-trap-token.repository.d.ts.map +1 -0
  295. package/dist/security/time-trap-token.repository.js +3 -0
  296. package/dist/security/time-trap.d.ts +7 -0
  297. package/dist/security/time-trap.d.ts.map +1 -0
  298. package/dist/security/time-trap.js +49 -0
  299. package/dist/seed-ddl-destructive.d.ts +40 -0
  300. package/dist/seed-ddl-destructive.d.ts.map +1 -0
  301. package/dist/seed-ddl-destructive.js +149 -0
  302. package/dist/seed-ddl.d.ts +40 -0
  303. package/dist/seed-ddl.d.ts.map +1 -0
  304. package/dist/seed-ddl.js +100 -0
  305. package/dist/seed-layout.d.ts +145 -0
  306. package/dist/seed-layout.d.ts.map +1 -0
  307. package/dist/seed-layout.js +192 -0
  308. package/dist/seed-layout.repository.d.ts +22 -0
  309. package/dist/seed-layout.repository.d.ts.map +1 -0
  310. package/dist/seed-layout.repository.js +3 -0
  311. package/dist/seed-registry.d.ts +71 -0
  312. package/dist/seed-registry.d.ts.map +1 -0
  313. package/dist/seed-registry.js +143 -0
  314. package/dist/seed-types-generator.d.ts +1 -1
  315. package/dist/seed-types-generator.d.ts.map +1 -1
  316. package/dist/seed-validation.d.ts +23 -0
  317. package/dist/seed-validation.d.ts.map +1 -0
  318. package/dist/seed-validation.js +224 -0
  319. package/dist/seed.repository.d.ts +37 -0
  320. package/dist/seed.repository.d.ts.map +1 -0
  321. package/dist/seed.repository.js +3 -0
  322. package/dist/seeds.d.ts +20 -0
  323. package/dist/seeds.d.ts.map +1 -0
  324. package/dist/seeds.js +21 -0
  325. package/dist/site-settings.repository.d.ts +16 -0
  326. package/dist/site-settings.repository.d.ts.map +1 -0
  327. package/dist/site-settings.repository.js +3 -0
  328. package/dist/slug-utils.d.ts +18 -0
  329. package/dist/slug-utils.d.ts.map +1 -0
  330. package/dist/slug-utils.js +32 -0
  331. package/dist/storage.d.ts +46 -0
  332. package/dist/storage.d.ts.map +1 -0
  333. package/dist/storage.js +3 -0
  334. package/dist/transfer/csv.d.ts +34 -0
  335. package/dist/transfer/csv.d.ts.map +1 -0
  336. package/dist/transfer/csv.js +135 -0
  337. package/dist/transfer/flat-seed.d.ts +13 -0
  338. package/dist/transfer/flat-seed.d.ts.map +1 -0
  339. package/dist/transfer/flat-seed.js +45 -0
  340. package/dist/transfer/index.d.ts +7 -0
  341. package/dist/transfer/index.d.ts.map +1 -0
  342. package/dist/transfer/index.js +8 -0
  343. package/dist/transfer/ndjson.d.ts +25 -0
  344. package/dist/transfer/ndjson.d.ts.map +1 -0
  345. package/dist/transfer/ndjson.js +62 -0
  346. package/dist/transfer/row-mapping.d.ts +36 -0
  347. package/dist/transfer/row-mapping.d.ts.map +1 -0
  348. package/dist/transfer/row-mapping.js +90 -0
  349. package/dist/transfer/transfer.constants.d.ts +20 -0
  350. package/dist/transfer/transfer.constants.d.ts.map +1 -0
  351. package/dist/transfer/transfer.constants.js +21 -0
  352. package/dist/transfer/transfer.types.d.ts +46 -0
  353. package/dist/transfer/transfer.types.d.ts.map +1 -0
  354. package/dist/transfer/transfer.types.js +6 -0
  355. package/dist/types.d.ts +243 -0
  356. package/dist/types.d.ts.map +1 -0
  357. package/dist/types.js +3 -0
  358. package/dist/validation.d.ts +50 -0
  359. package/dist/validation.d.ts.map +1 -0
  360. package/dist/validation.js +660 -0
  361. package/dist/webhook-crypto.d.ts +2 -0
  362. package/dist/webhook-crypto.d.ts.map +1 -1
  363. package/dist/webhook-crypto.js +2 -2
  364. package/dist/webhook-validation.d.ts +3 -0
  365. package/dist/webhook-validation.d.ts.map +1 -0
  366. package/dist/webhook-validation.js +6 -0
  367. package/dist/webhooks/webhook-crypto.d.ts +2 -0
  368. package/dist/webhooks/webhook-crypto.d.ts.map +1 -1
  369. package/dist/webhooks/webhook-crypto.js +2 -2
  370. package/package.json +23 -15
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /**
4
+ * @module engine/schema-fingerprint
5
+ * A deterministic name for "the shape this API answers in".
6
+ *
7
+ * Computed from live seed definitions (`introspectSeedDefinitions`), embedded in generated client
8
+ * types at build time (sprint 3), returned on every public response as `X-Schema-Revision`
9
+ * (sprint 4), and compared by the client at request time (sprint 5). Both ends MUST compute it
10
+ * with this function — two implementations would be free to disagree, and a drift detector whose
11
+ * ends disagree is worse than none.
12
+ *
13
+ * What is hashed is a CONTRACT PROJECTION, not the whole seed definition: labels, hints, dashboard
14
+ * icons and layout cannot change a single response byte, and hashing them would fire a hard
15
+ * `BeechProblem` in someone else's production client over a typo fix.
16
+ */
17
+ import { canonicalStringify } from '../common/canonical-json.js';
18
+ import { resolvePolicies } from './policies.js';
19
+ /**
20
+ * Bumped ONLY when the projection below or the canonical byte format changes — i.e. when
21
+ * fingerprints computed by two builds are no longer comparable. It is carried in the fingerprint
22
+ * string itself so a version skew reads as a mismatch instead of as an accidental match.
23
+ */
24
+ export const SCHEMA_FINGERPRINT_VERSION = 1;
25
+ function projectBranch(branch) {
26
+ const policies = resolvePolicies(branch);
27
+ return {
28
+ alias: branch.alias,
29
+ type: branch.type,
30
+ ...(branch.format !== undefined ? { format: branch.format } : {}),
31
+ ...(branch.multiple !== undefined ? { multiple: branch.multiple } : {}),
32
+ // Option order is the contract: it is the order a select renders and a union type lists.
33
+ ...(branch.options !== undefined ? { options: [...branch.options] } : {}),
34
+ requiredOnCreate: branch.requiredOnCreate ?? false,
35
+ requiredOnUpdate: branch.requiredOnUpdate ?? false,
36
+ ...(branch.targetSeed !== undefined ? { targetSeed: branch.targetSeed } : {}),
37
+ visibility: policies.visibility,
38
+ publicRead: policies.public,
39
+ publicEdit: policies.publicEdit,
40
+ ...(branch.minItems !== undefined ? { minItems: branch.minItems } : {}),
41
+ ...(branch.maxItems !== undefined ? { maxItems: branch.maxItems } : {}),
42
+ // Spread only when true so every schema without localized branches keeps its fingerprint.
43
+ ...(branch.localized === true ? { localized: true } : {}),
44
+ ...(branch.fields ? { fields: branch.fields.map(projectBranch) } : {}),
45
+ };
46
+ }
47
+ /**
48
+ * Reduces seeds to what a response shape depends on.
49
+ *
50
+ * Deliberately EXCLUDED, and each for a reason a future reader must not "fix":
51
+ * - `id` (`br_XX`) — a stable internal handle; it never appears in a payload, and minting order
52
+ * differs between an environment built by migration and one grown through the dashboard.
53
+ * - `label`, `labelPlural`, `hint`, `dashboard`, `layout` — presentation, never response shape.
54
+ * - `retentionDays`, `onDelete`, `policies.search|filter|sort`, `numberOptions`, `fileOptions`,
55
+ * `policies.classification|privacy` — behaviour and storage concerns that leave the response
56
+ * shape identical. (`privacy` DOES change a stored value's encoding, but the API type stays
57
+ * `string`; drift there is a data concern, not a type concern.)
58
+ * - anything physical (column order, index names) — see the VETO Audit.
59
+ *
60
+ * Seeds are sorted by slug; BRANCH ORDER IS PRESERVED, because it is the declared field order the
61
+ * generated types and the dashboard form both follow.
62
+ */
63
+ export function projectSchemaContract(seeds) {
64
+ return {
65
+ fingerprintVersion: SCHEMA_FINGERPRINT_VERSION,
66
+ seeds: [...seeds]
67
+ .sort((a, b) => a.slug.localeCompare(b.slug))
68
+ .map(seed => ({
69
+ slug: seed.slug,
70
+ displayNameAlias: seed.displayNameAlias,
71
+ allowDrafts: seed.allowDrafts ?? false,
72
+ allowPublicRead: seed.allowPublicRead ?? false,
73
+ allowPublicPost: seed.allowPublicPost ?? false,
74
+ allowPublicEdit: seed.allowPublicEdit ?? false,
75
+ branches: seed.branches.map(projectBranch),
76
+ })),
77
+ };
78
+ }
79
+ function toHex(buffer) {
80
+ return Array.from(new Uint8Array(buffer), byte => byte.toString(16).padStart(2, '0')).join('');
81
+ }
82
+ /**
83
+ * `v{VERSION}:{32 lowercase hex}` — SHA-256 over the canonical JSON of the contract projection,
84
+ * truncated to 128 bits.
85
+ *
86
+ * Truncation is deliberate: this value travels on EVERY public response as `X-Schema-Revision`,
87
+ * and 128 bits of a SHA-256 digest carries no realistic collision risk for a value space of a few
88
+ * hundred schema revisions. It is a revision marker, never a security token.
89
+ *
90
+ * Web Crypto only — the same API the Worker and Node ≥ 18 both expose, as `webhook-crypto.ts`
91
+ * already relies on. `@beechcms/core` gains no dependency.
92
+ */
93
+ export async function computeSchemaFingerprint(seeds) {
94
+ const bytes = new TextEncoder().encode(canonicalStringify(projectSchemaContract(seeds)));
95
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
96
+ return `v${SCHEMA_FINGERPRINT_VERSION}:${toHex(digest).slice(0, 32)}`;
97
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../../src/engine/seed-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAI9C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAID,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,KAAK,EAAE,IAAI,EAAE;IAkCzB,GAAG,IAAI,IAAI,EAAE;IAIb,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI9B,kBAAkB,IAAI,IAAI,EAAE;IAI5B,cAAc,IAAI,IAAI,EAAE;IAIxB,YAAY,IAAI,IAAI,EAAE;CAGvB;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG;AAEzD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,MAAM,CAQjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAsE1E"}
1
+ {"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../../src/engine/seed-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAI9C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAID,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IAErC,YAAY,KAAK,EAAE,IAAI,EAAE,EAgCxB;IAED,GAAG,IAAI,IAAI,EAAE,CAEZ;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE7B;IAED,kBAAkB,IAAI,IAAI,EAAE,CAE3B;IAED,cAAc,IAAI,IAAI,EAAE,CAEvB;IAED,YAAY,IAAI,IAAI,EAAE,CAErB;CACF;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG;AAEzD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,MAAM,CAQjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CA0F1E"}
@@ -113,6 +113,8 @@ export function sortSeedsByDependencies(seeds) {
113
113
  .filter(b => b.type === 'relation' && b.targetSeed)
114
114
  .map(b => b.targetSeed));
115
115
  for (const target of targets) {
116
+ if (target === seed.slug)
117
+ continue; // self-reference needs no ordering
116
118
  // seed depends on target → increment seed's in-degree
117
119
  inDegree.set(seed.slug, (inDegree.get(seed.slug) ?? 0) + 1);
118
120
  dependents.get(target).push(seed.slug);
@@ -137,11 +139,28 @@ export function sortSeedsByDependencies(seeds) {
137
139
  }
138
140
  }
139
141
  if (result.length !== seeds.length) {
140
- // Cycle exists — collect participating slugs (those with in-degree > 0 after BFS)
141
- const cycle = [...inDegree.entries()]
142
+ // Isolate true cycle members
143
+ const stuck = new Set([...inDegree.entries()]
142
144
  .filter(([, deg]) => deg > 0)
143
- .map(([slug]) => slug)
144
- .sort();
145
+ .map(([slug]) => slug));
146
+ const cycleNodes = [...stuck].filter(slug => {
147
+ const visited = new Set();
148
+ const queue = [...(dependents.get(slug) ?? [])].filter(s => stuck.has(s));
149
+ while (queue.length > 0) {
150
+ const curr = queue.shift();
151
+ if (curr === slug)
152
+ return true;
153
+ if (!visited.has(curr)) {
154
+ visited.add(curr);
155
+ for (const next of dependents.get(curr) ?? []) {
156
+ if (stuck.has(next))
157
+ queue.push(next);
158
+ }
159
+ }
160
+ }
161
+ return false;
162
+ });
163
+ const cycle = (cycleNodes.length > 0 ? cycleNodes : [...stuck]).sort();
145
164
  throw new Error(`Cyclic dependency detected among seeds: ${cycle.join(', ')}`);
146
165
  }
147
166
  return result;
@@ -1 +1 @@
1
- {"version":3,"file":"seed-types-generator.d.ts","sourceRoot":"","sources":["../../src/engine/seed-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAe9C,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhD;AAMD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoCtD;AAWD,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAYnD;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CASvD"}
1
+ {"version":3,"file":"seed-types-generator.d.ts","sourceRoot":"","sources":["../../src/engine/seed-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAe9C,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhD;AAMD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoCtD;AAWD,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAYnD;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAcvD"}
@@ -78,9 +78,12 @@ export function interfaceForSeed(seed) {
78
78
  export function generateSeedTypes(seeds) {
79
79
  const sorted = [...seeds].sort((a, b) => a.slug.localeCompare(b.slug));
80
80
  const interfaces = sorted.map(interfaceForSeed).join('\n');
81
- const registry = `export interface SeedRegistryTypes {\n` +
82
- sorted.map(s => ` ${propName({ alias: s.slug })}: ${pascalCase(s.slug)}`).join('\n') +
83
- (sorted.length ? '\n' : '') +
84
- `}\n`;
85
- return `${HEADER}\n${interfaces}\n${registry}`;
81
+ const registryProps = sorted
82
+ .map(s => ` ${propName({ alias: s.slug })}: ${pascalCase(s.slug)}`)
83
+ .join('\n');
84
+ const databaseRegistry = `export interface BeechDatabase {\n` +
85
+ (registryProps ? registryProps + '\n' : '') +
86
+ `}\n\n` +
87
+ `export type SeedRegistryTypes = BeechDatabase\n`;
88
+ return `${HEADER}\n${interfaces}\n${databaseRegistry}`;
86
89
  }
@@ -7,6 +7,13 @@ import type { Seed } from './types.js';
7
7
  * exact same guard — do not inline a divergent copy.
8
8
  */
9
9
  export declare const BRANCH_ALIAS_RE: RegExp;
10
+ /**
11
+ * Allowed charset for a seed slug identifier.
12
+ * Must consist solely of lowercase ASCII letters, digits, and underscores.
13
+ * Exported so route handlers and validation functions reuse the exact same guard.
14
+ */
15
+ export declare const SEED_SLUG_RE: RegExp;
16
+ export declare const SLUG_RE: RegExp;
10
17
  export interface SeedValidationIssue {
11
18
  slug: string;
12
19
  messages: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"seed-validation.d.ts","sourceRoot":"","sources":["../../src/engine/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAOtC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAAyB,CAAA;AAErD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,mBAAmB,EAAE,CA8P5E;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAErD"}
1
+ {"version":3,"file":"seed-validation.d.ts","sourceRoot":"","sources":["../../src/engine/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAQtC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAAyB,CAAA;AAErD;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAiB,CAAA;AAC1C,eAAO,MAAM,OAAO,QAAe,CAAA;AAGnC,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,mBAAmB,EAAE,CAkT5E;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAErD"}
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (c) 2024–2026 Flavio De Musso
3
3
  import { AUTOMATION_RESERVED_WORDS } from '../automations/automations-grammar-words.js';
4
+ import { SYSTEM_COLUMNS } from './ddl.js';
4
5
  import { sortSeedsByDependencies } from './seed-registry.js';
5
6
  import { SQL_RESERVED_WORDS } from './sql-reserved-words.js';
6
7
  const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
@@ -12,6 +13,13 @@ const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
12
13
  * exact same guard — do not inline a divergent copy.
13
14
  */
14
15
  export const BRANCH_ALIAS_RE = /^[a-z][a-zA-Z0-9_]*$/;
16
+ /**
17
+ * Allowed charset for a seed slug identifier.
18
+ * Must consist solely of lowercase ASCII letters, digits, and underscores.
19
+ * Exported so route handlers and validation functions reuse the exact same guard.
20
+ */
21
+ export const SEED_SLUG_RE = /^[a-z0-9_]+$/;
22
+ export const SLUG_RE = SEED_SLUG_RE;
15
23
  /**
16
24
  * Pure, console-free, throw-free validation of a seed set.
17
25
  * The single seed being created/edited should be validated in the context of
@@ -113,7 +121,11 @@ export function validateSeedDefinitions(seeds) {
113
121
  for (const seed of seeds) {
114
122
  const messages = [];
115
123
  for (const branch of seed.branches) {
116
- if (AUTOMATION_RESERVED_WORDS.has(branch.alias)) {
124
+ if (SYSTEM_COLUMNS.has(branch.alias)) {
125
+ messages.push(`branch '${branch.alias}' collides with reserved system column. ` +
126
+ `System columns (id, slug, status, created_at, updated_at) cannot be used as branch aliases.`);
127
+ }
128
+ else if (AUTOMATION_RESERVED_WORDS.has(branch.alias)) {
117
129
  messages.push(`branch '${branch.alias}' uses reserved alias. ` +
118
130
  `This word is used by the automation template grammar.`);
119
131
  }
@@ -242,6 +254,16 @@ export function validateSeedDefinitions(seeds) {
242
254
  fatal: true,
243
255
  });
244
256
  }
257
+ else if (SYSTEM_COLUMNS.has(seed.displayNameAlias)) {
258
+ result.push({
259
+ slug: seed.slug,
260
+ messages: [
261
+ `displayNameAlias '${seed.displayNameAlias}' collides with reserved system column. ` +
262
+ `System columns (id, slug, status, created_at, updated_at) cannot be used as displayNameAlias.`,
263
+ ],
264
+ fatal: true,
265
+ });
266
+ }
245
267
  else if (SQL_RESERVED_WORDS.has(seed.displayNameAlias.toLowerCase())) {
246
268
  result.push({
247
269
  slug: seed.slug,
@@ -253,6 +275,42 @@ export function validateSeedDefinitions(seeds) {
253
275
  });
254
276
  }
255
277
  }
278
+ // ── Fatal 13: retentionDays validation ─────────────────────────────────────
279
+ for (const seed of seeds) {
280
+ if (seed.retentionDays !== undefined && (!Number.isInteger(seed.retentionDays) || seed.retentionDays <= 0)) {
281
+ result.push({
282
+ slug: seed.slug,
283
+ messages: [
284
+ `retentionDays must be a positive integer >= 1 (got ${seed.retentionDays}).`,
285
+ ],
286
+ fatal: true,
287
+ });
288
+ }
289
+ }
290
+ // ── Fatal 14: publicEdit validation ─────────────────────────────────────────
291
+ for (const seed of seeds) {
292
+ const messages = [];
293
+ for (const branch of seed.branches) {
294
+ if (branch.policies?.publicEdit !== undefined && typeof branch.policies.publicEdit !== 'boolean') {
295
+ messages.push(`branch '${branch.alias}': policies.publicEdit must be a boolean`);
296
+ }
297
+ }
298
+ if (messages.length > 0)
299
+ result.push({ slug: seed.slug, messages, fatal: true });
300
+ }
301
+ // ── Fatal 15: seed slug format validation ─────────────────────────────────
302
+ for (const seed of seeds) {
303
+ if (typeof seed.slug !== 'string' || !SEED_SLUG_RE.test(seed.slug)) {
304
+ result.push({
305
+ slug: seed.slug,
306
+ messages: [
307
+ `slug '${seed.slug}' is invalid. Expected format ${SEED_SLUG_RE.source} ` +
308
+ `(lowercase letters, numbers, and underscores only).`,
309
+ ],
310
+ fatal: true,
311
+ });
312
+ }
313
+ }
256
314
  return result;
257
315
  }
258
316
  /** Convenience: true iff no fatal issues. */
@@ -0,0 +1,3 @@
1
+ import type { Seed } from '../types.js';
2
+ export declare function defineSeed(seed: Seed): Seed;
3
+ //# sourceMappingURL=define-seed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-seed.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/define-seed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAE3C"}
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export function defineSeed(seed) {
4
+ return seed;
5
+ }
@@ -0,0 +1,71 @@
1
+ import type { Seed, Branch } from '../types.js';
2
+ export interface ISeedRegistry {
3
+ /**
4
+ * Returns all seeds as a flat array, preserving insertion order.
5
+ * Decouples callers from the internal storage shape so the registry
6
+ * implementation can change without touching every route handler.
7
+ */
8
+ all(): Seed[];
9
+ /**
10
+ * Returns the seed with the given slug, or null if not found.
11
+ * Provides a single lookup point that can be overridden in tests
12
+ * without rebuilding the full registry object.
13
+ */
14
+ get(slug: string): Seed | null;
15
+ /**
16
+ * Returns seeds that are visible in the dashboard sidebar.
17
+ * A seed is visible when dashboard.hidden is not explicitly true.
18
+ * Eliminates the seeds.filter(s => !s.dashboard?.hidden) pattern
19
+ * that would otherwise be duplicated across route handlers.
20
+ */
21
+ visibleInDashboard(): Seed[];
22
+ /**
23
+ * Returns seeds that have allowPublicRead enabled.
24
+ * Eliminates the seeds.filter(s => s.allowPublicRead) pattern.
25
+ */
26
+ publicReadable(): Seed[];
27
+ /**
28
+ * Returns seeds that have the draft workflow enabled.
29
+ * Eliminates the seeds.filter(s => s.allowDrafts) pattern.
30
+ */
31
+ draftEnabled(): Seed[];
32
+ }
33
+ export declare class SeedRegistry implements ISeedRegistry {
34
+ private readonly seedMap;
35
+ private readonly orderedSeeds;
36
+ constructor(seeds: Seed[]);
37
+ all(): Seed[];
38
+ get(slug: string): Seed | null;
39
+ visibleInDashboard(): Seed[];
40
+ publicReadable(): Seed[];
41
+ draftEnabled(): Seed[];
42
+ }
43
+ /**
44
+ * Named subclass of SeedRegistry for use in test suites.
45
+ * Gives tests a semantic name without requiring any dependency on factory.ts.
46
+ */
47
+ export declare class InMemorySeedRegistry extends SeedRegistry {
48
+ }
49
+ /** Finds a branch by its stable id within a Seed. Returns null if not found. */
50
+ export declare function findBranchById(seed: Seed, id: string): Branch | null;
51
+ /**
52
+ * Returns the next free branch id for a seed in the form `br_NN` (zero-padded to 2,
53
+ * growing as needed). Scans existing ids, never reuses a number already present —
54
+ * even if a branch was removed, its id is not recycled (ids must be globally stable
55
+ * for the life of the seed so layouts/automations/FTS triggers never collide).
56
+ */
57
+ export declare function nextBranchId(seed: Pick<Seed, 'branches'>): string;
58
+ /**
59
+ * Returns the input seeds reordered so that every seed appears AFTER all the
60
+ * seeds it depends on (its `relation` branch targets). Pure function — does
61
+ * not read from any global registry.
62
+ *
63
+ * Uses Kahn's algorithm (BFS on in-degree) for cycle detection and topological
64
+ * ordering. Sprint 3 (CLI / migration runner) consumes this to create tables in
65
+ * the correct order without disabling SQLite FK constraints.
66
+ *
67
+ * @throws {Error} When a relation targets an unknown slug not in `seeds`.
68
+ * @throws {Error} When a cyclic dependency is detected, listing involved slugs.
69
+ */
70
+ export declare function sortSeedsByDependencies(seeds: ReadonlyArray<Seed>): Seed[];
71
+ //# sourceMappingURL=seed-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/seed-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAI/C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAID,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,KAAK,EAAE,IAAI,EAAE;IAkCzB,GAAG,IAAI,IAAI,EAAE;IAIb,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI9B,kBAAkB,IAAI,IAAI,EAAE;IAI5B,cAAc,IAAI,IAAI,EAAE;IAIxB,YAAY,IAAI,IAAI,EAAE;CAGvB;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG;AAEzD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,MAAM,CAQjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CA0F1E"}
@@ -0,0 +1,167 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { AUTOMATION_RESERVED_WORDS } from '../../automations/automations-grammar-words.js';
4
+ import { SQL_RESERVED_WORDS } from '../sql-reserved-words.js';
5
+ const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
6
+ export class SeedRegistry {
7
+ seedMap;
8
+ orderedSeeds;
9
+ constructor(seeds) {
10
+ for (const seed of seeds) {
11
+ const idsInSeed = new Set();
12
+ for (const branch of seed.branches) {
13
+ if (AUTOMATION_RESERVED_WORDS.has(branch.alias)) {
14
+ throw new Error(`Seed "${seed.slug}" uses reserved alias "${branch.alias}". `
15
+ + `Pick a different alias — this word is used by the automation template grammar.`);
16
+ }
17
+ if (SQL_RESERVED_WORDS.has(branch.alias.toLowerCase())) {
18
+ throw new Error(`Seed "${seed.slug}" uses SQL reserved keyword "${branch.alias}". `
19
+ + `Please pick a different alias to prevent database errors.`);
20
+ }
21
+ if (!branch.id || !BRANCH_ID_RE.test(branch.id)) {
22
+ throw new Error(`Seed "${seed.slug}" branch "${branch.alias}" has invalid id "${branch.id}". `
23
+ + `Expected format ^br_[A-Za-z0-9]+$.`);
24
+ }
25
+ if (idsInSeed.has(branch.id)) {
26
+ throw new Error(`Seed "${seed.slug}" has duplicate branch id "${branch.id}".`);
27
+ }
28
+ idsInSeed.add(branch.id);
29
+ }
30
+ }
31
+ this.orderedSeeds = seeds;
32
+ this.seedMap = new Map(seeds.map(seed => [seed.slug, seed]));
33
+ }
34
+ all() {
35
+ return this.orderedSeeds;
36
+ }
37
+ get(slug) {
38
+ return this.seedMap.get(slug) ?? null;
39
+ }
40
+ visibleInDashboard() {
41
+ return this.orderedSeeds.filter(seed => seed.dashboard?.hidden !== true);
42
+ }
43
+ publicReadable() {
44
+ return this.orderedSeeds.filter(seed => seed.allowPublicRead === true);
45
+ }
46
+ draftEnabled() {
47
+ return this.orderedSeeds.filter(seed => seed.allowDrafts === true);
48
+ }
49
+ }
50
+ /**
51
+ * Named subclass of SeedRegistry for use in test suites.
52
+ * Gives tests a semantic name without requiring any dependency on factory.ts.
53
+ */
54
+ export class InMemorySeedRegistry extends SeedRegistry {
55
+ }
56
+ /** Finds a branch by its stable id within a Seed. Returns null if not found. */
57
+ export function findBranchById(seed, id) {
58
+ return seed.branches.find(b => b.id === id) ?? null;
59
+ }
60
+ /**
61
+ * Returns the next free branch id for a seed in the form `br_NN` (zero-padded to 2,
62
+ * growing as needed). Scans existing ids, never reuses a number already present —
63
+ * even if a branch was removed, its id is not recycled (ids must be globally stable
64
+ * for the life of the seed so layouts/automations/FTS triggers never collide).
65
+ */
66
+ export function nextBranchId(seed) {
67
+ let max = 0;
68
+ for (const b of seed.branches) {
69
+ const m = /^br_0*([0-9]+)$/.exec(b.id);
70
+ if (m)
71
+ max = Math.max(max, parseInt(m[1], 10));
72
+ }
73
+ const n = max + 1;
74
+ return `br_${String(n).padStart(2, '0')}`;
75
+ }
76
+ /**
77
+ * Returns the input seeds reordered so that every seed appears AFTER all the
78
+ * seeds it depends on (its `relation` branch targets). Pure function — does
79
+ * not read from any global registry.
80
+ *
81
+ * Uses Kahn's algorithm (BFS on in-degree) for cycle detection and topological
82
+ * ordering. Sprint 3 (CLI / migration runner) consumes this to create tables in
83
+ * the correct order without disabling SQLite FK constraints.
84
+ *
85
+ * @throws {Error} When a relation targets an unknown slug not in `seeds`.
86
+ * @throws {Error} When a cyclic dependency is detected, listing involved slugs.
87
+ */
88
+ export function sortSeedsByDependencies(seeds) {
89
+ const slugSet = new Set(seeds.map(s => s.slug));
90
+ // Validate all relation targets exist in the input array
91
+ for (const seed of seeds) {
92
+ for (const branch of seed.branches) {
93
+ if (branch.type === 'relation' && branch.targetSeed) {
94
+ if (!slugSet.has(branch.targetSeed)) {
95
+ throw new Error(`Seed '${seed.slug}' relates to unknown target '${branch.targetSeed}'`);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ // Build adjacency: inDegree[slug] = number of dependencies
101
+ // dependents[slug] = slugs that depend on slug
102
+ const inDegree = new Map();
103
+ const dependents = new Map();
104
+ for (const seed of seeds) {
105
+ if (!inDegree.has(seed.slug))
106
+ inDegree.set(seed.slug, 0);
107
+ if (!dependents.has(seed.slug))
108
+ dependents.set(seed.slug, []);
109
+ }
110
+ for (const seed of seeds) {
111
+ // Deduplicate targets to avoid inflating inDegree on multi-branch refs to same seed
112
+ const targets = new Set(seed.branches
113
+ .filter(b => b.type === 'relation' && b.targetSeed)
114
+ .map(b => b.targetSeed));
115
+ for (const target of targets) {
116
+ if (target === seed.slug)
117
+ continue; // self-reference needs no ordering
118
+ // seed depends on target → increment seed's in-degree
119
+ inDegree.set(seed.slug, (inDegree.get(seed.slug) ?? 0) + 1);
120
+ dependents.get(target).push(seed.slug);
121
+ }
122
+ }
123
+ // Kahn's BFS: start with seeds that have no dependencies
124
+ const seedBySlug = new Map(seeds.map(s => [s.slug, s]));
125
+ const queue = [];
126
+ for (const [slug, deg] of inDegree) {
127
+ if (deg === 0)
128
+ queue.push(slug);
129
+ }
130
+ const result = [];
131
+ while (queue.length > 0) {
132
+ const slug = queue.shift();
133
+ result.push(seedBySlug.get(slug));
134
+ for (const dependent of dependents.get(slug) ?? []) {
135
+ const newDeg = (inDegree.get(dependent) ?? 0) - 1;
136
+ inDegree.set(dependent, newDeg);
137
+ if (newDeg === 0)
138
+ queue.push(dependent);
139
+ }
140
+ }
141
+ if (result.length !== seeds.length) {
142
+ // Isolate true cycle members
143
+ const stuck = new Set([...inDegree.entries()]
144
+ .filter(([, deg]) => deg > 0)
145
+ .map(([slug]) => slug));
146
+ const cycleNodes = [...stuck].filter(slug => {
147
+ const visited = new Set();
148
+ const queue = [...(dependents.get(slug) ?? [])].filter(s => stuck.has(s));
149
+ while (queue.length > 0) {
150
+ const curr = queue.shift();
151
+ if (curr === slug)
152
+ return true;
153
+ if (!visited.has(curr)) {
154
+ visited.add(curr);
155
+ for (const next of dependents.get(curr) ?? []) {
156
+ if (stuck.has(next))
157
+ queue.push(next);
158
+ }
159
+ }
160
+ }
161
+ return false;
162
+ });
163
+ const cycle = (cycleNodes.length > 0 ? cycleNodes : [...stuck]).sort();
164
+ throw new Error(`Cyclic dependency detected among seeds: ${cycle.join(', ')}`);
165
+ }
166
+ return result;
167
+ }
@@ -0,0 +1,25 @@
1
+ import type { Seed, Branch } from '../types.js';
2
+ /** slug/alias → PascalCase identifier. 'blog-posts' → 'BlogPosts'. */
3
+ export declare function pascalCase(input: string): string;
4
+ /** Maps a single Branch to its TypeScript type expression (no optional marker). */
5
+ export declare function tsTypeForBranch(branch: Branch): string;
6
+ /** Emits one exported interface for a single Seed. */
7
+ export declare function interfaceForSeed(seed: Seed): string;
8
+ /** Options that affect the emitted module beyond the seed interfaces themselves. */
9
+ export interface SeedTypesOptions {
10
+ /**
11
+ * Schema fingerprint to embed as `SCHEMA_FINGERPRINT`, from `computeSchemaFingerprint`.
12
+ * Omitted ⇒ no constant is emitted and the output is byte-identical to a pre-fingerprint build,
13
+ * which is what keeps the legacy `beech gen-types` aliases non-breaking.
14
+ */
15
+ fingerprint?: string;
16
+ }
17
+ /**
18
+ * Pure entry point. Deterministic: sorts seeds by slug for stable diffs.
19
+ *
20
+ * The embedded fingerprint is the client's half of the drift check: `@beechcms/client` compares it
21
+ * against the `X-Schema-Revision` header the API returns and raises an actionable error on a
22
+ * mismatch, instead of trusting a stale response shape.
23
+ */
24
+ export declare function generateSeedTypes(seeds: Seed[], options?: SeedTypesOptions): string;
25
+ //# sourceMappingURL=seed-types-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-types-generator.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/seed-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAe/C,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhD;AAMD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoCtD;AAWD,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAYnD;AAKD,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAcvF"}