@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,574 @@
1
+ import { resolvePolicies, resolveClassification } from '../privacy/policies.js';
2
+ /**
3
+ * Helper function to determine if a branch requires a blind index column.
4
+ * Returns true if storage classification is 'encrypt' and filtering is not explicitly disabled.
5
+ */
6
+ export function hasBlindIndex(branch) {
7
+ const resolved = resolveClassification(branch);
8
+ return resolved.storage === 'encrypt' && branch.policies?.filter !== false;
9
+ }
10
+ /**
11
+ * Mapping of core branch types to SQLite column types.
12
+ */
13
+ const BRANCH_TYPE_SQL = {
14
+ text: { sqlType: 'TEXT' },
15
+ number: { sqlType: 'REAL' },
16
+ boolean: { sqlType: 'INTEGER' },
17
+ date: { sqlType: 'INTEGER' }, // Unix timestamp (seconds)
18
+ json: { sqlType: 'TEXT' }, // Serialized JSON string
19
+ richtext: { sqlType: 'TEXT' },
20
+ file: { sqlType: 'TEXT' }, // Single URL or serialized JSON array of URLs
21
+ tags: { sqlType: 'TEXT' }, // Serialized JSON array or object
22
+ relation: { sqlType: 'TEXT' }, // Foreign Key reference stored as TEXT (id of the target row)
23
+ repeater: { sqlType: 'TEXT' }, // Serialized JSON array of records
24
+ };
25
+ /**
26
+ * System-defined columns that exist on all content tables.
27
+ */
28
+ export const SYSTEM_COLUMNS = new Set(['id', 'slug', 'status', 'created_at', 'updated_at', 'deleted_at']);
29
+ /**
30
+ * Returns the SQL table name for a given Seed.
31
+ *
32
+ * @param seed The seed definition.
33
+ * @returns The table name.
34
+ */
35
+ export function tableName(seed) {
36
+ return `content_${seed.slug}`;
37
+ }
38
+ /**
39
+ * Returns the draft SQL table name for a given Seed.
40
+ *
41
+ * @param seed The seed definition.
42
+ * @returns The draft table name.
43
+ */
44
+ function draftTableName(seed) {
45
+ return `content_${seed.slug}_drafts`;
46
+ }
47
+ /**
48
+ * Returns the FTS (Full-Text Search) virtual table name for a given Seed.
49
+ *
50
+ * @param seed The seed definition.
51
+ * @returns The FTS table name.
52
+ */
53
+ export function ftsTableName(seed) {
54
+ return `fts_${seed.slug}`;
55
+ }
56
+ /**
57
+ * Returns the vector table name for a given Seed.
58
+ *
59
+ * @param seed The seed definition.
60
+ * @returns The vector table name.
61
+ */
62
+ export function vectorTableName(seed) {
63
+ return `vector_${seed.slug}`;
64
+ }
65
+ /**
66
+ * Determines whether a column is valid for a given Seed (either system column or branch alias).
67
+ *
68
+ * @param seed The seed definition.
69
+ * @param col The column name to check.
70
+ * @returns True if the column is valid, false otherwise.
71
+ */
72
+ export function isValidColumn(seed, col) {
73
+ if (col === 'deleted_at')
74
+ return seed.softDelete === true;
75
+ if (SYSTEM_COLUMNS.has(col))
76
+ return true;
77
+ return seed.branches.some(b => b.alias === col);
78
+ }
79
+ /**
80
+ * Filters the branches of a Seed to return only the ones that are indexable for full-text search.
81
+ *
82
+ * @param seed The seed definition.
83
+ * @returns An array of indexable branches.
84
+ */
85
+ export function indexableSearchBranches(seed) {
86
+ return seed.branches.filter(b => {
87
+ if (b.type !== 'text' && b.type !== 'richtext')
88
+ return false;
89
+ const { search, public: isPublic } = resolvePolicies(b);
90
+ return search && isPublic;
91
+ });
92
+ }
93
+ /**
94
+ * The default ON DELETE referential integrity rule for single-value relations.
95
+ */
96
+ const DEFAULT_ON_DELETE_RULE = 'SET NULL';
97
+ /**
98
+ * Builds the SQL foreign key clause for a single-value relation branch.
99
+ *
100
+ * Multi-relation branches (multiple: true) produce NO column on the parent table;
101
+ * they live in a dedicated junction table. Returns an empty string for them.
102
+ *
103
+ * @param branch The branch definition.
104
+ * @throws {Error} If the relation branch is missing the `targetSeed` parameter.
105
+ * @returns The foreign key SQL clause, or an empty string.
106
+ */
107
+ function buildForeignKeyClause(branch) {
108
+ if (branch.type !== 'relation')
109
+ return '';
110
+ if (branch.multiple === true)
111
+ return '';
112
+ if (!branch.targetSeed) {
113
+ throw new Error(`Branch "${branch.alias}" is of type 'relation' but has no targetSeed`);
114
+ }
115
+ const onDeleteRule = branch.onDelete ?? DEFAULT_ON_DELETE_RULE;
116
+ return ` REFERENCES content_${branch.targetSeed}(id) ON DELETE ${onDeleteRule}`;
117
+ }
118
+ /**
119
+ * Generates the SQL `CREATE TABLE IF NOT EXISTS content_{slug}` statement
120
+ * with system columns and one column per branch.
121
+ *
122
+ * @param seed The seed definition.
123
+ * @returns The CREATE TABLE SQL statement.
124
+ */
125
+ export function generateCreateTable(seed) {
126
+ const table = tableName(seed);
127
+ const lines = [
128
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
129
+ ` id TEXT NOT NULL PRIMARY KEY,`,
130
+ // Soft-delete tables carry uniqueness in a PARTIAL unique index instead, so a trashed
131
+ // row stops reserving its slug. An inline UNIQUE would become a sqlite_autoindex that
132
+ // no ALTER TABLE can drop later.
133
+ seed.softDelete
134
+ ? ` slug TEXT NOT NULL,`
135
+ : ` slug TEXT NOT NULL UNIQUE,`,
136
+ ` status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),`,
137
+ ];
138
+ for (const branch of seed.branches) {
139
+ if (branch.type === 'relation' && branch.multiple === true)
140
+ continue;
141
+ const { sqlType } = BRANCH_TYPE_SQL[branch.type];
142
+ let col = ` ${branch.alias} ${sqlType}`;
143
+ if (branch.requiredOnCreate)
144
+ col += ' NOT NULL';
145
+ if (branch.type === 'boolean')
146
+ col += ` CHECK (${branch.alias} IN (0, 1))`;
147
+ col += buildForeignKeyClause(branch);
148
+ lines.push(col + ',');
149
+ if (hasBlindIndex(branch)) {
150
+ lines.push(` ${branch.alias}_bidx TEXT,`);
151
+ }
152
+ }
153
+ lines.push(` created_at INTEGER NOT NULL DEFAULT (unixepoch()),`);
154
+ lines.push(` updated_at INTEGER NOT NULL DEFAULT (unixepoch())` + (seed.softDelete ? ',' : ''));
155
+ if (seed.softDelete)
156
+ lines.push(` deleted_at INTEGER`);
157
+ lines.push(`);`);
158
+ return lines.join('\n');
159
+ }
160
+ /**
161
+ * Generates the SQL `CREATE TABLE IF NOT EXISTS content_{slug}_drafts` statement
162
+ * for seeds that allow drafts.
163
+ *
164
+ * @param seed The seed definition.
165
+ * @returns The draft table CREATE TABLE SQL statement, or null if drafts are disabled.
166
+ */
167
+ export function generateDraftTable(seed) {
168
+ if (!seed.allowDrafts)
169
+ return null;
170
+ const table = `content_${seed.slug}_drafts`;
171
+ const mainTable = `content_${seed.slug}`;
172
+ const lines = [
173
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
174
+ ` entry_id TEXT NOT NULL PRIMARY KEY`,
175
+ ` REFERENCES ${mainTable}(id) ON DELETE CASCADE,`,
176
+ ];
177
+ for (const branch of seed.branches) {
178
+ if (branch.type === 'relation' && branch.multiple === true)
179
+ continue;
180
+ const { sqlType } = BRANCH_TYPE_SQL[branch.type];
181
+ let col = ` ${branch.alias} ${sqlType}`;
182
+ if (branch.type === 'boolean')
183
+ col += ` CHECK (${branch.alias} IN (0, 1))`;
184
+ lines.push(col + ',');
185
+ if (hasBlindIndex(branch)) {
186
+ lines.push(` ${branch.alias}_bidx TEXT,`);
187
+ }
188
+ }
189
+ lines.push(` _touched_fields TEXT,`);
190
+ // Live-row `updated_at` as it stood when this draft was created, written once and never re-based.
191
+ // Nullable on purpose: drafts created before optimistic-concurrency publish existed carry NULL and
192
+ // publish unchecked, so enabling this feature cannot block an in-flight draft.
193
+ lines.push(` live_snapshot_at INTEGER,`);
194
+ lines.push(` updated_at INTEGER NOT NULL DEFAULT (unixepoch())`);
195
+ lines.push(`);`);
196
+ return lines.join('\n');
197
+ }
198
+ /**
199
+ * Generates the SQL `ALTER TABLE content_{slug} ADD COLUMN {alias} {type}` statement.
200
+ *
201
+ * @param seed The seed definition.
202
+ * @param branch The branch definition for the new column.
203
+ * @returns The ALTER TABLE SQL statement.
204
+ */
205
+ export function generateAddColumn(seed, branch) {
206
+ const { sqlType } = BRANCH_TYPE_SQL[branch.type];
207
+ return `ALTER TABLE ${tableName(seed)} ADD COLUMN ${branch.alias} ${sqlType}${buildForeignKeyClause(branch)};`;
208
+ }
209
+ /**
210
+ * Generates the `ALTER TABLE content_{slug}_drafts ADD COLUMN live_snapshot_at INTEGER` statement
211
+ * for a draft table provisioned before the column existed.
212
+ *
213
+ * System column, not a branch — `generateAddColumn` iterates `seed.branches` and can never emit it.
214
+ * NOT idempotent: `ADD COLUMN` fails when the column is already present and `ISchemaMutator.execDdl`
215
+ * aborts the whole batch on the first failing statement, so the caller MUST check the physical
216
+ * columns first (see `planExtendSeed`).
217
+ *
218
+ * @param seed The seed definition. Caller must have verified `seed.allowDrafts`.
219
+ * @returns The ALTER TABLE statement.
220
+ */
221
+ export function generateAddDraftSnapshotColumn(seed) {
222
+ return `ALTER TABLE content_${seed.slug}_drafts ADD COLUMN live_snapshot_at INTEGER;`;
223
+ }
224
+ /**
225
+ * Generates SQL index statements for system fields (status, created_at)
226
+ * and indexable branch columns.
227
+ *
228
+ * @param seed The seed definition.
229
+ * @returns An array of CREATE INDEX SQL statements.
230
+ */
231
+ export function generateIndexes(seed) {
232
+ const table = tableName(seed);
233
+ const slug = seed.slug;
234
+ const indexes = [
235
+ `CREATE INDEX IF NOT EXISTS idx_${slug}_status ON ${table}(status);`,
236
+ `CREATE INDEX IF NOT EXISTS idx_${slug}_created_at ON ${table}(created_at);`,
237
+ ];
238
+ for (const branch of seed.branches) {
239
+ if (branch.type === 'relation' && branch.multiple === true)
240
+ continue;
241
+ const isRelation = branch.type === 'relation';
242
+ if (!isRelation && !resolvePolicies(branch).filter)
243
+ continue;
244
+ if (['text', 'number', 'date', 'boolean', 'relation'].includes(branch.type)) {
245
+ indexes.push(`CREATE INDEX IF NOT EXISTS idx_${slug}_${branch.alias} ON ${table}(${branch.alias});`);
246
+ }
247
+ if (hasBlindIndex(branch)) {
248
+ indexes.push(`CREATE INDEX IF NOT EXISTS idx_${slug}_${branch.alias}_bidx ON ${table}(${branch.alias}_bidx);`);
249
+ }
250
+ }
251
+ if (seed.softDelete) {
252
+ indexes.push(`CREATE INDEX IF NOT EXISTS idx_${slug}_deleted_at ON ${table}(deleted_at);`);
253
+ // Active rows only: a trashed row releases its slug for reuse (feature brief §4).
254
+ indexes.push(`CREATE UNIQUE INDEX IF NOT EXISTS idx_${slug}_slug_active ON ${table}(slug) WHERE deleted_at IS NULL;`);
255
+ }
256
+ return indexes;
257
+ }
258
+ /**
259
+ * Generates the FTS5 virtual table definition for indexable text/richtext branches.
260
+ *
261
+ * @param seed The seed definition.
262
+ * @returns The CREATE VIRTUAL TABLE SQL statement, or null if no branches are indexable.
263
+ */
264
+ export function generateFtsTable(seed) {
265
+ const rtBranches = indexableSearchBranches(seed);
266
+ if (rtBranches.length === 0)
267
+ return null;
268
+ const ftsTable = ftsTableName(seed);
269
+ const cols = rtBranches.map(b => ` ${b.alias}`).join(',\n');
270
+ return [
271
+ `CREATE VIRTUAL TABLE IF NOT EXISTS ${ftsTable} USING fts5(`,
272
+ ` entry_id UNINDEXED,`,
273
+ `${cols},`,
274
+ ` tokenize = 'unicode61'`,
275
+ `);`,
276
+ ].join('\n');
277
+ }
278
+ /**
279
+ * Generates FTS5 triggers (insert, update, delete) to keep the FTS virtual table synchronized.
280
+ *
281
+ * @param seed The seed definition.
282
+ * @returns An array of CREATE TRIGGER SQL statements.
283
+ */
284
+ export function generateFtsTriggers(seed) {
285
+ const rtBranches = indexableSearchBranches(seed);
286
+ if (rtBranches.length === 0)
287
+ return [];
288
+ const table = tableName(seed);
289
+ const ftsTable = ftsTableName(seed);
290
+ const slug = seed.slug;
291
+ const cols = rtBranches.map(b => b.alias);
292
+ const ftsColList = ['entry_id', ...cols].join(', ');
293
+ const newValList = ['new.id', ...cols.map(c => `new.${c}`)].join(', ');
294
+ return [
295
+ [
296
+ `CREATE TRIGGER IF NOT EXISTS fts_${slug}_insert`,
297
+ `AFTER INSERT ON ${table} BEGIN`,
298
+ ` INSERT INTO ${ftsTable}(${ftsColList}) VALUES (${newValList});`,
299
+ `END;`,
300
+ ].join('\n'),
301
+ [
302
+ `CREATE TRIGGER IF NOT EXISTS fts_${slug}_update`,
303
+ `AFTER UPDATE OF ${cols.join(', ')} ON ${table} BEGIN`,
304
+ ` DELETE FROM ${ftsTable} WHERE entry_id = old.id;`,
305
+ ` INSERT INTO ${ftsTable}(${ftsColList}) VALUES (${newValList});`,
306
+ `END;`,
307
+ ].join('\n'),
308
+ [
309
+ `CREATE TRIGGER IF NOT EXISTS fts_${slug}_delete`,
310
+ `AFTER DELETE ON ${table} BEGIN`,
311
+ ` DELETE FROM ${ftsTable} WHERE entry_id = old.id;`,
312
+ `END;`,
313
+ ].join('\n'),
314
+ ];
315
+ }
316
+ /**
317
+ * Generates the SQL `CREATE TABLE IF NOT EXISTS vector_{slug}` statement
318
+ * for indexable text/richtext branches.
319
+ *
320
+ * @param seed The seed definition.
321
+ * @returns The CREATE TABLE SQL statement, or null if no branches are indexable.
322
+ */
323
+ export function generateVectorTable(seed) {
324
+ const rtBranches = indexableSearchBranches(seed);
325
+ if (rtBranches.length === 0)
326
+ return null;
327
+ const table = vectorTableName(seed);
328
+ return [
329
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
330
+ ` entry_id TEXT NOT NULL PRIMARY KEY REFERENCES content_${seed.slug}(id) ON DELETE CASCADE,`,
331
+ ` vector BLOB NOT NULL`,
332
+ `);`,
333
+ ].join('\n');
334
+ }
335
+ /**
336
+ * Returns the list of expected columns for a Seed's table.
337
+ * Used to compare current vs expected schema (diffing).
338
+ *
339
+ * @param seed The seed definition.
340
+ * @returns The array of expected schema columns.
341
+ */
342
+ export function getExpectedColumns(seed) {
343
+ const branchCols = [];
344
+ for (const b of seed.branches) {
345
+ if (b.type === 'relation' && b.multiple === true)
346
+ continue;
347
+ branchCols.push({
348
+ name: b.alias,
349
+ sqlType: BRANCH_TYPE_SQL[b.type].sqlType,
350
+ notNull: b.requiredOnCreate ?? false,
351
+ isPk: false,
352
+ });
353
+ if (hasBlindIndex(b)) {
354
+ branchCols.push({
355
+ name: `${b.alias}_bidx`,
356
+ sqlType: 'TEXT',
357
+ notNull: false,
358
+ isPk: false,
359
+ });
360
+ }
361
+ }
362
+ return [
363
+ { name: 'id', sqlType: 'TEXT', notNull: true, isPk: true },
364
+ { name: 'slug', sqlType: 'TEXT', notNull: true, isPk: false },
365
+ { name: 'status', sqlType: 'TEXT', notNull: true, isPk: false },
366
+ ...branchCols,
367
+ { name: 'created_at', sqlType: 'INTEGER', notNull: true, isPk: false },
368
+ { name: 'updated_at', sqlType: 'INTEGER', notNull: true, isPk: false },
369
+ ...(seed.softDelete
370
+ ? [{ name: 'deleted_at', sqlType: 'INTEGER', notNull: false, isPk: false }]
371
+ : []),
372
+ ];
373
+ }
374
+ /**
375
+ * Returns the junction table name for a many-to-many relation branch.
376
+ * Format: `rel_<seedSlug>_<branchAlias>`.
377
+ *
378
+ * @param seedSlug The slug of the parent Seed.
379
+ * @param branchAlias The alias of the relation branch.
380
+ * @returns The junction table name.
381
+ */
382
+ export function junctionTableName(seedSlug, branchAlias) {
383
+ return `rel_${seedSlug}_${branchAlias}`;
384
+ }
385
+ /**
386
+ * Generates `CREATE TABLE IF NOT EXISTS rel_<seed>_<alias>` statement for a multi-relation branch.
387
+ *
388
+ * @param seed The parent seed definition.
389
+ * @param branch The multi-relation branch definition.
390
+ * @throws {Error} If called on a non-multi-relation branch or if `targetSeed` is missing.
391
+ * @returns The CREATE TABLE SQL statement.
392
+ */
393
+ export function generateJunctionTable(seed, branch) {
394
+ if (branch.type !== 'relation' || branch.multiple !== true) {
395
+ throw new Error(`Branch "${branch.alias}" is not a multi-relation`);
396
+ }
397
+ if (!branch.targetSeed) {
398
+ throw new Error(`Branch "${branch.alias}" has no targetSeed`);
399
+ }
400
+ const onDeleteRule = branch.onDelete ?? 'CASCADE';
401
+ const table = junctionTableName(seed.slug, branch.alias);
402
+ return [
403
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
404
+ ` parent_id TEXT NOT NULL REFERENCES content_${seed.slug}(id) ON DELETE CASCADE,`,
405
+ ` target_id TEXT NOT NULL REFERENCES content_${branch.targetSeed}(id) ON DELETE ${onDeleteRule},`,
406
+ ` position INTEGER NOT NULL DEFAULT 0,`,
407
+ ` created_at INTEGER NOT NULL DEFAULT (unixepoch()),`,
408
+ ` PRIMARY KEY (parent_id, target_id)`,
409
+ `);`,
410
+ ].join('\n');
411
+ }
412
+ /**
413
+ * Generates the two B-tree indexes for a junction table:
414
+ * one on `parent_id` (listing entries by parent) and one on `target_id` (handling cascade checks from target side).
415
+ *
416
+ * @param seed The parent seed definition.
417
+ * @param branch The multi-relation branch definition.
418
+ * @returns An array of CREATE INDEX SQL statements.
419
+ */
420
+ export function generateJunctionIndexes(seed, branch) {
421
+ const table = junctionTableName(seed.slug, branch.alias);
422
+ return [
423
+ `CREATE INDEX IF NOT EXISTS idx_${table}_parent ON ${table}(parent_id);`,
424
+ `CREATE INDEX IF NOT EXISTS idx_${table}_target ON ${table}(target_id);`,
425
+ ];
426
+ }
427
+ /**
428
+ * Generates the drafts junction table `rel_<seed>_<alias>_drafts` for a many-to-many relation branch.
429
+ *
430
+ * @param seed The parent seed definition.
431
+ * @param branch The multi-relation branch definition.
432
+ * @returns The CREATE TABLE SQL statement, or null if drafts are disabled.
433
+ */
434
+ export function generateJunctionDraftTable(seed, branch) {
435
+ if (!seed.allowDrafts)
436
+ return null;
437
+ if (branch.type !== 'relation' || branch.multiple !== true)
438
+ return null;
439
+ const table = `${junctionTableName(seed.slug, branch.alias)}_drafts`;
440
+ const mainDraftTable = `content_${seed.slug}_drafts`;
441
+ return [
442
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
443
+ ` entry_id TEXT NOT NULL REFERENCES ${mainDraftTable}(entry_id) ON DELETE CASCADE,`,
444
+ ` target_id TEXT NOT NULL,`,
445
+ ` position INTEGER NOT NULL DEFAULT 0,`,
446
+ ` PRIMARY KEY (entry_id, target_id)`,
447
+ `);`,
448
+ ].join('\n');
449
+ }
450
+ /**
451
+ * Returns every `DROP TABLE IF EXISTS` needed to fully remove a content type:
452
+ * the main table, the drafts table (if drafts enabled), full-text search table/triggers, and junction tables.
453
+ *
454
+ * @param seed The seed definition.
455
+ * @returns An array of DROP TABLE / DROP TRIGGER SQL statements.
456
+ */
457
+ export function generateDropTable(seed) {
458
+ const stmts = [];
459
+ for (const branch of seed.branches) {
460
+ if (branch.type !== 'relation' || branch.multiple !== true)
461
+ continue;
462
+ const jt = junctionTableName(seed.slug, branch.alias);
463
+ stmts.push(`DROP TABLE IF EXISTS ${jt}_drafts;`);
464
+ stmts.push(`DROP TABLE IF EXISTS ${jt};`);
465
+ }
466
+ if (seed.allowDrafts) {
467
+ stmts.push(`DROP TABLE IF EXISTS ${draftTableName(seed)};`);
468
+ }
469
+ if (indexableSearchBranches(seed).length > 0) {
470
+ const slug = seed.slug;
471
+ stmts.push(`DROP TRIGGER IF EXISTS fts_${slug}_insert;`);
472
+ stmts.push(`DROP TRIGGER IF EXISTS fts_${slug}_update;`);
473
+ stmts.push(`DROP TRIGGER IF EXISTS fts_${slug}_delete;`);
474
+ stmts.push(`DROP TABLE IF EXISTS ${ftsTableName(seed)};`);
475
+ stmts.push(`DROP TABLE IF EXISTS ${vectorTableName(seed)};`);
476
+ }
477
+ stmts.push(`DROP TABLE IF EXISTS ${tableName(seed)};`);
478
+ return stmts;
479
+ }
480
+ /**
481
+ * Returns the `DROP COLUMN` SQL statements for removing a single field.
482
+ * Drops the junction table for multi-relations, or alters the tables to drop the column.
483
+ *
484
+ * @param seed The seed definition.
485
+ * @param alias The alias of the branch to drop.
486
+ * @returns An array of SQL statements.
487
+ */
488
+ export function generateDropColumn(seed, alias) {
489
+ const branch = seed.branches.find(b => b.alias === alias);
490
+ if (branch && branch.type === 'relation' && branch.multiple === true) {
491
+ const jt = junctionTableName(seed.slug, alias);
492
+ return [
493
+ `DROP TABLE IF EXISTS ${jt}_drafts;`,
494
+ `DROP TABLE IF EXISTS ${jt};`,
495
+ ];
496
+ }
497
+ const stmts = [`ALTER TABLE ${tableName(seed)} DROP COLUMN ${alias};`];
498
+ if (branch && seed.allowDrafts) {
499
+ stmts.push(`ALTER TABLE ${draftTableName(seed)} DROP COLUMN ${alias};`);
500
+ }
501
+ return stmts;
502
+ }
503
+ /**
504
+ * Returns the `RENAME COLUMN` SQL statements for renaming a field's alias.
505
+ * For a multi-relation branch, renames the junction table (+ drafts junction table) instead.
506
+ *
507
+ * @param seed The seed definition.
508
+ * @param from The old alias.
509
+ * @param to The new alias.
510
+ * @returns An array of SQL statements.
511
+ */
512
+ export function generateRenameColumn(seed, from, to) {
513
+ const branch = seed.branches.find(b => b.alias === from);
514
+ if (branch && branch.type === 'relation' && branch.multiple === true) {
515
+ const oldJt = junctionTableName(seed.slug, from);
516
+ const newJt = junctionTableName(seed.slug, to);
517
+ const stmts = [`ALTER TABLE ${oldJt} RENAME TO ${newJt};`];
518
+ if (seed.allowDrafts) {
519
+ stmts.push(`ALTER TABLE ${oldJt}_drafts RENAME TO ${newJt}_drafts;`);
520
+ }
521
+ return stmts;
522
+ }
523
+ const stmts = [`ALTER TABLE ${tableName(seed)} RENAME COLUMN ${from} TO ${to};`];
524
+ if (branch && seed.allowDrafts) {
525
+ stmts.push(`ALTER TABLE ${draftTableName(seed)} RENAME COLUMN ${from} TO ${to};`);
526
+ }
527
+ return stmts;
528
+ }
529
+ /**
530
+ * Returns the statements that change a column's SQL type in place using CAST, bypassing SQLite rebuild limitations.
531
+ *
532
+ * @param seed The seed definition.
533
+ * @param branch The target branch definition (carrying the new type).
534
+ * @throws {Error} If called on a multi-relation branch.
535
+ * @returns An array of SQL statements.
536
+ */
537
+ export function generateRetypeColumn(seed, branch) {
538
+ if (branch.type === 'relation' && branch.multiple === true) {
539
+ throw new Error(`Branch "${branch.alias}" is a multi-relation and has no column to retype`);
540
+ }
541
+ const { sqlType } = BRANCH_TYPE_SQL[branch.type];
542
+ const alias = branch.alias;
543
+ const tmp = `__retype_${alias}`;
544
+ const rebuild = (table) => [
545
+ `ALTER TABLE ${table} ADD COLUMN ${tmp} ${sqlType};`,
546
+ `UPDATE ${table} SET ${tmp} = CAST(${alias} AS ${sqlType});`,
547
+ `ALTER TABLE ${table} DROP COLUMN ${alias};`,
548
+ `ALTER TABLE ${table} RENAME COLUMN ${tmp} TO ${alias};`,
549
+ ];
550
+ const stmts = rebuild(tableName(seed));
551
+ if (seed.allowDrafts)
552
+ stmts.push(...rebuild(draftTableName(seed)));
553
+ return stmts;
554
+ }
555
+ /**
556
+ * Additive statements that turn soft delete on for a table that already exists.
557
+ *
558
+ * The partial unique index is emitted for correctness on tables that were CREATED with
559
+ * `softDelete: true`. On a pre-existing table the inline `slug … UNIQUE` survives as a
560
+ * sqlite_autoindex that SQLite cannot drop via ALTER; there the partial index is redundant
561
+ * and a trashed slug stays reserved until the table is rebuilt (see ROADMAP deferral).
562
+ * Returns [] when the seed does not opt in.
563
+ */
564
+ export function generateEnableSoftDelete(seed) {
565
+ if (!seed.softDelete)
566
+ return [];
567
+ const table = tableName(seed);
568
+ const slug = seed.slug;
569
+ return [
570
+ `ALTER TABLE ${table} ADD COLUMN deleted_at INTEGER;`,
571
+ `CREATE INDEX IF NOT EXISTS idx_${slug}_deleted_at ON ${table}(deleted_at);`,
572
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_${slug}_slug_active ON ${table}(slug) WHERE deleted_at IS NULL;`,
573
+ ];
574
+ }
@@ -0,0 +1,26 @@
1
+ /** Executes additive schema DDL against the live database and introspects columns.
2
+ * Implemented by D1SchemaMutator in apps/api/src/shared/schema-mutator.d1.ts.
3
+ * This is the ONLY sanctioned channel for runtime DDL — handlers never touch env.DB. */
4
+ export interface ISchemaMutator {
5
+ /** Column names currently on a table, or null if the table does not exist. */
6
+ getColumns(table: string): Promise<Set<string> | null>;
7
+ /** Runs the given DDL statements in order as a single D1 batch.
8
+ * All statements must be additive (CREATE … IF NOT EXISTS / ALTER … ADD COLUMN /
9
+ * CREATE INDEX IF NOT EXISTS). Throws on the first failing statement. */
10
+ execDdl(statements: string[]): Promise<void>;
11
+ /** `DROP TABLE IF EXISTS {table}`. */
12
+ dropTable(table: string): Promise<void>;
13
+ /** `ALTER TABLE {table} DROP COLUMN {column}`. */
14
+ dropColumn(table: string, column: string): Promise<void>;
15
+ /** `ALTER TABLE {table} RENAME COLUMN {from} TO {to}`. */
16
+ renameColumn(table: string, from: string, to: string): Promise<void>;
17
+ /** Runs a multi-statement destructive batch atomically (FTS rebuild,
18
+ * type-change column rebuild, full type drop). The caller assembles the
19
+ * statements; the impl validates identifiers and runs them as one D1 batch. */
20
+ execDestructive(statements: string[]): Promise<void>;
21
+ /** Reads all rows from a table for the specified columns only.
22
+ * Used before DROP TABLE to collect R2 keys for media cleanup.
23
+ * All identifiers are validated against `^[A-Za-z0-9_]+$`. */
24
+ fetchRows(table: string, columns: string[]): Promise<Record<string, unknown>[]>;
25
+ }
26
+ //# sourceMappingURL=schema-mutator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-mutator.d.ts","sourceRoot":"","sources":["../../../src/engine/ddl/schema-mutator.ts"],"names":[],"mappings":"AAGA;;yFAEyF;AACzF,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAA;IACtD;;8EAE0E;IAC1E,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAS5C,sCAAsC;IACtC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,kDAAkD;IAClD,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,0DAA0D;IAC1D,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpE;;oFAEgF;IAChF,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEpD;;mEAE+D;IAC/D,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;CAChF"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,40 @@
1
+ import type { Seed, BranchType } from '../types.js';
2
+ /**
3
+ * Ordered DROP TABLE statements for a full hard-delete of a seed's tables.
4
+ * Order: junction drafts → junctions → FTS triggers → FTS table → drafts → main table.
5
+ * All statements use IF EXISTS (idempotent).
6
+ */
7
+ export declare function generateDropTable(seed: Seed): string[];
8
+ /**
9
+ * DROP COLUMN statements for a single branch (by alias).
10
+ * For multi-relation branches: drops the junction tables (no column on parent).
11
+ * For regular branches: drops from main table + drafts table (if allowDrafts).
12
+ * Does NOT include FTS handling — callers must prepend ftsTriggerDrops() and
13
+ * append planFtsRebuild() when the branch is searchable.
14
+ */
15
+ export declare function generateDropColumn(seed: Seed, alias: string): string[];
16
+ /**
17
+ * RENAME COLUMN statements for a branch alias change.
18
+ * Updates main table + drafts (if allowDrafts).
19
+ * Does NOT handle FTS — callers must rebuild FTS separately (SQLite auto-updates
20
+ * trigger bodies on RENAME but the FTS virtual-table columns keep the old name).
21
+ */
22
+ export declare function generateRenameColumn(seed: Seed, fromAlias: string, toAlias: string): string[];
23
+ /**
24
+ * Full FTS rebuild: drop triggers + FTS table, then recreate them, then backfill.
25
+ * Returns an empty array if the seed has no searchable branches.
26
+ * The drop steps use IF EXISTS so this is safe to call even if FTS was already dropped.
27
+ */
28
+ export declare function planFtsRebuild(seed: Seed): string[];
29
+ export interface RetypeColumnPlan {
30
+ statements: string[];
31
+ ftsRebuildNeeded: boolean;
32
+ }
33
+ /**
34
+ * Generates statements to change a branch's SQL storage type using the
35
+ * add-new-column / copy / drop-old / rename approach.
36
+ * Includes FTS rebuild statements when the branch is (or becomes) searchable.
37
+ * `updatedSeed` must already reflect the new type on the renamed branch.
38
+ */
39
+ export declare function planRetypeColumn(seed: Seed, alias: string, newType: BranchType, updatedSeed: Seed): RetypeColumnPlan;
40
+ //# sourceMappingURL=seed-ddl-destructive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-ddl-destructive.d.ts","sourceRoot":"","sources":["../../../src/engine/ddl/seed-ddl-destructive.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAU,UAAU,EAAE,MAAM,aAAa,CAAA;AAkC3D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAgBtD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBtE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ7F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAiBnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EACnB,WAAW,EAAE,IAAI,GAChB,gBAAgB,CAyClB"}