emdash 0.0.0-b → 0.0.2

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 (661) hide show
  1. package/README.md +87 -43
  2. package/dist/adapters-BLMa4JGD.d.mts +106 -0
  3. package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
  4. package/dist/apply-Bjfq_b4-.mjs +1293 -0
  5. package/dist/apply-Bjfq_b4-.mjs.map +1 -0
  6. package/dist/astro/index.d.mts +51 -0
  7. package/dist/astro/index.d.mts.map +1 -0
  8. package/dist/astro/index.mjs +1336 -0
  9. package/dist/astro/index.mjs.map +1 -0
  10. package/dist/astro/middleware/auth.d.mts +31 -0
  11. package/dist/astro/middleware/auth.d.mts.map +1 -0
  12. package/dist/astro/middleware/auth.mjs +654 -0
  13. package/dist/astro/middleware/auth.mjs.map +1 -0
  14. package/dist/astro/middleware/redirect.d.mts +22 -0
  15. package/dist/astro/middleware/redirect.d.mts.map +1 -0
  16. package/dist/astro/middleware/redirect.mjs +63 -0
  17. package/dist/astro/middleware/redirect.mjs.map +1 -0
  18. package/dist/astro/middleware/request-context.d.mts +18 -0
  19. package/dist/astro/middleware/request-context.d.mts.map +1 -0
  20. package/dist/astro/middleware/request-context.mjs +1310 -0
  21. package/dist/astro/middleware/request-context.mjs.map +1 -0
  22. package/dist/astro/middleware/setup.d.mts +20 -0
  23. package/dist/astro/middleware/setup.d.mts.map +1 -0
  24. package/dist/astro/middleware/setup.mjs +47 -0
  25. package/dist/astro/middleware/setup.mjs.map +1 -0
  26. package/dist/astro/middleware.d.mts +13 -0
  27. package/dist/astro/middleware.d.mts.map +1 -0
  28. package/dist/astro/middleware.mjs +1613 -0
  29. package/dist/astro/middleware.mjs.map +1 -0
  30. package/dist/astro/types.d.mts +250 -0
  31. package/dist/astro/types.d.mts.map +1 -0
  32. package/dist/astro/types.mjs +1 -0
  33. package/dist/base64-MBPo9ozB.mjs +59 -0
  34. package/dist/base64-MBPo9ozB.mjs.map +1 -0
  35. package/dist/byline-CL847F26.mjs +213 -0
  36. package/dist/byline-CL847F26.mjs.map +1 -0
  37. package/dist/bylines-C2a-2TGt.mjs +136 -0
  38. package/dist/bylines-C2a-2TGt.mjs.map +1 -0
  39. package/dist/chunk-ClPoSABd.mjs +21 -0
  40. package/dist/cli/index.d.mts +1 -0
  41. package/dist/cli/index.mjs +3909 -0
  42. package/dist/cli/index.mjs.map +1 -0
  43. package/dist/client/cf-access.d.mts +60 -0
  44. package/dist/client/cf-access.d.mts.map +1 -0
  45. package/dist/client/cf-access.mjs +179 -0
  46. package/dist/client/cf-access.mjs.map +1 -0
  47. package/dist/client/index.d.mts +398 -0
  48. package/dist/client/index.d.mts.map +1 -0
  49. package/dist/client/index.mjs +346 -0
  50. package/dist/client/index.mjs.map +1 -0
  51. package/dist/config-CKE8p9xM.mjs +55 -0
  52. package/dist/config-CKE8p9xM.mjs.map +1 -0
  53. package/dist/connection-B4zVnQIa.mjs +40 -0
  54. package/dist/connection-B4zVnQIa.mjs.map +1 -0
  55. package/dist/content-D6C2WsZC.mjs +824 -0
  56. package/dist/content-D6C2WsZC.mjs.map +1 -0
  57. package/dist/db/index.d.mts +4 -0
  58. package/dist/db/index.mjs +62 -0
  59. package/dist/db/index.mjs.map +1 -0
  60. package/dist/db/libsql.d.mts +11 -0
  61. package/dist/db/libsql.d.mts.map +1 -0
  62. package/dist/db/libsql.mjs +17 -0
  63. package/dist/db/libsql.mjs.map +1 -0
  64. package/dist/db/postgres.d.mts +11 -0
  65. package/dist/db/postgres.d.mts.map +1 -0
  66. package/dist/db/postgres.mjs +30 -0
  67. package/dist/db/postgres.mjs.map +1 -0
  68. package/dist/db/sqlite.d.mts +11 -0
  69. package/dist/db/sqlite.d.mts.map +1 -0
  70. package/dist/db/sqlite.mjs +16 -0
  71. package/dist/db/sqlite.mjs.map +1 -0
  72. package/dist/default-Cyi4aAxu.mjs +81 -0
  73. package/dist/default-Cyi4aAxu.mjs.map +1 -0
  74. package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
  75. package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
  76. package/dist/error-Cxz0tQeO.mjs +27 -0
  77. package/dist/error-Cxz0tQeO.mjs.map +1 -0
  78. package/dist/index-C1xF3OGh.d.mts +4527 -0
  79. package/dist/index-C1xF3OGh.d.mts.map +1 -0
  80. package/dist/index.d.mts +16 -0
  81. package/dist/index.mjs +30 -0
  82. package/dist/load-yOOlckBj.mjs +28 -0
  83. package/dist/load-yOOlckBj.mjs.map +1 -0
  84. package/dist/loader-fz8Q_3EO.mjs +447 -0
  85. package/dist/loader-fz8Q_3EO.mjs.map +1 -0
  86. package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
  87. package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
  88. package/dist/media/index.d.mts +26 -0
  89. package/dist/media/index.d.mts.map +1 -0
  90. package/dist/media/index.mjs +55 -0
  91. package/dist/media/index.mjs.map +1 -0
  92. package/dist/media/local-runtime.d.mts +39 -0
  93. package/dist/media/local-runtime.d.mts.map +1 -0
  94. package/dist/media/local-runtime.mjs +133 -0
  95. package/dist/media/local-runtime.mjs.map +1 -0
  96. package/dist/media-DqHVh136.mjs +200 -0
  97. package/dist/media-DqHVh136.mjs.map +1 -0
  98. package/dist/mode-C2EzN1uE.mjs +23 -0
  99. package/dist/mode-C2EzN1uE.mjs.map +1 -0
  100. package/dist/page/index.d.mts +140 -0
  101. package/dist/page/index.d.mts.map +1 -0
  102. package/dist/page/index.mjs +416 -0
  103. package/dist/page/index.mjs.map +1 -0
  104. package/dist/placeholder-CmGAmqeO.d.mts +276 -0
  105. package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
  106. package/dist/placeholder-SmpOx-_v.mjs +243 -0
  107. package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
  108. package/dist/plugin-utils.d.mts +58 -0
  109. package/dist/plugin-utils.d.mts.map +1 -0
  110. package/dist/plugin-utils.mjs +78 -0
  111. package/dist/plugin-utils.mjs.map +1 -0
  112. package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
  113. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
  114. package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
  115. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
  116. package/dist/query-CS_iSj34.mjs +460 -0
  117. package/dist/query-CS_iSj34.mjs.map +1 -0
  118. package/dist/redirect-DIfIni3r.mjs +329 -0
  119. package/dist/redirect-DIfIni3r.mjs.map +1 -0
  120. package/dist/registry-D_w5HW4G.mjs +863 -0
  121. package/dist/registry-D_w5HW4G.mjs.map +1 -0
  122. package/dist/request-context.d.mts +49 -0
  123. package/dist/request-context.d.mts.map +1 -0
  124. package/dist/request-context.mjs +43 -0
  125. package/dist/request-context.mjs.map +1 -0
  126. package/dist/runner-C0hCbYnD.mjs +1412 -0
  127. package/dist/runner-C0hCbYnD.mjs.map +1 -0
  128. package/dist/runner-EAtf0ZIe.d.mts +27 -0
  129. package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
  130. package/dist/runtime.d.mts +26 -0
  131. package/dist/runtime.d.mts.map +1 -0
  132. package/dist/runtime.mjs +42 -0
  133. package/dist/runtime.mjs.map +1 -0
  134. package/dist/search-DG603UrT.mjs +9211 -0
  135. package/dist/search-DG603UrT.mjs.map +1 -0
  136. package/dist/seed/index.d.mts +3 -0
  137. package/dist/seed/index.mjs +15 -0
  138. package/dist/seo/index.d.mts +70 -0
  139. package/dist/seo/index.d.mts.map +1 -0
  140. package/dist/seo/index.mjs +70 -0
  141. package/dist/seo/index.mjs.map +1 -0
  142. package/dist/storage/local.d.mts +39 -0
  143. package/dist/storage/local.d.mts.map +1 -0
  144. package/dist/storage/local.mjs +166 -0
  145. package/dist/storage/local.mjs.map +1 -0
  146. package/dist/storage/s3.d.mts +32 -0
  147. package/dist/storage/s3.d.mts.map +1 -0
  148. package/dist/storage/s3.mjs +175 -0
  149. package/dist/storage/s3.mjs.map +1 -0
  150. package/dist/tokens-DpgrkrXK.mjs +171 -0
  151. package/dist/tokens-DpgrkrXK.mjs.map +1 -0
  152. package/dist/transport-BFGblqwG.d.mts +42 -0
  153. package/dist/transport-BFGblqwG.d.mts.map +1 -0
  154. package/dist/transport-yxiQsi8I.mjs +418 -0
  155. package/dist/transport-yxiQsi8I.mjs.map +1 -0
  156. package/dist/types-BRuPJGdV.d.mts +102 -0
  157. package/dist/types-BRuPJGdV.d.mts.map +1 -0
  158. package/dist/types-C4-fAxN3.d.mts +182 -0
  159. package/dist/types-C4-fAxN3.d.mts.map +1 -0
  160. package/dist/types-CMMN0pNg.mjs +31 -0
  161. package/dist/types-CMMN0pNg.mjs.map +1 -0
  162. package/dist/types-CUBbjgmP.mjs +16 -0
  163. package/dist/types-CUBbjgmP.mjs.map +1 -0
  164. package/dist/types-DRjfYOEv.d.mts +426 -0
  165. package/dist/types-DRjfYOEv.d.mts.map +1 -0
  166. package/dist/types-DY5zk5HN.mjs +73 -0
  167. package/dist/types-DY5zk5HN.mjs.map +1 -0
  168. package/dist/types-DaNLHo_T.d.mts +184 -0
  169. package/dist/types-DaNLHo_T.d.mts.map +1 -0
  170. package/dist/types-DvhsUmSJ.d.mts +1111 -0
  171. package/dist/types-DvhsUmSJ.d.mts.map +1 -0
  172. package/dist/validate-CpBtVMsD.d.mts +378 -0
  173. package/dist/validate-CpBtVMsD.d.mts.map +1 -0
  174. package/dist/validate-CqRJb_xU.mjs +97 -0
  175. package/dist/validate-CqRJb_xU.mjs.map +1 -0
  176. package/dist/validate-O7PWmlnq.mjs +328 -0
  177. package/dist/validate-O7PWmlnq.mjs.map +1 -0
  178. package/locals.d.ts +46 -0
  179. package/package.json +233 -19
  180. package/src/api/authorize.ts +63 -0
  181. package/src/api/csrf.ts +48 -0
  182. package/src/api/error.ts +99 -0
  183. package/src/api/errors.ts +445 -0
  184. package/src/api/escape.ts +9 -0
  185. package/src/api/handlers/api-tokens.ts +240 -0
  186. package/src/api/handlers/comments.ts +314 -0
  187. package/src/api/handlers/content.ts +1315 -0
  188. package/src/api/handlers/dashboard.ts +205 -0
  189. package/src/api/handlers/device-flow.ts +684 -0
  190. package/src/api/handlers/index.ts +163 -0
  191. package/src/api/handlers/manifest.ts +158 -0
  192. package/src/api/handlers/marketplace.ts +930 -0
  193. package/src/api/handlers/media.ts +207 -0
  194. package/src/api/handlers/menus.ts +493 -0
  195. package/src/api/handlers/oauth-authorization.ts +429 -0
  196. package/src/api/handlers/oauth-clients.ts +349 -0
  197. package/src/api/handlers/oauth-user-lookup.ts +39 -0
  198. package/src/api/handlers/plugins.ts +254 -0
  199. package/src/api/handlers/redirects.ts +360 -0
  200. package/src/api/handlers/revision.ts +145 -0
  201. package/src/api/handlers/schema.ts +534 -0
  202. package/src/api/handlers/sections.ts +289 -0
  203. package/src/api/handlers/seo.ts +115 -0
  204. package/src/api/handlers/settings.ts +49 -0
  205. package/src/api/handlers/snapshot.ts +350 -0
  206. package/src/api/handlers/taxonomies.ts +523 -0
  207. package/src/api/index.ts +6 -0
  208. package/src/api/openapi/document.ts +2368 -0
  209. package/src/api/openapi/index.ts +1 -0
  210. package/src/api/parse.ts +139 -0
  211. package/src/api/redirect.ts +14 -0
  212. package/src/api/rev.ts +67 -0
  213. package/src/api/schemas/auth.ts +112 -0
  214. package/src/api/schemas/bylines.ts +85 -0
  215. package/src/api/schemas/comments.ts +117 -0
  216. package/src/api/schemas/common.ts +89 -0
  217. package/src/api/schemas/content.ts +191 -0
  218. package/src/api/schemas/import.ts +52 -0
  219. package/src/api/schemas/index.ts +17 -0
  220. package/src/api/schemas/media.ts +116 -0
  221. package/src/api/schemas/menus.ts +111 -0
  222. package/src/api/schemas/redirects.ts +155 -0
  223. package/src/api/schemas/schema.ts +203 -0
  224. package/src/api/schemas/search.ts +63 -0
  225. package/src/api/schemas/sections.ts +67 -0
  226. package/src/api/schemas/settings.ts +63 -0
  227. package/src/api/schemas/setup.ts +37 -0
  228. package/src/api/schemas/taxonomies.ts +113 -0
  229. package/src/api/schemas/users.ts +96 -0
  230. package/src/api/schemas/widgets.ts +80 -0
  231. package/src/api/site-url.ts +25 -0
  232. package/src/api/types.ts +82 -0
  233. package/src/astro/index.ts +27 -0
  234. package/src/astro/integration/index.ts +303 -0
  235. package/src/astro/integration/routes.ts +834 -0
  236. package/src/astro/integration/runtime.ts +338 -0
  237. package/src/astro/integration/virtual-modules.ts +469 -0
  238. package/src/astro/integration/vite-config.ts +335 -0
  239. package/src/astro/middleware/auth.ts +743 -0
  240. package/src/astro/middleware/redirect.ts +89 -0
  241. package/src/astro/middleware/request-context.ts +129 -0
  242. package/src/astro/middleware/setup.ts +89 -0
  243. package/src/astro/middleware.ts +398 -0
  244. package/src/astro/routes/PluginRegistry.tsx +15 -0
  245. package/src/astro/routes/admin.astro +81 -0
  246. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  247. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  248. package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
  249. package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
  250. package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
  251. package/src/astro/routes/api/admin/bylines/index.ts +72 -0
  252. package/src/astro/routes/api/admin/comments/[id]/status.ts +116 -0
  253. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  254. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  255. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  256. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  257. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
  258. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  259. package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
  260. package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
  261. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
  262. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
  263. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  264. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
  265. package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
  266. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  267. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
  268. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  269. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
  270. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  271. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  272. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  273. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
  274. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  275. package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
  276. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  277. package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
  278. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  279. package/src/astro/routes/api/admin/users/index.ts +66 -0
  280. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  281. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  282. package/src/astro/routes/api/auth/invite/complete.ts +84 -0
  283. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  284. package/src/astro/routes/api/auth/logout.ts +40 -0
  285. package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
  286. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  287. package/src/astro/routes/api/auth/me.ts +60 -0
  288. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
  289. package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
  290. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  291. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  292. package/src/astro/routes/api/auth/passkey/options.ts +82 -0
  293. package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
  294. package/src/astro/routes/api/auth/passkey/register/verify.ts +115 -0
  295. package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
  296. package/src/astro/routes/api/auth/signup/complete.ts +85 -0
  297. package/src/astro/routes/api/auth/signup/request.ts +77 -0
  298. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  299. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
  300. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  301. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
  302. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
  303. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
  304. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  305. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
  306. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
  307. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  308. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +101 -0
  309. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
  310. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
  311. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
  312. package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
  313. package/src/astro/routes/api/content/[collection]/index.ts +59 -0
  314. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  315. package/src/astro/routes/api/dashboard.ts +32 -0
  316. package/src/astro/routes/api/dev/emails.ts +36 -0
  317. package/src/astro/routes/api/import/probe.ts +47 -0
  318. package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
  319. package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
  320. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  321. package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
  322. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
  323. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  324. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  325. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
  326. package/src/astro/routes/api/manifest.ts +62 -0
  327. package/src/astro/routes/api/mcp.ts +124 -0
  328. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  329. package/src/astro/routes/api/media/[id].ts +145 -0
  330. package/src/astro/routes/api/media/file/[key].ts +79 -0
  331. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
  332. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  333. package/src/astro/routes/api/media/providers/index.ts +30 -0
  334. package/src/astro/routes/api/media/upload-url.ts +137 -0
  335. package/src/astro/routes/api/media.ts +190 -0
  336. package/src/astro/routes/api/menus/[name]/items.ts +87 -0
  337. package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
  338. package/src/astro/routes/api/menus/[name].ts +65 -0
  339. package/src/astro/routes/api/menus/index.ts +47 -0
  340. package/src/astro/routes/api/oauth/authorize.ts +412 -0
  341. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  342. package/src/astro/routes/api/oauth/device/code.ts +51 -0
  343. package/src/astro/routes/api/oauth/device/token.ts +69 -0
  344. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  345. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  346. package/src/astro/routes/api/oauth/token.ts +184 -0
  347. package/src/astro/routes/api/openapi.json.ts +32 -0
  348. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
  349. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  350. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  351. package/src/astro/routes/api/redirects/[id].ts +84 -0
  352. package/src/astro/routes/api/redirects/index.ts +52 -0
  353. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  354. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +58 -0
  355. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
  356. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
  357. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
  358. package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
  359. package/src/astro/routes/api/schema/collections/index.ts +47 -0
  360. package/src/astro/routes/api/schema/index.ts +109 -0
  361. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  362. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  363. package/src/astro/routes/api/search/enable.ts +64 -0
  364. package/src/astro/routes/api/search/index.ts +55 -0
  365. package/src/astro/routes/api/search/rebuild.ts +72 -0
  366. package/src/astro/routes/api/search/stats.ts +35 -0
  367. package/src/astro/routes/api/search/suggest.ts +53 -0
  368. package/src/astro/routes/api/sections/[slug].ts +84 -0
  369. package/src/astro/routes/api/sections/index.ts +52 -0
  370. package/src/astro/routes/api/settings/email.ts +150 -0
  371. package/src/astro/routes/api/settings.ts +67 -0
  372. package/src/astro/routes/api/setup/admin-verify.ts +100 -0
  373. package/src/astro/routes/api/setup/admin.ts +94 -0
  374. package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
  375. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  376. package/src/astro/routes/api/setup/index.ts +126 -0
  377. package/src/astro/routes/api/setup/status.ts +122 -0
  378. package/src/astro/routes/api/snapshot.ts +75 -0
  379. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
  380. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
  381. package/src/astro/routes/api/taxonomies/index.ts +59 -0
  382. package/src/astro/routes/api/themes/preview.ts +77 -0
  383. package/src/astro/routes/api/typegen.ts +114 -0
  384. package/src/astro/routes/api/well-known/auth.ts +68 -0
  385. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
  386. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
  387. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +68 -0
  388. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
  389. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
  390. package/src/astro/routes/api/widget-areas/[name].ts +87 -0
  391. package/src/astro/routes/api/widget-areas/index.ts +99 -0
  392. package/src/astro/routes/api/widget-components.ts +22 -0
  393. package/src/astro/routes/robots.txt.ts +77 -0
  394. package/src/astro/routes/sitemap.xml.ts +97 -0
  395. package/src/astro/storage/adapters.ts +74 -0
  396. package/src/astro/storage/index.ts +19 -0
  397. package/src/astro/storage/types.ts +60 -0
  398. package/src/astro/types.ts +346 -0
  399. package/src/auth/api-tokens.ts +25 -0
  400. package/src/auth/challenge-store.ts +80 -0
  401. package/src/auth/mode.ts +96 -0
  402. package/src/auth/oauth-state-store.ts +96 -0
  403. package/src/auth/passkey-config.ts +27 -0
  404. package/src/auth/rate-limit.ts +158 -0
  405. package/src/auth/scopes.ts +33 -0
  406. package/src/auth/types.ts +104 -0
  407. package/src/aws-sdk.d.ts +100 -0
  408. package/src/bylines/index.ts +237 -0
  409. package/src/cleanup.ts +153 -0
  410. package/src/cli/client-factory.ts +100 -0
  411. package/src/cli/commands/auth.ts +46 -0
  412. package/src/cli/commands/bundle-utils.ts +247 -0
  413. package/src/cli/commands/bundle.ts +609 -0
  414. package/src/cli/commands/content.ts +442 -0
  415. package/src/cli/commands/dev.ts +191 -0
  416. package/src/cli/commands/doctor.ts +211 -0
  417. package/src/cli/commands/export-seed.ts +630 -0
  418. package/src/cli/commands/import/wordpress.ts +1056 -0
  419. package/src/cli/commands/init.ts +192 -0
  420. package/src/cli/commands/login.ts +547 -0
  421. package/src/cli/commands/media.ts +165 -0
  422. package/src/cli/commands/menu.ts +67 -0
  423. package/src/cli/commands/plugin-init.ts +291 -0
  424. package/src/cli/commands/plugin-validate.ts +31 -0
  425. package/src/cli/commands/plugin.ts +33 -0
  426. package/src/cli/commands/publish.ts +697 -0
  427. package/src/cli/commands/schema.ts +233 -0
  428. package/src/cli/commands/search-cmd.ts +54 -0
  429. package/src/cli/commands/seed.ts +286 -0
  430. package/src/cli/commands/taxonomy.ts +128 -0
  431. package/src/cli/commands/types.ts +68 -0
  432. package/src/cli/credentials.ts +236 -0
  433. package/src/cli/index.ts +70 -0
  434. package/src/cli/output.ts +75 -0
  435. package/src/cli/wxr/parser.ts +969 -0
  436. package/src/client/cf-access.ts +193 -0
  437. package/src/client/index.ts +854 -0
  438. package/src/client/portable-text.ts +413 -0
  439. package/src/client/transport.ts +200 -0
  440. package/src/comments/moderator.ts +46 -0
  441. package/src/comments/notifications.ts +144 -0
  442. package/src/comments/query.ts +105 -0
  443. package/src/comments/service.ts +213 -0
  444. package/src/components/Break.astro +45 -0
  445. package/src/components/Button.astro +71 -0
  446. package/src/components/Buttons.astro +49 -0
  447. package/src/components/Code.astro +59 -0
  448. package/src/components/Columns.astro +59 -0
  449. package/src/components/CommentForm.astro +315 -0
  450. package/src/components/Comments.astro +232 -0
  451. package/src/components/Cover.astro +128 -0
  452. package/src/components/EmDashBodyEnd.astro +32 -0
  453. package/src/components/EmDashBodyStart.astro +32 -0
  454. package/src/components/EmDashHead.astro +53 -0
  455. package/src/components/EmDashImage.astro +178 -0
  456. package/src/components/EmDashMedia.astro +167 -0
  457. package/src/components/Embed.astro +128 -0
  458. package/src/components/File.astro +122 -0
  459. package/src/components/Gallery.astro +93 -0
  460. package/src/components/HtmlBlock.astro +33 -0
  461. package/src/components/Image.astro +178 -0
  462. package/src/components/InlineEditor.astro +27 -0
  463. package/src/components/InlinePortableTextEditor.tsx +1905 -0
  464. package/src/components/LiveSearch.astro +614 -0
  465. package/src/components/PortableText.astro +51 -0
  466. package/src/components/Pullquote.astro +51 -0
  467. package/src/components/Table.astro +108 -0
  468. package/src/components/WidgetArea.astro +22 -0
  469. package/src/components/WidgetRenderer.astro +72 -0
  470. package/src/components/index.ts +116 -0
  471. package/src/components/marks/Link.astro +31 -0
  472. package/src/components/marks/StrikeThrough.astro +7 -0
  473. package/src/components/marks/Subscript.astro +7 -0
  474. package/src/components/marks/Superscript.astro +7 -0
  475. package/src/components/marks/Underline.astro +7 -0
  476. package/src/components/widgets/Archives.astro +65 -0
  477. package/src/components/widgets/Categories.astro +35 -0
  478. package/src/components/widgets/RecentPosts.astro +51 -0
  479. package/src/components/widgets/Search.astro +18 -0
  480. package/src/components/widgets/Tags.astro +38 -0
  481. package/src/content/converters/index.ts +9 -0
  482. package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
  483. package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
  484. package/src/content/converters/types.ts +120 -0
  485. package/src/content/index.ts +5 -0
  486. package/src/database/connection.ts +67 -0
  487. package/src/database/dialect-helpers.ts +138 -0
  488. package/src/database/index.ts +5 -0
  489. package/src/database/migrations/001_initial.ts +170 -0
  490. package/src/database/migrations/002_media_status.ts +26 -0
  491. package/src/database/migrations/003_schema_registry.ts +79 -0
  492. package/src/database/migrations/004_plugins.ts +62 -0
  493. package/src/database/migrations/005_menus.ts +67 -0
  494. package/src/database/migrations/006_taxonomy_defs.ts +51 -0
  495. package/src/database/migrations/007_widgets.ts +42 -0
  496. package/src/database/migrations/008_auth.ts +194 -0
  497. package/src/database/migrations/009_user_disabled.ts +27 -0
  498. package/src/database/migrations/011_sections.ts +65 -0
  499. package/src/database/migrations/012_search.ts +25 -0
  500. package/src/database/migrations/013_scheduled_publishing.ts +51 -0
  501. package/src/database/migrations/014_draft_revisions.ts +72 -0
  502. package/src/database/migrations/015_indexes.ts +82 -0
  503. package/src/database/migrations/016_api_tokens.ts +89 -0
  504. package/src/database/migrations/017_authorization_codes.ts +45 -0
  505. package/src/database/migrations/018_seo.ts +56 -0
  506. package/src/database/migrations/019_i18n.ts +618 -0
  507. package/src/database/migrations/020_collection_url_pattern.ts +23 -0
  508. package/src/database/migrations/021_remove_section_categories.ts +43 -0
  509. package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
  510. package/src/database/migrations/023_plugin_metadata.ts +33 -0
  511. package/src/database/migrations/024_media_placeholders.ts +32 -0
  512. package/src/database/migrations/025_oauth_clients.ts +28 -0
  513. package/src/database/migrations/026_cron_tasks.ts +49 -0
  514. package/src/database/migrations/027_comments.ts +87 -0
  515. package/src/database/migrations/028_drop_author_url.ts +9 -0
  516. package/src/database/migrations/029_redirects.ts +67 -0
  517. package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
  518. package/src/database/migrations/031_bylines.ts +90 -0
  519. package/src/database/migrations/032_rate_limits.ts +39 -0
  520. package/src/database/migrations/runner.ts +170 -0
  521. package/src/database/repositories/audit.ts +294 -0
  522. package/src/database/repositories/byline.ts +387 -0
  523. package/src/database/repositories/comment.ts +458 -0
  524. package/src/database/repositories/content.ts +1144 -0
  525. package/src/database/repositories/index.ts +30 -0
  526. package/src/database/repositories/media.ts +347 -0
  527. package/src/database/repositories/options.ts +150 -0
  528. package/src/database/repositories/plugin-storage.ts +373 -0
  529. package/src/database/repositories/redirect.ts +480 -0
  530. package/src/database/repositories/revision.ts +200 -0
  531. package/src/database/repositories/seo.ts +176 -0
  532. package/src/database/repositories/taxonomy.ts +294 -0
  533. package/src/database/repositories/types.ts +132 -0
  534. package/src/database/repositories/user.ts +258 -0
  535. package/src/database/transaction.ts +54 -0
  536. package/src/database/types.ts +501 -0
  537. package/src/database/validate.ts +138 -0
  538. package/src/db/adapters.ts +125 -0
  539. package/src/db/index.ts +37 -0
  540. package/src/db/libsql.ts +23 -0
  541. package/src/db/postgres.ts +30 -0
  542. package/src/db/sqlite.ts +27 -0
  543. package/src/emdash-runtime.ts +2096 -0
  544. package/src/fields/boolean.ts +34 -0
  545. package/src/fields/datetime.ts +44 -0
  546. package/src/fields/file.ts +41 -0
  547. package/src/fields/image.ts +34 -0
  548. package/src/fields/index.ts +42 -0
  549. package/src/fields/integer.ts +50 -0
  550. package/src/fields/json.ts +37 -0
  551. package/src/fields/multiselect.ts +48 -0
  552. package/src/fields/number.ts +52 -0
  553. package/src/fields/portable-text.ts +33 -0
  554. package/src/fields/reference.ts +29 -0
  555. package/src/fields/richtext.ts +31 -0
  556. package/src/fields/select.ts +46 -0
  557. package/src/fields/slug.ts +38 -0
  558. package/src/fields/text.ts +55 -0
  559. package/src/fields/textarea.ts +52 -0
  560. package/src/fields/types.ts +64 -0
  561. package/src/i18n/config.ts +68 -0
  562. package/src/import/index.ts +90 -0
  563. package/src/import/menus.ts +436 -0
  564. package/src/import/registry.ts +111 -0
  565. package/src/import/sections.ts +103 -0
  566. package/src/import/settings.ts +281 -0
  567. package/src/import/sources/wordpress-plugin.ts +641 -0
  568. package/src/import/sources/wordpress-rest.ts +191 -0
  569. package/src/import/sources/wxr.ts +330 -0
  570. package/src/import/ssrf.ts +260 -0
  571. package/src/import/types.ts +418 -0
  572. package/src/import/utils.ts +412 -0
  573. package/src/index.ts +481 -0
  574. package/src/loader.ts +770 -0
  575. package/src/mcp/server.ts +1463 -0
  576. package/src/media/index.ts +32 -0
  577. package/src/media/local-runtime.ts +213 -0
  578. package/src/media/local.ts +46 -0
  579. package/src/media/normalize.ts +190 -0
  580. package/src/media/placeholder.ts +150 -0
  581. package/src/media/provider-loader.ts +78 -0
  582. package/src/media/types.ts +279 -0
  583. package/src/menus/index.ts +324 -0
  584. package/src/menus/types.ts +112 -0
  585. package/src/page/context.ts +93 -0
  586. package/src/page/fragments.ts +89 -0
  587. package/src/page/index.ts +58 -0
  588. package/src/page/jsonld.ts +94 -0
  589. package/src/page/metadata.ts +185 -0
  590. package/src/page/seo-contributions.ts +136 -0
  591. package/src/plugin-utils.ts +80 -0
  592. package/src/plugins/adapt-sandbox-entry.ts +207 -0
  593. package/src/plugins/context.ts +833 -0
  594. package/src/plugins/cron.ts +361 -0
  595. package/src/plugins/define-plugin.ts +259 -0
  596. package/src/plugins/email-console.ts +73 -0
  597. package/src/plugins/email.ts +209 -0
  598. package/src/plugins/hooks.ts +1273 -0
  599. package/src/plugins/index.ts +193 -0
  600. package/src/plugins/manager.ts +595 -0
  601. package/src/plugins/manifest-schema.ts +230 -0
  602. package/src/plugins/marketplace.ts +460 -0
  603. package/src/plugins/request-meta.ts +139 -0
  604. package/src/plugins/routes.ts +302 -0
  605. package/src/plugins/sandbox/index.ts +18 -0
  606. package/src/plugins/sandbox/noop.ts +76 -0
  607. package/src/plugins/sandbox/types.ts +173 -0
  608. package/src/plugins/scheduler/node.ts +122 -0
  609. package/src/plugins/scheduler/piggyback.ts +71 -0
  610. package/src/plugins/scheduler/types.ts +27 -0
  611. package/src/plugins/state.ts +208 -0
  612. package/src/plugins/storage-indexes.ts +326 -0
  613. package/src/plugins/storage-query.ts +240 -0
  614. package/src/plugins/types.ts +1284 -0
  615. package/src/preview/helpers.ts +27 -0
  616. package/src/preview/index.ts +40 -0
  617. package/src/preview/tokens.ts +279 -0
  618. package/src/preview/urls.ts +118 -0
  619. package/src/query.ts +674 -0
  620. package/src/redirects/patterns.ts +224 -0
  621. package/src/request-context.ts +67 -0
  622. package/src/runtime.ts +21 -0
  623. package/src/schema/index.ts +29 -0
  624. package/src/schema/query.ts +44 -0
  625. package/src/schema/registry.ts +965 -0
  626. package/src/schema/types.ts +276 -0
  627. package/src/schema/zod-generator.ts +413 -0
  628. package/src/search/fts-manager.ts +452 -0
  629. package/src/search/index.ts +26 -0
  630. package/src/search/query.ts +396 -0
  631. package/src/search/text-extraction.ts +162 -0
  632. package/src/search/types.ts +114 -0
  633. package/src/sections/index.ts +226 -0
  634. package/src/sections/types.ts +86 -0
  635. package/src/seed/apply.ts +1141 -0
  636. package/src/seed/default.ts +86 -0
  637. package/src/seed/index.ts +28 -0
  638. package/src/seed/load.ts +35 -0
  639. package/src/seed/types.ts +341 -0
  640. package/src/seed/validate.ts +642 -0
  641. package/src/seo/index.ts +179 -0
  642. package/src/settings/index.ts +203 -0
  643. package/src/settings/types.ts +58 -0
  644. package/src/storage/index.ts +28 -0
  645. package/src/storage/local.ts +249 -0
  646. package/src/storage/s3.ts +263 -0
  647. package/src/storage/types.ts +204 -0
  648. package/src/taxonomies/index.ts +309 -0
  649. package/src/taxonomies/types.ts +61 -0
  650. package/src/ui.ts +75 -0
  651. package/src/utils/base64.ts +73 -0
  652. package/src/utils/hash.ts +36 -0
  653. package/src/utils/sanitize.ts +20 -0
  654. package/src/utils/slugify.ts +29 -0
  655. package/src/utils/url.ts +48 -0
  656. package/src/virtual-modules.d.ts +111 -0
  657. package/src/visual-editing/editable.ts +108 -0
  658. package/src/visual-editing/toolbar.ts +1229 -0
  659. package/src/widgets/components.ts +105 -0
  660. package/src/widgets/index.ts +131 -0
  661. package/src/widgets/types.ts +81 -0
@@ -0,0 +1,965 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { Selectable } from "kysely";
3
+ import { sql } from "kysely";
4
+ import { ulid } from "ulidx";
5
+
6
+ import { currentTimestamp, listTablesLike, tableExists } from "../database/dialect-helpers.js";
7
+ import { withTransaction } from "../database/transaction.js";
8
+ import type { CollectionTable, Database, FieldTable } from "../database/types.js";
9
+ import { FTSManager } from "../search/fts-manager.js";
10
+ import {
11
+ type Collection,
12
+ type CollectionSource,
13
+ type ColumnType,
14
+ type Field,
15
+ type CreateCollectionInput,
16
+ type UpdateCollectionInput,
17
+ type CreateFieldInput,
18
+ type UpdateFieldInput,
19
+ type CollectionWithFields,
20
+ type FieldType,
21
+ FIELD_TYPE_TO_COLUMN,
22
+ RESERVED_FIELD_SLUGS,
23
+ RESERVED_COLLECTION_SLUGS,
24
+ } from "./types.js";
25
+
26
+ // Regex patterns for schema registry
27
+ const SLUG_VALIDATION_PATTERN = /^[a-z][a-z0-9_]*$/;
28
+ const EC_PREFIX_PATTERN = /^ec_/;
29
+ const SINGLE_QUOTE_PATTERN = /'/g;
30
+ const UNDERSCORE_PATTERN = /_/g;
31
+ const WORD_BOUNDARY_PATTERN = /\b\w/g;
32
+
33
+ /** Valid column types for runtime validation */
34
+ const COLUMN_TYPES: ReadonlySet<string> = new Set(["TEXT", "REAL", "INTEGER", "JSON"]);
35
+
36
+ /** Valid collection source prefixes/values */
37
+ const VALID_SOURCES: ReadonlySet<string> = new Set(["manual", "discovered", "seed"]);
38
+
39
+ function isCollectionSource(value: string): value is CollectionSource {
40
+ return VALID_SOURCES.has(value) || value.startsWith("template:") || value.startsWith("import:");
41
+ }
42
+
43
+ function isFieldType(value: string): value is FieldType {
44
+ return value in FIELD_TYPE_TO_COLUMN;
45
+ }
46
+
47
+ function isColumnType(value: string): value is ColumnType {
48
+ return COLUMN_TYPES.has(value);
49
+ }
50
+
51
+ /**
52
+ * Error thrown when a schema operation fails
53
+ */
54
+ export class SchemaError extends Error {
55
+ constructor(
56
+ message: string,
57
+ public code: string,
58
+ public details?: Record<string, unknown>,
59
+ ) {
60
+ super(message);
61
+ this.name = "SchemaError";
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Schema Registry
67
+ *
68
+ * Manages collection and field definitions stored in D1.
69
+ * Handles runtime DDL operations (CREATE TABLE, ALTER TABLE).
70
+ */
71
+ export class SchemaRegistry {
72
+ constructor(private db: Kysely<Database>) {}
73
+
74
+ // ============================================
75
+ // Collection Operations
76
+ // ============================================
77
+
78
+ /**
79
+ * List all collections
80
+ */
81
+ async listCollections(): Promise<Collection[]> {
82
+ const rows = await this.db
83
+ .selectFrom("_emdash_collections")
84
+ .selectAll()
85
+ .orderBy("slug", "asc")
86
+ .execute();
87
+
88
+ return rows.map(this.mapCollectionRow);
89
+ }
90
+
91
+ /**
92
+ * Get a collection by slug
93
+ */
94
+ async getCollection(slug: string): Promise<Collection | null> {
95
+ const row = await this.db
96
+ .selectFrom("_emdash_collections")
97
+ .where("slug", "=", slug)
98
+ .selectAll()
99
+ .executeTakeFirst();
100
+
101
+ return row ? this.mapCollectionRow(row) : null;
102
+ }
103
+
104
+ /**
105
+ * Get a collection with all its fields
106
+ */
107
+ async getCollectionWithFields(slug: string): Promise<CollectionWithFields | null> {
108
+ const collection = await this.getCollection(slug);
109
+ if (!collection) return null;
110
+
111
+ const fields = await this.listFields(collection.id);
112
+
113
+ return { ...collection, fields };
114
+ }
115
+
116
+ /**
117
+ * Create a new collection
118
+ */
119
+ async createCollection(input: CreateCollectionInput): Promise<Collection> {
120
+ // Validate slug
121
+ this.validateSlug(input.slug, "collection");
122
+ if (RESERVED_COLLECTION_SLUGS.includes(input.slug)) {
123
+ throw new SchemaError(`Collection slug "${input.slug}" is reserved`, "RESERVED_SLUG");
124
+ }
125
+
126
+ // Check if collection already exists
127
+ const existing = await this.getCollection(input.slug);
128
+ if (existing) {
129
+ throw new SchemaError(`Collection "${input.slug}" already exists`, "COLLECTION_EXISTS");
130
+ }
131
+
132
+ const id = ulid();
133
+
134
+ // Insert collection record and create content table in a transaction
135
+ // so a failure in table creation doesn't leave an orphaned row.
136
+ // Uses withTransaction for D1 compatibility (no transaction support).
137
+ // Derive hasSeo from supports array if not explicitly set
138
+ const hasSeo = input.hasSeo ?? input.supports?.includes("seo") ?? false;
139
+
140
+ await withTransaction(this.db, async (trx) => {
141
+ await trx
142
+ .insertInto("_emdash_collections")
143
+ .values({
144
+ id,
145
+ slug: input.slug,
146
+ label: input.label,
147
+ label_singular: input.labelSingular ?? null,
148
+ description: input.description ?? null,
149
+ icon: input.icon ?? null,
150
+ supports: input.supports ? JSON.stringify(input.supports) : null,
151
+ source: input.source ?? "manual",
152
+ has_seo: hasSeo ? 1 : 0,
153
+ comments_enabled: input.commentsEnabled ? 1 : 0,
154
+ url_pattern: input.urlPattern ?? null,
155
+ })
156
+ .execute();
157
+
158
+ // Create the content table for this collection
159
+ await this.createContentTable(input.slug, trx);
160
+ });
161
+
162
+ const collection = await this.getCollection(input.slug);
163
+ if (!collection) {
164
+ throw new SchemaError("Failed to create collection", "CREATE_FAILED");
165
+ }
166
+
167
+ return collection;
168
+ }
169
+
170
+ /**
171
+ * Update a collection
172
+ */
173
+ async updateCollection(slug: string, input: UpdateCollectionInput): Promise<Collection> {
174
+ const existing = await this.getCollection(slug);
175
+ if (!existing) {
176
+ throw new SchemaError(`Collection "${slug}" not found`, "COLLECTION_NOT_FOUND");
177
+ }
178
+
179
+ const now = new Date().toISOString();
180
+
181
+ // Derive hasSeo from supports array if supports is being updated and hasSeo not explicitly set
182
+ const supportsArray = input.supports ?? existing.supports;
183
+ const hasSeo =
184
+ input.hasSeo !== undefined
185
+ ? input.hasSeo
186
+ : input.supports !== undefined
187
+ ? supportsArray.includes("seo")
188
+ : existing.hasSeo;
189
+
190
+ await this.db
191
+ .updateTable("_emdash_collections")
192
+ .set({
193
+ label: input.label ?? existing.label,
194
+ label_singular: input.labelSingular ?? existing.labelSingular ?? null,
195
+ description: input.description ?? existing.description ?? null,
196
+ icon: input.icon ?? existing.icon ?? null,
197
+ supports: input.supports
198
+ ? JSON.stringify(input.supports)
199
+ : JSON.stringify(existing.supports),
200
+ url_pattern:
201
+ input.urlPattern !== undefined
202
+ ? (input.urlPattern ?? null)
203
+ : (existing.urlPattern ?? null),
204
+ has_seo: hasSeo ? 1 : 0,
205
+ comments_enabled:
206
+ input.commentsEnabled !== undefined
207
+ ? input.commentsEnabled
208
+ ? 1
209
+ : 0
210
+ : existing.commentsEnabled
211
+ ? 1
212
+ : 0,
213
+ comments_moderation: input.commentsModeration ?? existing.commentsModeration,
214
+ comments_closed_after_days:
215
+ input.commentsClosedAfterDays !== undefined
216
+ ? input.commentsClosedAfterDays
217
+ : existing.commentsClosedAfterDays,
218
+ comments_auto_approve_users:
219
+ input.commentsAutoApproveUsers !== undefined
220
+ ? input.commentsAutoApproveUsers
221
+ ? 1
222
+ : 0
223
+ : existing.commentsAutoApproveUsers
224
+ ? 1
225
+ : 0,
226
+ updated_at: now,
227
+ })
228
+ .where("slug", "=", slug)
229
+ .execute();
230
+
231
+ const updated = await this.getCollection(slug);
232
+ if (!updated) {
233
+ throw new SchemaError("Failed to update collection", "UPDATE_FAILED");
234
+ }
235
+
236
+ return updated;
237
+ }
238
+
239
+ /**
240
+ * Delete a collection
241
+ */
242
+ async deleteCollection(slug: string, options?: { force?: boolean }): Promise<void> {
243
+ const existing = await this.getCollection(slug);
244
+ if (!existing) {
245
+ throw new SchemaError(`Collection "${slug}" not found`, "COLLECTION_NOT_FOUND");
246
+ }
247
+
248
+ // Check if collection has content
249
+ if (!options?.force) {
250
+ const hasContent = await this.collectionHasContent(slug);
251
+ if (hasContent) {
252
+ throw new SchemaError(
253
+ `Collection "${slug}" has content. Use force: true to delete.`,
254
+ "COLLECTION_HAS_CONTENT",
255
+ );
256
+ }
257
+ }
258
+
259
+ // Drop the content table
260
+ await this.dropContentTable(slug);
261
+
262
+ // Delete the collection record (fields will cascade)
263
+ await this.db.deleteFrom("_emdash_collections").where("id", "=", existing.id).execute();
264
+ }
265
+
266
+ // ============================================
267
+ // Field Operations
268
+ // ============================================
269
+
270
+ /**
271
+ * List fields for a collection
272
+ */
273
+ async listFields(collectionId: string): Promise<Field[]> {
274
+ const rows = await this.db
275
+ .selectFrom("_emdash_fields")
276
+ .where("collection_id", "=", collectionId)
277
+ .selectAll()
278
+ .orderBy("sort_order", "asc")
279
+ .orderBy("created_at", "asc")
280
+ .execute();
281
+
282
+ return rows.map(this.mapFieldRow);
283
+ }
284
+
285
+ /**
286
+ * Get a field by slug within a collection
287
+ */
288
+ async getField(collectionSlug: string, fieldSlug: string): Promise<Field | null> {
289
+ const collection = await this.getCollection(collectionSlug);
290
+ if (!collection) return null;
291
+
292
+ const row = await this.db
293
+ .selectFrom("_emdash_fields")
294
+ .where("collection_id", "=", collection.id)
295
+ .where("slug", "=", fieldSlug)
296
+ .selectAll()
297
+ .executeTakeFirst();
298
+
299
+ return row ? this.mapFieldRow(row) : null;
300
+ }
301
+
302
+ /**
303
+ * Create a new field
304
+ */
305
+ async createField(collectionSlug: string, input: CreateFieldInput): Promise<Field> {
306
+ const collection = await this.getCollection(collectionSlug);
307
+ if (!collection) {
308
+ throw new SchemaError(`Collection "${collectionSlug}" not found`, "COLLECTION_NOT_FOUND");
309
+ }
310
+
311
+ // Validate slug
312
+ this.validateSlug(input.slug, "field");
313
+ if (RESERVED_FIELD_SLUGS.includes(input.slug)) {
314
+ throw new SchemaError(`Field slug "${input.slug}" is reserved`, "RESERVED_SLUG");
315
+ }
316
+
317
+ // Check if field already exists
318
+ const existing = await this.getField(collectionSlug, input.slug);
319
+ if (existing) {
320
+ throw new SchemaError(
321
+ `Field "${input.slug}" already exists in collection "${collectionSlug}"`,
322
+ "FIELD_EXISTS",
323
+ );
324
+ }
325
+
326
+ const id = ulid();
327
+ const columnType = FIELD_TYPE_TO_COLUMN[input.type];
328
+
329
+ // Get max sort order
330
+ const maxSort = await this.db
331
+ .selectFrom("_emdash_fields")
332
+ .where("collection_id", "=", collection.id)
333
+ .select((eb) => eb.fn.max<number>("sort_order").as("max"))
334
+ .executeTakeFirst();
335
+
336
+ const sortOrder = input.sortOrder ?? (maxSort?.max ?? -1) + 1;
337
+
338
+ // Insert field record
339
+ await this.db
340
+ .insertInto("_emdash_fields")
341
+ .values({
342
+ id,
343
+ collection_id: collection.id,
344
+ slug: input.slug,
345
+ label: input.label,
346
+ type: input.type,
347
+ column_type: columnType,
348
+ required: input.required ? 1 : 0,
349
+ unique: input.unique ? 1 : 0,
350
+ default_value: input.defaultValue !== undefined ? JSON.stringify(input.defaultValue) : null,
351
+ validation: input.validation ? JSON.stringify(input.validation) : null,
352
+ widget: input.widget ?? null,
353
+ options: input.options ? JSON.stringify(input.options) : null,
354
+ sort_order: sortOrder,
355
+ searchable: input.searchable ? 1 : 0,
356
+ translatable: input.translatable === false ? 0 : 1,
357
+ })
358
+ .execute();
359
+
360
+ // Add column to content table
361
+ await this.addColumn(collectionSlug, input.slug, input.type, {
362
+ required: input.required,
363
+ defaultValue: input.defaultValue,
364
+ });
365
+
366
+ const field = await this.getField(collectionSlug, input.slug);
367
+ if (!field) {
368
+ throw new SchemaError("Failed to create field", "CREATE_FAILED");
369
+ }
370
+
371
+ return field;
372
+ }
373
+
374
+ /**
375
+ * Update a field
376
+ */
377
+ async updateField(
378
+ collectionSlug: string,
379
+ fieldSlug: string,
380
+ input: UpdateFieldInput,
381
+ ): Promise<Field> {
382
+ const field = await this.getField(collectionSlug, fieldSlug);
383
+ if (!field) {
384
+ throw new SchemaError(
385
+ `Field "${fieldSlug}" not found in collection "${collectionSlug}"`,
386
+ "FIELD_NOT_FOUND",
387
+ );
388
+ }
389
+
390
+ await this.db
391
+ .updateTable("_emdash_fields")
392
+ .set({
393
+ label: input.label ?? field.label,
394
+ required: input.required !== undefined ? (input.required ? 1 : 0) : field.required ? 1 : 0,
395
+ unique: input.unique !== undefined ? (input.unique ? 1 : 0) : field.unique ? 1 : 0,
396
+ searchable:
397
+ input.searchable !== undefined ? (input.searchable ? 1 : 0) : field.searchable ? 1 : 0,
398
+ translatable:
399
+ input.translatable !== undefined
400
+ ? input.translatable
401
+ ? 1
402
+ : 0
403
+ : field.translatable
404
+ ? 1
405
+ : 0,
406
+ default_value:
407
+ input.defaultValue !== undefined
408
+ ? JSON.stringify(input.defaultValue)
409
+ : field.defaultValue !== undefined
410
+ ? JSON.stringify(field.defaultValue)
411
+ : null,
412
+ validation: input.validation
413
+ ? JSON.stringify(input.validation)
414
+ : field.validation
415
+ ? JSON.stringify(field.validation)
416
+ : null,
417
+ widget: input.widget ?? field.widget ?? null,
418
+ options: input.options
419
+ ? JSON.stringify(input.options)
420
+ : field.options
421
+ ? JSON.stringify(field.options)
422
+ : null,
423
+ sort_order: input.sortOrder ?? field.sortOrder,
424
+ })
425
+ .where("id", "=", field.id)
426
+ .execute();
427
+
428
+ const updated = await this.getField(collectionSlug, fieldSlug);
429
+ if (!updated) {
430
+ throw new SchemaError("Failed to update field", "UPDATE_FAILED");
431
+ }
432
+
433
+ // If searchable changed, rebuild the FTS index for this collection
434
+ const searchableChanged =
435
+ input.searchable !== undefined && input.searchable !== field.searchable;
436
+ if (searchableChanged) {
437
+ await this.rebuildSearchIndex(collectionSlug);
438
+ }
439
+
440
+ return updated;
441
+ }
442
+
443
+ /**
444
+ * Rebuild the search index for a collection
445
+ *
446
+ * Called when searchable fields change. If search is enabled for the collection,
447
+ * this will rebuild the FTS table with the updated field list.
448
+ */
449
+ private async rebuildSearchIndex(collectionSlug: string): Promise<void> {
450
+ const ftsManager = new FTSManager(this.db);
451
+
452
+ // Check if search is enabled for this collection
453
+ const config = await ftsManager.getSearchConfig(collectionSlug);
454
+ if (!config?.enabled) {
455
+ // Search not enabled, nothing to do
456
+ return;
457
+ }
458
+
459
+ // Get current searchable fields
460
+ const searchableFields = await ftsManager.getSearchableFields(collectionSlug);
461
+
462
+ if (searchableFields.length === 0) {
463
+ // No searchable fields left, disable search
464
+ await ftsManager.disableSearch(collectionSlug);
465
+ } else {
466
+ // Rebuild the index with updated fields
467
+ await ftsManager.rebuildIndex(collectionSlug, searchableFields, config.weights);
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Delete a field
473
+ */
474
+ async deleteField(collectionSlug: string, fieldSlug: string): Promise<void> {
475
+ const field = await this.getField(collectionSlug, fieldSlug);
476
+ if (!field) {
477
+ throw new SchemaError(
478
+ `Field "${fieldSlug}" not found in collection "${collectionSlug}"`,
479
+ "FIELD_NOT_FOUND",
480
+ );
481
+ }
482
+
483
+ // Drop column from content table
484
+ await this.dropColumn(collectionSlug, fieldSlug);
485
+
486
+ // Delete field record
487
+ await this.db.deleteFrom("_emdash_fields").where("id", "=", field.id).execute();
488
+ }
489
+
490
+ /**
491
+ * Reorder fields
492
+ */
493
+ async reorderFields(collectionSlug: string, fieldSlugs: string[]): Promise<void> {
494
+ const collection = await this.getCollection(collectionSlug);
495
+ if (!collection) {
496
+ throw new SchemaError(`Collection "${collectionSlug}" not found`, "COLLECTION_NOT_FOUND");
497
+ }
498
+
499
+ // Update sort_order for each field
500
+ for (let i = 0; i < fieldSlugs.length; i++) {
501
+ await this.db
502
+ .updateTable("_emdash_fields")
503
+ .set({ sort_order: i })
504
+ .where("collection_id", "=", collection.id)
505
+ .where("slug", "=", fieldSlugs[i])
506
+ .execute();
507
+ }
508
+ }
509
+
510
+ // ============================================
511
+ // DDL Operations
512
+ // ============================================
513
+
514
+ /**
515
+ * Create a content table for a collection
516
+ */
517
+ private async createContentTable(slug: string, db?: Kysely<Database>): Promise<void> {
518
+ const conn = db ?? this.db;
519
+ const tableName = this.getTableName(slug);
520
+
521
+ await conn.schema
522
+ .createTable(tableName)
523
+ .addColumn("id", "text", (col) => col.primaryKey())
524
+ .addColumn("slug", "text")
525
+ .addColumn("status", "text", (col) => col.defaultTo("draft"))
526
+ .addColumn("author_id", "text")
527
+ .addColumn("primary_byline_id", "text")
528
+ .addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(conn)))
529
+ .addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(conn)))
530
+ .addColumn("published_at", "text")
531
+ .addColumn("scheduled_at", "text")
532
+ .addColumn("deleted_at", "text")
533
+ .addColumn("version", "integer", (col) => col.defaultTo(1))
534
+ .addColumn("live_revision_id", "text", (col) => col.references("revisions.id"))
535
+ .addColumn("draft_revision_id", "text", (col) => col.references("revisions.id"))
536
+ .addColumn("locale", "text", (col) => col.notNull().defaultTo("en"))
537
+ .addColumn("translation_group", "text")
538
+ .addUniqueConstraint(`${tableName}_slug_locale_unique`, ["slug", "locale"])
539
+ .execute();
540
+
541
+ // Create standard indexes
542
+ await sql`
543
+ CREATE INDEX ${sql.ref(`idx_${tableName}_status`)}
544
+ ON ${sql.ref(tableName)} (status)
545
+ `.execute(conn);
546
+
547
+ await sql`
548
+ CREATE INDEX ${sql.ref(`idx_${tableName}_slug`)}
549
+ ON ${sql.ref(tableName)} (slug)
550
+ `.execute(conn);
551
+
552
+ await sql`
553
+ CREATE INDEX ${sql.ref(`idx_${tableName}_created`)}
554
+ ON ${sql.ref(tableName)} (created_at)
555
+ `.execute(conn);
556
+
557
+ await sql`
558
+ CREATE INDEX ${sql.ref(`idx_${tableName}_deleted`)}
559
+ ON ${sql.ref(tableName)} (deleted_at)
560
+ `.execute(conn);
561
+
562
+ await sql`
563
+ CREATE INDEX ${sql.ref(`idx_${tableName}_scheduled`)}
564
+ ON ${sql.ref(tableName)} (scheduled_at)
565
+ WHERE scheduled_at IS NOT NULL
566
+ `.execute(conn);
567
+
568
+ await sql`
569
+ CREATE INDEX ${sql.ref(`idx_${tableName}_live_revision`)}
570
+ ON ${sql.ref(tableName)} (live_revision_id)
571
+ `.execute(conn);
572
+
573
+ await sql`
574
+ CREATE INDEX ${sql.ref(`idx_${tableName}_draft_revision`)}
575
+ ON ${sql.ref(tableName)} (draft_revision_id)
576
+ `.execute(conn);
577
+
578
+ await sql`
579
+ CREATE INDEX ${sql.ref(`idx_${tableName}_author`)}
580
+ ON ${sql.ref(tableName)} (author_id)
581
+ `.execute(conn);
582
+
583
+ await sql`
584
+ CREATE INDEX ${sql.ref(`idx_${tableName}_primary_byline`)}
585
+ ON ${sql.ref(tableName)} (primary_byline_id)
586
+ `.execute(conn);
587
+
588
+ await sql`
589
+ CREATE INDEX ${sql.ref(`idx_${tableName}_updated`)}
590
+ ON ${sql.ref(tableName)} (updated_at)
591
+ `.execute(conn);
592
+
593
+ await sql`
594
+ CREATE INDEX ${sql.ref(`idx_${tableName}_locale`)}
595
+ ON ${sql.ref(tableName)} (locale)
596
+ `.execute(conn);
597
+
598
+ await sql`
599
+ CREATE INDEX ${sql.ref(`idx_${tableName}_translation_group`)}
600
+ ON ${sql.ref(tableName)} (translation_group)
601
+ `.execute(conn);
602
+ }
603
+
604
+ /**
605
+ * Drop a content table
606
+ */
607
+ private async dropContentTable(slug: string): Promise<void> {
608
+ const tableName = this.getTableName(slug);
609
+ await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(this.db);
610
+ }
611
+
612
+ /**
613
+ * Add a column to a content table
614
+ */
615
+ private async addColumn(
616
+ collectionSlug: string,
617
+ fieldSlug: string,
618
+ fieldType: FieldType,
619
+ options?: { required?: boolean; defaultValue?: unknown },
620
+ ): Promise<void> {
621
+ const tableName = this.getTableName(collectionSlug);
622
+ const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
623
+ const columnName = this.getColumnName(fieldSlug);
624
+
625
+ // Build ALTER TABLE statement
626
+ // Note: SQLite requires DEFAULT for NOT NULL columns in ALTER TABLE
627
+ if (options?.required && options?.defaultValue !== undefined) {
628
+ const defaultVal = this.formatDefaultValue(options.defaultValue, fieldType);
629
+ await sql`
630
+ ALTER TABLE ${sql.ref(tableName)}
631
+ ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
632
+ `.execute(this.db);
633
+ } else if (options?.required) {
634
+ // For required fields without default, use empty string/0 as default
635
+ const defaultVal = this.getEmptyDefault(fieldType);
636
+ await sql`
637
+ ALTER TABLE ${sql.ref(tableName)}
638
+ ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
639
+ `.execute(this.db);
640
+ } else {
641
+ await sql`
642
+ ALTER TABLE ${sql.ref(tableName)}
643
+ ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)}
644
+ `.execute(this.db);
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Drop a column from a content table
650
+ */
651
+ private async dropColumn(collectionSlug: string, fieldSlug: string): Promise<void> {
652
+ const tableName = this.getTableName(collectionSlug);
653
+ const columnName = this.getColumnName(fieldSlug);
654
+
655
+ await sql`
656
+ ALTER TABLE ${sql.ref(tableName)}
657
+ DROP COLUMN ${sql.ref(columnName)}
658
+ `.execute(this.db);
659
+ }
660
+
661
+ // ============================================
662
+ // Helpers
663
+ // ============================================
664
+
665
+ /**
666
+ * Check if a collection has any content
667
+ */
668
+ private async collectionHasContent(slug: string): Promise<boolean> {
669
+ const tableName = this.getTableName(slug);
670
+ try {
671
+ const result = await sql<{ count: number }>`
672
+ SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
673
+ WHERE deleted_at IS NULL
674
+ `.execute(this.db);
675
+ return (result.rows[0]?.count ?? 0) > 0;
676
+ } catch {
677
+ // Table might not exist
678
+ return false;
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Get table name for a collection
684
+ */
685
+ private getTableName(slug: string): string {
686
+ return `ec_${slug}`;
687
+ }
688
+
689
+ /**
690
+ * Get column name for a field
691
+ */
692
+ private getColumnName(slug: string): string {
693
+ return slug;
694
+ }
695
+
696
+ /**
697
+ * Validate a slug
698
+ */
699
+ private validateSlug(slug: string, type: "collection" | "field"): void {
700
+ if (!slug || typeof slug !== "string") {
701
+ throw new SchemaError(`${type} slug is required`, "INVALID_SLUG");
702
+ }
703
+
704
+ if (!SLUG_VALIDATION_PATTERN.test(slug)) {
705
+ throw new SchemaError(
706
+ `${type} slug must start with a letter and contain only lowercase letters, numbers, and underscores`,
707
+ "INVALID_SLUG",
708
+ );
709
+ }
710
+
711
+ if (slug.length > 63) {
712
+ throw new SchemaError(`${type} slug must be 63 characters or less`, "INVALID_SLUG");
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Format a default value for SQL.
718
+ *
719
+ * SQLite `ALTER TABLE ADD COLUMN ... DEFAULT` requires a literal constant
720
+ * expression — parameterized values cannot be used here. We manually escape
721
+ * single quotes and coerce types to ensure the output is safe.
722
+ *
723
+ * INTEGER/REAL values are coerced through `Number()` which can only produce
724
+ * digits, `.`, `-`, `e`, `Infinity`, or `NaN` — all safe in SQL.
725
+ * TEXT/JSON values have single quotes escaped via SQL standard doubling (`''`).
726
+ */
727
+ private formatDefaultValue(value: unknown, fieldType: FieldType): string {
728
+ if (value === null || value === undefined) {
729
+ return "NULL";
730
+ }
731
+
732
+ const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
733
+
734
+ if (columnType === "JSON") {
735
+ // JSON.stringify produces valid JSON; escape single quotes for SQL literal
736
+ const json = JSON.stringify(value);
737
+ return `'${json.replace(SINGLE_QUOTE_PATTERN, "''")}'`;
738
+ }
739
+
740
+ if (columnType === "INTEGER") {
741
+ if (typeof value === "boolean") {
742
+ return value ? "1" : "0";
743
+ }
744
+ const num = Number(value);
745
+ if (!Number.isFinite(num)) {
746
+ return "0";
747
+ }
748
+ return String(Math.trunc(num));
749
+ }
750
+
751
+ if (columnType === "REAL") {
752
+ const num = Number(value);
753
+ if (!Number.isFinite(num)) {
754
+ return "0";
755
+ }
756
+ return String(num);
757
+ }
758
+
759
+ // TEXT — escape single quotes via SQL standard doubling
760
+ let text: string;
761
+ if (typeof value === "string") {
762
+ text = value;
763
+ } else if (typeof value === "number" || typeof value === "boolean") {
764
+ text = String(value);
765
+ } else if (typeof value === "object" && value !== null) {
766
+ text = JSON.stringify(value);
767
+ } else {
768
+ text = "";
769
+ }
770
+ return `'${text.replace(SINGLE_QUOTE_PATTERN, "''")}'`;
771
+ }
772
+
773
+ /**
774
+ * Get empty default for a field type
775
+ */
776
+ private getEmptyDefault(fieldType: FieldType): string {
777
+ const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
778
+
779
+ switch (columnType) {
780
+ case "INTEGER":
781
+ return "0";
782
+ case "REAL":
783
+ return "0.0";
784
+ case "JSON":
785
+ return "'null'";
786
+ default:
787
+ return "''";
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Map a collection row to a Collection object
793
+ */
794
+ private mapCollectionRow = (row: Selectable<CollectionTable>): Collection => {
795
+ const moderation = row.comments_moderation;
796
+ return {
797
+ id: row.id,
798
+ slug: row.slug,
799
+ label: row.label,
800
+ labelSingular: row.label_singular ?? undefined,
801
+ description: row.description ?? undefined,
802
+ icon: row.icon ?? undefined,
803
+ supports: row.supports ? JSON.parse(row.supports) : [],
804
+ source: row.source && isCollectionSource(row.source) ? row.source : undefined,
805
+ hasSeo: row.has_seo === 1,
806
+ urlPattern: row.url_pattern ?? undefined,
807
+ commentsEnabled: row.comments_enabled === 1,
808
+ commentsModeration:
809
+ moderation === "all" || moderation === "first_time" || moderation === "none"
810
+ ? moderation
811
+ : "first_time",
812
+ commentsClosedAfterDays: row.comments_closed_after_days ?? 90,
813
+ commentsAutoApproveUsers: row.comments_auto_approve_users === 1,
814
+ createdAt: row.created_at,
815
+ updatedAt: row.updated_at,
816
+ };
817
+ };
818
+
819
+ /**
820
+ * Map a field row to a Field object
821
+ */
822
+ private mapFieldRow = (row: Selectable<FieldTable>): Field => {
823
+ return {
824
+ id: row.id,
825
+ collectionId: row.collection_id,
826
+ slug: row.slug,
827
+ label: row.label,
828
+ type: isFieldType(row.type) ? row.type : "string",
829
+ columnType: isColumnType(row.column_type) ? row.column_type : "TEXT",
830
+ required: row.required === 1,
831
+ unique: row.unique === 1,
832
+ defaultValue: row.default_value ? JSON.parse(row.default_value) : undefined,
833
+ validation: row.validation ? JSON.parse(row.validation) : undefined,
834
+ widget: row.widget ?? undefined,
835
+ options: row.options ? JSON.parse(row.options) : undefined,
836
+ sortOrder: row.sort_order,
837
+ searchable: row.searchable === 1,
838
+ translatable: row.translatable !== 0,
839
+ createdAt: row.created_at,
840
+ };
841
+ };
842
+
843
+ // ============================================
844
+ // Discovery
845
+ // ============================================
846
+
847
+ /**
848
+ * Discover orphaned content tables
849
+ *
850
+ * Finds ec_* tables that exist in the database but don't have a
851
+ * corresponding entry in _emdash_collections.
852
+ */
853
+ async discoverOrphanedTables(): Promise<
854
+ Array<{ slug: string; tableName: string; rowCount: number }>
855
+ > {
856
+ // Get all ec_* tables
857
+ // Content tables are ec_* (e.g., ec_posts, ec_pages)
858
+ // Internal tables are _emdash_* (e.g., _emdash_collections, _emdash_fts_posts)
859
+ const allTables = await listTablesLike(this.db, "ec_%");
860
+
861
+ // Get registered collections
862
+ const registered = await this.listCollections();
863
+ const registeredSlugs = new Set(registered.map((c) => c.slug));
864
+
865
+ // Find orphans
866
+ const orphans: Array<{
867
+ slug: string;
868
+ tableName: string;
869
+ rowCount: number;
870
+ }> = [];
871
+
872
+ for (const tableName of allTables) {
873
+ const slug = tableName.replace(EC_PREFIX_PATTERN, "");
874
+
875
+ if (!registeredSlugs.has(slug)) {
876
+ // Count rows in the orphaned table
877
+ try {
878
+ const countResult = await sql<{ count: number }>`
879
+ SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
880
+ WHERE deleted_at IS NULL
881
+ `.execute(this.db);
882
+
883
+ orphans.push({
884
+ slug,
885
+ tableName,
886
+ rowCount: countResult.rows[0]?.count ?? 0,
887
+ });
888
+ } catch {
889
+ // Table might have unexpected schema, still report it
890
+ orphans.push({
891
+ slug,
892
+ tableName,
893
+ rowCount: 0,
894
+ });
895
+ }
896
+ }
897
+ }
898
+
899
+ return orphans;
900
+ }
901
+
902
+ /**
903
+ * Register an orphaned table as a collection
904
+ *
905
+ * Creates a _emdash_collections entry for an existing ec_* table.
906
+ */
907
+ async registerOrphanedTable(
908
+ slug: string,
909
+ options?: {
910
+ label?: string;
911
+ labelSingular?: string;
912
+ description?: string;
913
+ },
914
+ ): Promise<Collection> {
915
+ // Verify table exists
916
+ const tableName = this.getTableName(slug);
917
+ const exists = await tableExists(this.db, tableName);
918
+
919
+ if (!exists) {
920
+ throw new SchemaError(`Table "${tableName}" does not exist`, "TABLE_NOT_FOUND");
921
+ }
922
+
923
+ // Check if already registered
924
+ const existing = await this.getCollection(slug);
925
+ if (existing) {
926
+ throw new SchemaError(`Collection "${slug}" is already registered`, "COLLECTION_EXISTS");
927
+ }
928
+
929
+ // Create collection entry
930
+ const id = ulid();
931
+ const label = options?.label || this.slugToLabel(slug);
932
+
933
+ await this.db
934
+ .insertInto("_emdash_collections")
935
+ .values({
936
+ id,
937
+ slug,
938
+ label,
939
+ label_singular: options?.labelSingular ?? null,
940
+ description: options?.description ?? null,
941
+ icon: null,
942
+ supports: JSON.stringify([]),
943
+ source: "discovered",
944
+ has_seo: 0,
945
+ url_pattern: null,
946
+ })
947
+ .execute();
948
+
949
+ const collection = await this.getCollection(slug);
950
+ if (!collection) {
951
+ throw new SchemaError("Failed to register orphaned table", "REGISTER_FAILED");
952
+ }
953
+
954
+ return collection;
955
+ }
956
+
957
+ /**
958
+ * Convert slug to human-readable label
959
+ */
960
+ private slugToLabel(slug: string): string {
961
+ return slug
962
+ .replace(UNDERSCORE_PATTERN, " ")
963
+ .replace(WORD_BOUNDARY_PATTERN, (c) => c.toUpperCase());
964
+ }
965
+ }