emdash 0.0.0-a → 0.0.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 (661) hide show
  1. package/README.md +87 -1
  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 +1333 -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-B-u2F2b6.mjs +1412 -0
  127. package/dist/runner-B-u2F2b6.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 +687 -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 +353 -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 +328 -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 +120 -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 +117 -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 +105 -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 +62 -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 +72 -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 +699 -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 +288 -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 +136 -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 +42 -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 +253 -0
  646. package/src/storage/s3.ts +271 -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,56 @@
1
+ import type { Kysely } from "kysely";
2
+ import { sql } from "kysely";
3
+
4
+ import { currentTimestamp } from "../dialect-helpers.js";
5
+
6
+ /**
7
+ * Migration: SEO support
8
+ *
9
+ * Creates:
10
+ * - `_emdash_seo` table: per-content SEO metadata (separate from content tables)
11
+ * - `has_seo` column on `_emdash_collections`: opt-in flag per collection
12
+ *
13
+ * SEO is not a universal concern — only collections representing web pages
14
+ * need it. The `has_seo` flag controls whether the admin shows SEO fields
15
+ * and whether the collection's content appears in sitemaps.
16
+ */
17
+ export async function up(db: Kysely<unknown>): Promise<void> {
18
+ // Create the SEO table
19
+ await db.schema
20
+ .createTable("_emdash_seo")
21
+ .addColumn("collection", "text", (col) => col.notNull())
22
+ .addColumn("content_id", "text", (col) => col.notNull())
23
+ .addColumn("seo_title", "text")
24
+ .addColumn("seo_description", "text")
25
+ .addColumn("seo_image", "text")
26
+ .addColumn("seo_canonical", "text")
27
+ .addColumn("seo_no_index", "integer", (col) => col.notNull().defaultTo(0))
28
+ .addColumn("created_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db)))
29
+ .addColumn("updated_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db)))
30
+ .addPrimaryKeyConstraint("_emdash_seo_pk", ["collection", "content_id"])
31
+ .execute();
32
+
33
+ // Index for batch lookups by collection (PK covers point lookups).
34
+ // Sitemap queries join on (collection, content_id) which the PK covers,
35
+ // and filter seo_no_index. This index supports getMany() batch queries.
36
+ await sql`
37
+ CREATE INDEX idx_emdash_seo_collection
38
+ ON _emdash_seo (collection)
39
+ `.execute(db);
40
+
41
+ // Add has_seo flag to collections
42
+ await sql`
43
+ ALTER TABLE _emdash_collections
44
+ ADD COLUMN has_seo INTEGER NOT NULL DEFAULT 0
45
+ `.execute(db);
46
+ }
47
+
48
+ export async function down(db: Kysely<unknown>): Promise<void> {
49
+ await sql`DROP TABLE IF EXISTS _emdash_seo`.execute(db);
50
+
51
+ // SQLite doesn't support DROP COLUMN before 3.35.0, but D1 does
52
+ await sql`
53
+ ALTER TABLE _emdash_collections
54
+ DROP COLUMN has_seo
55
+ `.execute(db);
56
+ }
@@ -0,0 +1,618 @@
1
+ import type { Kysely } from "kysely";
2
+ import { sql } from "kysely";
3
+
4
+ import { isSqlite, listTablesLike } from "../dialect-helpers.js";
5
+ import { validateIdentifier } from "../validate.js";
6
+
7
+ /**
8
+ * Migration: i18n support (row-per-locale model)
9
+ *
10
+ * Each piece of content can exist in multiple locales. Translations of the
11
+ * same content share a `translation_group` ULID while each row carries its
12
+ * own `locale` code. Slugs are unique per-locale, not globally.
13
+ *
14
+ * Changes:
15
+ * 1. For every ec_* content table:
16
+ * - Rebuild the table to replace inline `slug TEXT UNIQUE` with
17
+ * `slug TEXT` + a compound `UNIQUE(slug, locale)` constraint.
18
+ * - Add `locale TEXT NOT NULL DEFAULT 'en'`
19
+ * - Add `translation_group TEXT`
20
+ * - Backfill `translation_group = id` for existing rows
21
+ * - Recreate all standard indexes plus new locale/translation_group indexes
22
+ *
23
+ * 2. Add `translatable` column to `_emdash_fields`
24
+ *
25
+ * The table-rebuild approach is required because SQLite cannot drop an inline
26
+ * UNIQUE constraint via ALTER TABLE. We use PRAGMA table_info to discover all
27
+ * columns (including dynamic user-defined fields) and rebuild dynamically.
28
+ */
29
+
30
+ // Column info returned by PRAGMA table_info
31
+ interface ColumnInfo {
32
+ cid: number;
33
+ name: string;
34
+ type: string;
35
+ notnull: number;
36
+ dflt_value: string | null;
37
+ pk: number;
38
+ }
39
+
40
+ // Index info returned by PRAGMA index_list
41
+ interface IndexInfo {
42
+ seq: number;
43
+ name: string;
44
+ unique: number;
45
+ origin: string;
46
+ partial: number;
47
+ }
48
+
49
+ // Index column info returned by PRAGMA index_info
50
+ interface IndexColumnInfo {
51
+ seqno: number;
52
+ cid: number;
53
+ name: string;
54
+ }
55
+
56
+ /**
57
+ * Quote an identifier for use in raw SQL. Escapes embedded double-quotes
58
+ * per SQL standard (double them). The name should first pass
59
+ * validateIdentifier() or validateTableName() for defense-in-depth.
60
+ */
61
+ const DOUBLE_QUOTE_RE = /"/g;
62
+ function quoteIdent(name: string): string {
63
+ return `"${name.replace(DOUBLE_QUOTE_RE, '""')}"`;
64
+ }
65
+
66
+ /** Suffix added to tmp tables during i18n migration rebuild. */
67
+ const I18N_TMP_SUFFIX = /_i18n_tmp$/;
68
+
69
+ /** Table names from sqlite_master are ec_{slug} — validate the pattern. */
70
+ const TABLE_NAME_PATTERN = /^ec_[a-z][a-z0-9_]*$/;
71
+ function validateTableName(name: string): void {
72
+ if (!TABLE_NAME_PATTERN.test(name)) {
73
+ throw new Error(`Invalid content table name: "${name}"`);
74
+ }
75
+ }
76
+
77
+ /** SQLite column types produced by EmDash's schema registry. */
78
+ const ALLOWED_COLUMN_TYPES = new Set(["TEXT", "INTEGER", "REAL", "BLOB", "JSON", "NUMERIC", ""]);
79
+ function validateColumnType(type: string, colName: string): void {
80
+ if (!ALLOWED_COLUMN_TYPES.has(type.toUpperCase())) {
81
+ throw new Error(`Unexpected column type "${type}" for column "${colName}"`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Validate that a default value expression from PRAGMA table_info is safe
87
+ * to interpolate into DDL. Allows: string literals, numeric literals,
88
+ * NULL, and known function calls like datetime('now').
89
+ *
90
+ * Note: PRAGMA table_info strips the outer parens from expression defaults,
91
+ * so `DEFAULT (datetime('now'))` is reported as `datetime('now')`.
92
+ * We accept both forms and re-wrap in parens via normalizeDdlDefault().
93
+ */
94
+ const SAFE_DEFAULT_PATTERN =
95
+ /^(?:'[^']*'|NULL|-?\d+(?:\.\d+)?|\(?datetime\('now'\)\)?|\(?json\('[^']*'\)\)?|0|1)$/i;
96
+ function validateDefaultValue(value: string, colName: string): void {
97
+ if (!SAFE_DEFAULT_PATTERN.test(value)) {
98
+ throw new Error(`Unexpected default value "${value}" for column "${colName}"`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Normalize a PRAGMA table_info default value for use in DDL.
104
+ * Function-call defaults (e.g. `datetime('now')`) must be wrapped in parens
105
+ * to form valid expression defaults: `DEFAULT (datetime('now'))`.
106
+ * PRAGMA strips the outer parens, so we re-add them here.
107
+ */
108
+ const FUNCTION_DEFAULT_PATTERN = /^(?:datetime|json)\(/i;
109
+ function normalizeDdlDefault(value: string): string {
110
+ // Already wrapped in parens — return as-is
111
+ if (value.startsWith("(")) return value;
112
+ if (FUNCTION_DEFAULT_PATTERN.test(value)) return `(${value})`;
113
+ return value;
114
+ }
115
+
116
+ /**
117
+ * Validate that a CREATE INDEX statement from sqlite_master is safe to replay.
118
+ * Must start with CREATE [UNIQUE] INDEX and not contain suspicious patterns.
119
+ */
120
+ const CREATE_INDEX_PATTERN = /^CREATE\s+(UNIQUE\s+)?INDEX\s+/i;
121
+ function validateCreateIndexSql(sqlStr: string, idxName: string): void {
122
+ if (!CREATE_INDEX_PATTERN.test(sqlStr)) {
123
+ throw new Error(`Unexpected index SQL for "${idxName}": does not match CREATE INDEX pattern`);
124
+ }
125
+ // Reject semicolons which could allow statement injection
126
+ if (sqlStr.includes(";")) {
127
+ throw new Error(`Unexpected index SQL for "${idxName}": contains semicolon`);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * PostgreSQL path: ALTER TABLE supports ADD COLUMN and DROP CONSTRAINT directly.
133
+ * No table rebuild needed.
134
+ */
135
+ async function upPostgres(db: Kysely<unknown>): Promise<void> {
136
+ const tableNames = await listTablesLike(db, "ec_%");
137
+
138
+ for (const t of tableNames) {
139
+ validateTableName(t);
140
+
141
+ // Check if already migrated (idempotency)
142
+ const hasLocale = await sql<{ exists: boolean }>`
143
+ SELECT EXISTS(
144
+ SELECT 1 FROM information_schema.columns
145
+ WHERE table_schema = 'public' AND table_name = ${t} AND column_name = 'locale'
146
+ ) as exists
147
+ `.execute(db);
148
+ if (hasLocale.rows[0]?.exists === true) continue;
149
+
150
+ // Add i18n columns
151
+ await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN locale TEXT NOT NULL DEFAULT 'en'`.execute(db);
152
+ await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN translation_group TEXT`.execute(db);
153
+
154
+ // Drop existing unique constraint on slug (Postgres auto-names these)
155
+ // Find the constraint name first
156
+ const constraints = await sql<{ conname: string }>`
157
+ SELECT conname FROM pg_constraint
158
+ WHERE conrelid = ${t}::regclass
159
+ AND contype = 'u'
160
+ AND array_length(conkey, 1) = 1
161
+ AND conkey[1] = (
162
+ SELECT attnum FROM pg_attribute
163
+ WHERE attrelid = ${t}::regclass AND attname = 'slug'
164
+ )
165
+ `.execute(db);
166
+
167
+ for (const c of constraints.rows) {
168
+ await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT ${sql.ref(c.conname)}`.execute(db);
169
+ }
170
+
171
+ // Add compound unique constraint
172
+ await sql`
173
+ ALTER TABLE ${sql.ref(t)}
174
+ ADD CONSTRAINT ${sql.ref(`${t}_slug_locale_unique`)} UNIQUE (slug, locale)
175
+ `.execute(db);
176
+
177
+ // Backfill translation_group
178
+ await sql`UPDATE ${sql.ref(t)} SET translation_group = id`.execute(db);
179
+
180
+ // Create indexes
181
+ await sql`CREATE INDEX ${sql.ref(`idx_${t}_locale`)} ON ${sql.ref(t)} (locale)`.execute(db);
182
+ await sql`
183
+ CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)}
184
+ ON ${sql.ref(t)} (translation_group)
185
+ `.execute(db);
186
+ }
187
+
188
+ // Add translatable flag to fields table
189
+ const hasTranslatable = await sql<{ exists: boolean }>`
190
+ SELECT EXISTS(
191
+ SELECT 1 FROM information_schema.columns
192
+ WHERE table_schema = 'public' AND table_name = '_emdash_fields' AND column_name = 'translatable'
193
+ ) as exists
194
+ `.execute(db);
195
+ if (hasTranslatable.rows[0]?.exists !== true) {
196
+ await sql`
197
+ ALTER TABLE _emdash_fields
198
+ ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
199
+ `.execute(db);
200
+ }
201
+ }
202
+
203
+ export async function up(db: Kysely<unknown>): Promise<void> {
204
+ if (!isSqlite(db)) {
205
+ return upPostgres(db);
206
+ }
207
+
208
+ // Clean up orphaned tmp tables from a previous partial run.
209
+ // On D1 (no transactions), a crash mid-migration can leave these behind.
210
+ const orphanedTmps = await listTablesLike(db, "ec_%_i18n_tmp");
211
+ for (const tmpName of orphanedTmps) {
212
+ validateTableName(tmpName.replace(I18N_TMP_SUFFIX, ""));
213
+ await sql`DROP TABLE IF EXISTS ${sql.ref(tmpName)}`.execute(db);
214
+ }
215
+
216
+ // Discover all ec_* content tables
217
+ const tableNames = await listTablesLike(db, "ec_%");
218
+ const tables = { rows: tableNames.map((name) => ({ name })) };
219
+
220
+ for (const table of tables.rows) {
221
+ const t = table.name;
222
+ validateTableName(t);
223
+ const tmp = `${t}_i18n_tmp`;
224
+
225
+ // Note: no transaction wrapper — D1 doesn't support transactions,
226
+ // and SQLite/better-sqlite3 is single-writer so crash-safety is
227
+ // handled by the journal mode. The tmp table approach is already
228
+ // crash-recoverable (orphaned tmp tables are harmless).
229
+ {
230
+ const trx = db;
231
+ // ── 1. Read existing column definitions ──────────────────────
232
+ const colResult = await sql<ColumnInfo>`
233
+ PRAGMA table_info(${sql.ref(t)})
234
+ `.execute(trx);
235
+ const columns = colResult.rows;
236
+
237
+ // ── Idempotency: skip tables already migrated ───────────────
238
+ // On D1, the migrator can't use transactions, so a partially-
239
+ // applied migration may not be recorded. If the table already
240
+ // has a `locale` column, it was already rebuilt — skip it.
241
+ if (columns.some((col) => col.name === "locale")) {
242
+ continue;
243
+ }
244
+
245
+ // ── 2. Read existing indexes (to recreate after rebuild) ─────
246
+ const idxResult = await sql<IndexInfo>`
247
+ PRAGMA index_list(${sql.ref(t)})
248
+ `.execute(trx);
249
+
250
+ // Collect non-autoindex, non-primary-key indexes for recreation
251
+ const indexDefs: { name: string; unique: boolean; columns: string[]; partial: number }[] = [];
252
+ for (const idx of idxResult.rows) {
253
+ // Skip autoindexes (created by inline UNIQUE) — we're removing them
254
+ if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
255
+
256
+ const idxColResult = await sql<IndexColumnInfo>`
257
+ PRAGMA index_info(${sql.ref(idx.name)})
258
+ `.execute(trx);
259
+
260
+ indexDefs.push({
261
+ name: idx.name,
262
+ unique: idx.unique === 1,
263
+ columns: idxColResult.rows.map((c) => c.name),
264
+ partial: idx.partial,
265
+ });
266
+ }
267
+
268
+ // For partial indexes we need the original CREATE statement
269
+ const partialSqls = new Map<string, string>();
270
+ for (const idx of indexDefs) {
271
+ if (idx.partial) {
272
+ const createResult = await sql<{ sql: string }>`
273
+ SELECT sql FROM sqlite_master
274
+ WHERE type = 'index' AND name = ${idx.name}
275
+ `.execute(trx);
276
+ if (createResult.rows[0]?.sql) {
277
+ partialSqls.set(idx.name, createResult.rows[0].sql);
278
+ }
279
+ }
280
+ }
281
+
282
+ // ── 3. Build column defs for the new table ──────────────────
283
+ // Validate all column names from PRAGMA before using them in raw SQL.
284
+ // These originate from our own schema, but defense-in-depth matters.
285
+ for (const col of columns) {
286
+ validateIdentifier(col.name, "column name");
287
+ }
288
+
289
+ // Replace slug's inline UNIQUE with a table-level UNIQUE(slug, locale)
290
+ const colDefs: string[] = [];
291
+ const colNames: string[] = [];
292
+
293
+ for (const col of columns) {
294
+ validateColumnType(col.type || "TEXT", col.name);
295
+ colNames.push(quoteIdent(col.name));
296
+ let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
297
+
298
+ if (col.pk) {
299
+ def += " PRIMARY KEY";
300
+ } else if (col.name === "slug") {
301
+ // Intentionally omit UNIQUE — compound unique below
302
+ } else {
303
+ if (col.notnull) def += " NOT NULL";
304
+ }
305
+
306
+ if (col.dflt_value !== null) {
307
+ validateDefaultValue(col.dflt_value, col.name);
308
+ def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
309
+ }
310
+
311
+ colDefs.push(def);
312
+ }
313
+
314
+ // Append new i18n columns
315
+ colDefs.push("\"locale\" TEXT NOT NULL DEFAULT 'en'");
316
+ colDefs.push('"translation_group" TEXT');
317
+
318
+ // Compound unique: same slug + locale must be unique
319
+ colDefs.push('UNIQUE("slug", "locale")');
320
+
321
+ const createColsSql = colDefs.join(",\n\t\t\t\t");
322
+ const selectColsSql = colNames.join(", ");
323
+
324
+ // ── 4. Rebuild the table ────────────────────────────────────
325
+ // Drop all existing indexes first (they reference the old table)
326
+ for (const idx of indexDefs) {
327
+ await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
328
+ }
329
+
330
+ // Create new table with updated schema
331
+ await sql
332
+ .raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`)
333
+ .execute(trx);
334
+
335
+ // Copy existing data, backfilling locale='en' and translation_group=id
336
+ await sql
337
+ .raw(
338
+ `INSERT INTO ${quoteIdent(tmp)} (${selectColsSql}, "locale", "translation_group")\n\t\t\t SELECT ${selectColsSql}, 'en', "id" FROM ${quoteIdent(t)}`,
339
+ )
340
+ .execute(trx);
341
+
342
+ // Swap tables
343
+ await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
344
+ await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
345
+
346
+ // ── 5. Recreate all original indexes ────────────────────────
347
+ for (const idx of indexDefs) {
348
+ // Skip the old slug-only index — replaced by slug_locale below
349
+ if (idx.name === `idx_${t}_slug`) continue;
350
+
351
+ if (idx.partial && partialSqls.has(idx.name)) {
352
+ // Partial indexes — validate the SQL before replaying
353
+ const idxSql = partialSqls.get(idx.name)!;
354
+ validateCreateIndexSql(idxSql, idx.name);
355
+ await sql.raw(idxSql).execute(trx);
356
+ } else {
357
+ // Validate index column names before interpolation
358
+ for (const c of idx.columns) {
359
+ validateIdentifier(c, "index column name");
360
+ }
361
+ const cols = idx.columns.map((c) => quoteIdent(c)).join(", ");
362
+ const unique = idx.unique ? "UNIQUE " : "";
363
+ await sql
364
+ .raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${cols})`)
365
+ .execute(trx);
366
+ }
367
+ }
368
+
369
+ // ── 6. Create new i18n indexes ──────────────────────────────
370
+ // slug_locale unique is handled by the table constraint above,
371
+ // but we still want a regular slug index for non-locale-aware queries
372
+ await sql`
373
+ CREATE INDEX ${sql.ref(`idx_${t}_slug`)}
374
+ ON ${sql.ref(t)} (slug)
375
+ `.execute(trx);
376
+
377
+ await sql`
378
+ CREATE INDEX ${sql.ref(`idx_${t}_locale`)}
379
+ ON ${sql.ref(t)} (locale)
380
+ `.execute(trx);
381
+
382
+ await sql`
383
+ CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)}
384
+ ON ${sql.ref(t)} (translation_group)
385
+ `.execute(trx);
386
+ }
387
+ }
388
+
389
+ // ── 7. Add translatable flag to fields table ────────────────────
390
+ // Guard against duplicate column — on D1 the migration may have
391
+ // partially applied without being recorded (no transaction support).
392
+ const fieldCols = await sql<ColumnInfo>`
393
+ PRAGMA table_info(_emdash_fields)
394
+ `.execute(db);
395
+ if (!fieldCols.rows.some((col) => col.name === "translatable")) {
396
+ await sql`
397
+ ALTER TABLE _emdash_fields
398
+ ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
399
+ `.execute(db);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * PostgreSQL down path: straightforward ALTER TABLE operations.
405
+ */
406
+ async function downPostgres(db: Kysely<unknown>): Promise<void> {
407
+ await sql`ALTER TABLE _emdash_fields DROP COLUMN translatable`.execute(db);
408
+
409
+ const tableNames = await listTablesLike(db, "ec_%");
410
+ for (const t of tableNames) {
411
+ validateTableName(t);
412
+
413
+ // Drop i18n indexes
414
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_locale`)}`.execute(db);
415
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_translation_group`)}`.execute(db);
416
+
417
+ // Drop compound unique constraint
418
+ await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT IF EXISTS ${sql.ref(`${t}_slug_locale_unique`)}`.execute(
419
+ db,
420
+ );
421
+
422
+ // Restore simple unique constraint on slug
423
+ await sql`ALTER TABLE ${sql.ref(t)} ADD CONSTRAINT ${sql.ref(`${t}_slug_unique`)} UNIQUE (slug)`.execute(
424
+ db,
425
+ );
426
+
427
+ // Drop i18n columns
428
+ await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN locale`.execute(db);
429
+ await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN translation_group`.execute(db);
430
+ }
431
+ }
432
+
433
+ export async function down(db: Kysely<unknown>): Promise<void> {
434
+ if (!isSqlite(db)) {
435
+ return downPostgres(db);
436
+ }
437
+
438
+ // Remove translatable column from fields table
439
+ await sql`
440
+ ALTER TABLE _emdash_fields
441
+ DROP COLUMN translatable
442
+ `.execute(db);
443
+
444
+ // Discover all ec_* content tables
445
+ const tableNames = await listTablesLike(db, "ec_%");
446
+
447
+ for (const tableName of tableNames) {
448
+ const t = tableName;
449
+ validateTableName(t);
450
+ const tmp = `${t}_i18n_tmp`;
451
+
452
+ // No transaction — see comment in up() above.
453
+ {
454
+ const trx = db;
455
+ // ── 1. Read current column definitions ──────────────────────
456
+ const colResult = await sql<ColumnInfo>`
457
+ PRAGMA table_info(${sql.ref(t)})
458
+ `.execute(trx);
459
+ const columns = colResult.rows;
460
+
461
+ // ── 2. Read current indexes ─────────────────────────────────
462
+ const idxResult = await sql<IndexInfo>`
463
+ PRAGMA index_list(${sql.ref(t)})
464
+ `.execute(trx);
465
+
466
+ const indexDefs: { name: string; unique: boolean; columns: string[]; partial: number }[] = [];
467
+ for (const idx of idxResult.rows) {
468
+ if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
469
+
470
+ const idxColResult = await sql<IndexColumnInfo>`
471
+ PRAGMA index_info(${sql.ref(idx.name)})
472
+ `.execute(trx);
473
+
474
+ indexDefs.push({
475
+ name: idx.name,
476
+ unique: idx.unique === 1,
477
+ columns: idxColResult.rows.map((c) => c.name),
478
+ partial: idx.partial,
479
+ });
480
+ }
481
+
482
+ // Save partial index SQL
483
+ const partialSqls = new Map<string, string>();
484
+ for (const idx of indexDefs) {
485
+ if (idx.partial) {
486
+ const createResult = await sql<{ sql: string }>`
487
+ SELECT sql FROM sqlite_master
488
+ WHERE type = 'index' AND name = ${idx.name}
489
+ `.execute(trx);
490
+ if (createResult.rows[0]?.sql) {
491
+ partialSqls.set(idx.name, createResult.rows[0].sql);
492
+ }
493
+ }
494
+ }
495
+
496
+ // ── 3. Build column defs WITHOUT locale/translation_group ───
497
+ // Validate all column names
498
+ for (const col of columns) {
499
+ if (col.name === "locale" || col.name === "translation_group") continue;
500
+ validateIdentifier(col.name, "column name");
501
+ }
502
+
503
+ // Restore slug's inline UNIQUE
504
+ const colDefs: string[] = [];
505
+ const colNames: string[] = [];
506
+
507
+ for (const col of columns) {
508
+ // Skip i18n columns
509
+ if (col.name === "locale" || col.name === "translation_group") continue;
510
+
511
+ validateColumnType(col.type || "TEXT", col.name);
512
+ colNames.push(quoteIdent(col.name));
513
+ let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
514
+
515
+ if (col.pk) {
516
+ def += " PRIMARY KEY";
517
+ } else if (col.name === "slug") {
518
+ // Restore inline UNIQUE
519
+ def += " UNIQUE";
520
+ } else {
521
+ if (col.notnull) def += " NOT NULL";
522
+ }
523
+
524
+ if (col.dflt_value !== null) {
525
+ validateDefaultValue(col.dflt_value, col.name);
526
+ def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
527
+ }
528
+
529
+ colDefs.push(def);
530
+ }
531
+
532
+ const createColsSql = colDefs.join(",\n\t\t\t\t");
533
+ const selectColsSql = colNames.join(", ");
534
+
535
+ // ── 4. Rebuild the table ────────────────────────────────────
536
+ // Drop all existing indexes first
537
+ for (const idx of indexDefs) {
538
+ await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
539
+ }
540
+
541
+ // Create table with original schema (slug UNIQUE, no i18n columns)
542
+ await sql
543
+ .raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`)
544
+ .execute(trx);
545
+
546
+ // Copy data — keep only one row per content item.
547
+ // Prefer locale='en' rows. For items without an 'en' row, pick the
548
+ // row with the smallest locale code (deterministic, unlike bare GROUP BY).
549
+ // Handle NULL translation_group by treating each such row as its own group.
550
+ // INSERT OR IGNORE skips any duplicate slugs from the fallback pass.
551
+ await sql
552
+ .raw(
553
+ `INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
554
+ SELECT ${selectColsSql} FROM ${quoteIdent(t)}
555
+ WHERE "locale" = 'en'`,
556
+ )
557
+ .execute(trx);
558
+
559
+ await sql
560
+ .raw(
561
+ `INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
562
+ SELECT ${selectColsSql} FROM ${quoteIdent(t)}
563
+ WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
564
+ AND "id" IN (
565
+ SELECT "id" FROM ${quoteIdent(t)} AS t2
566
+ WHERE t2."translation_group" IS NOT NULL
567
+ AND t2."locale" = (
568
+ SELECT MIN(t3."locale") FROM ${quoteIdent(t)} AS t3
569
+ WHERE t3."translation_group" = t2."translation_group"
570
+ )
571
+ )`,
572
+ )
573
+ .execute(trx);
574
+
575
+ // Pick up any rows with NULL translation_group that weren't already copied
576
+ await sql
577
+ .raw(
578
+ `INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
579
+ SELECT ${selectColsSql} FROM ${quoteIdent(t)}
580
+ WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
581
+ AND "translation_group" IS NULL`,
582
+ )
583
+ .execute(trx);
584
+
585
+ // Swap tables
586
+ await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
587
+ await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
588
+
589
+ // ── 5. Recreate indexes ─────────────────────────────────────
590
+ for (const idx of indexDefs) {
591
+ // Skip i18n-specific indexes — they don't exist in the old schema
592
+ if (idx.name === `idx_${t}_locale`) continue;
593
+ if (idx.name === `idx_${t}_translation_group`) continue;
594
+
595
+ if (idx.partial && partialSqls.has(idx.name)) {
596
+ // Partial indexes — validate the SQL before replaying
597
+ const idxSql = partialSqls.get(idx.name)!;
598
+ validateCreateIndexSql(idxSql, idx.name);
599
+ await sql.raw(idxSql).execute(trx);
600
+ } else {
601
+ // Filter out i18n columns from any index that might reference them
602
+ const cols = idx.columns.filter((c) => c !== "locale" && c !== "translation_group");
603
+ if (cols.length === 0) continue;
604
+
605
+ // Validate column names
606
+ for (const c of cols) {
607
+ validateIdentifier(c, "index column name");
608
+ }
609
+ const colsSql = cols.map((c) => quoteIdent(c)).join(", ");
610
+ const unique = idx.unique ? "UNIQUE " : "";
611
+ await sql
612
+ .raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${colsSql})`)
613
+ .execute(trx);
614
+ }
615
+ }
616
+ }
617
+ }
618
+ }
@@ -0,0 +1,23 @@
1
+ import type { Kysely } from "kysely";
2
+ import { sql } from "kysely";
3
+
4
+ /**
5
+ * Migration: URL pattern for collections
6
+ *
7
+ * Adds `url_pattern` column to `_emdash_collections` so each collection
8
+ * can declare its own URL structure (e.g. "/{slug}" for pages, "/blog/{slug}"
9
+ * for posts). Used for menu URL resolution, sitemaps, and path-based lookups.
10
+ */
11
+ export async function up(db: Kysely<unknown>): Promise<void> {
12
+ await sql`
13
+ ALTER TABLE _emdash_collections
14
+ ADD COLUMN url_pattern TEXT
15
+ `.execute(db);
16
+ }
17
+
18
+ export async function down(db: Kysely<unknown>): Promise<void> {
19
+ await sql`
20
+ ALTER TABLE _emdash_collections
21
+ DROP COLUMN url_pattern
22
+ `.execute(db);
23
+ }