@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -2,7 +2,8 @@ import { readdir } from "node:fs/promises";
2
2
  import { join, relative } from "node:path";
3
3
 
4
4
  export const SNAPSHOT_FORMAT = "jant-site-snapshot";
5
- export const SNAPSHOT_VERSION = 1;
5
+ export const SNAPSHOT_VERSION = 2;
6
+ export const SUPPORTED_SNAPSHOT_VERSIONS = [1, SNAPSHOT_VERSION];
6
7
 
7
8
  export const SNAPSHOT_TABLES = [
8
9
  "site_setting",
@@ -10,13 +11,13 @@ export const SNAPSHOT_TABLES = [
10
11
  "nav_item",
11
12
  "collection_directory_item",
12
13
  "post",
13
- "post_collection",
14
+ "thread_collection",
14
15
  "path_registry",
15
16
  "media",
16
17
  ];
17
18
 
18
19
  export const SNAPSHOT_CLEAR_TABLES = [
19
- "post_collection",
20
+ "thread_collection",
20
21
  "media",
21
22
  "path_registry",
22
23
  "collection_directory_item",
@@ -30,6 +31,8 @@ export const SNAPSHOT_SETTING_KEYS = [
30
31
  "SITE_DESCRIPTION",
31
32
  "SITE_LANGUAGE",
32
33
  "MAIN_RSS_FEED",
34
+ "PUBLIC_API_ENABLED",
35
+ "RSS_FEEDS_ENABLED",
33
36
  "THEME",
34
37
  "CUSTOM_CSS",
35
38
  "SITE_AVATAR",
@@ -81,11 +84,11 @@ const SELECT_SQL_BY_TABLE = {
81
84
  WHERE "site_id" = ?1
82
85
  ORDER BY "created_at", "id"
83
86
  `,
84
- post_collection: `
87
+ thread_collection: `
85
88
  SELECT *
86
- FROM "post_collection"
89
+ FROM "thread_collection"
87
90
  WHERE "site_id" = ?1
88
- ORDER BY "created_at", "post_id", "collection_id"
91
+ ORDER BY "created_at", "thread_id", "collection_id"
89
92
  `,
90
93
  path_registry: `
91
94
  SELECT *
@@ -206,9 +209,9 @@ export function assertSnapshotMeta(meta) {
206
209
  );
207
210
  }
208
211
 
209
- if (meta.version !== SNAPSHOT_VERSION) {
212
+ if (!SUPPORTED_SNAPSHOT_VERSIONS.includes(meta.version)) {
210
213
  throw new Error(
211
- `Unsupported snapshot version: expected ${SNAPSHOT_VERSION}, got ${String(meta.version)}`,
214
+ `Unsupported snapshot version: expected one of ${SUPPORTED_SNAPSHOT_VERSIONS.join(", ")}, got ${String(meta.version)}`,
212
215
  );
213
216
  }
214
217
 
@@ -376,22 +379,86 @@ function prependSiteIdInsert(sql, tableName, siteId) {
376
379
  function splitSqlStatements(sql) {
377
380
  const statements = [];
378
381
  let current = "";
379
- let inString = false;
382
+ let state = "sql";
380
383
 
381
384
  for (let index = 0; index < sql.length; index += 1) {
382
385
  const char = sql[index];
383
- if (char === "'") {
386
+
387
+ if (state === "line-comment") {
388
+ if (char === "\n") {
389
+ current += char;
390
+ state = "sql";
391
+ }
392
+ continue;
393
+ }
394
+
395
+ if (state === "block-comment") {
396
+ if (char === "*" && sql[index + 1] === "/") {
397
+ index += 1;
398
+ state = "sql";
399
+ } else if (char === "\n") {
400
+ current += char;
401
+ }
402
+ continue;
403
+ }
404
+
405
+ if (state === "string") {
384
406
  current += char;
385
- if (inString && sql[index + 1] === "'") {
407
+ if (char === "'" && sql[index + 1] === "'") {
386
408
  current += "'";
387
409
  index += 1;
388
410
  continue;
389
411
  }
390
- inString = !inString;
412
+ if (char === "'") {
413
+ state = "sql";
414
+ }
415
+ continue;
416
+ }
417
+
418
+ if (state === "identifier") {
419
+ current += char;
420
+ if (char === '"' && sql[index + 1] === '"') {
421
+ current += '"';
422
+ index += 1;
423
+ continue;
424
+ }
425
+ if (char === '"') {
426
+ state = "sql";
427
+ }
428
+ continue;
429
+ }
430
+
431
+ if (char === "'") {
432
+ current += char;
433
+ state = "string";
434
+ continue;
435
+ }
436
+
437
+ if (char === '"') {
438
+ current += char;
439
+ state = "identifier";
391
440
  continue;
392
441
  }
393
442
 
394
- if (char === ";" && !inString) {
443
+ if (char === "-" && sql[index + 1] === "-") {
444
+ if (current && !/\s$/.test(current)) {
445
+ current += " ";
446
+ }
447
+ index += 1;
448
+ state = "line-comment";
449
+ continue;
450
+ }
451
+
452
+ if (char === "/" && sql[index + 1] === "*") {
453
+ if (current && !/\s$/.test(current)) {
454
+ current += " ";
455
+ }
456
+ index += 1;
457
+ state = "block-comment";
458
+ continue;
459
+ }
460
+
461
+ if (char === ";") {
395
462
  const trimmed = current.trim();
396
463
  if (trimmed) {
397
464
  statements.push(trimmed);
@@ -403,6 +470,13 @@ function splitSqlStatements(sql) {
403
470
  current += char;
404
471
  }
405
472
 
473
+ if (state === "string" || state === "identifier") {
474
+ throw new Error("Snapshot SQL contains an unterminated quoted value.");
475
+ }
476
+ if (state === "block-comment") {
477
+ throw new Error("Snapshot SQL contains an unterminated block comment.");
478
+ }
479
+
406
480
  const trimmed = current.trim();
407
481
  if (trimmed) {
408
482
  statements.push(trimmed);
@@ -412,39 +486,270 @@ function splitSqlStatements(sql) {
412
486
  }
413
487
 
414
488
  export function rewriteLegacySnapshotSql(sql, siteId) {
415
- const uncommentedSql = sql
416
- .split("\n")
417
- .filter((line) => !line.trimStart().startsWith("--"))
418
- .join("\n");
419
-
420
- const rewrittenStatements = splitSqlStatements(uncommentedSql).map(
421
- (statement) => {
422
- const normalized = statement.trim();
423
- const legacySettingMatch = normalized.match(
424
- /^INSERT INTO "?setting"? \(([^)]*)\) VALUES\(([\s\S]*)\)$/i,
489
+ const rewrittenStatements = splitSqlStatements(sql).map((statement) => {
490
+ const normalized = statement.trim();
491
+ const legacySettingMatch = normalized.match(
492
+ /^INSERT INTO "?setting"? \(([^)]*)\) VALUES\(([\s\S]*)\)$/i,
493
+ );
494
+ if (legacySettingMatch) {
495
+ return `INSERT INTO "site_setting" ("site_id", ${legacySettingMatch[1]}) VALUES('${escapeSqlString(siteId)}', ${legacySettingMatch[2]})`;
496
+ }
497
+
498
+ let rewritten = normalized;
499
+ for (const tableName of [
500
+ "collection",
501
+ "nav_item",
502
+ "collection_directory_item",
503
+ "post",
504
+ "post_collection",
505
+ "thread_collection",
506
+ "path_registry",
507
+ "media",
508
+ ]) {
509
+ rewritten = prependSiteIdInsert(rewritten, tableName, siteId);
510
+ }
511
+
512
+ return rewritten;
513
+ });
514
+
515
+ return `${rewrittenStatements.join(";\n")};\n`;
516
+ }
517
+
518
+ function parseInsertStatement(statement) {
519
+ const match = statement.match(
520
+ /^INSERT\s+INTO\s+"?([^"\s]+)"?\s*\(([^)]*)\)\s*VALUES\s*\(([\s\S]*)\)$/i,
521
+ );
522
+ if (!match) {
523
+ return null;
524
+ }
525
+
526
+ const columns = match[2]
527
+ .split(",")
528
+ .map((column) => column.trim().replace(/^"|"$/g, ""));
529
+ const values = parseSqlValueList(match[3]);
530
+ if (columns.length !== values.length) {
531
+ throw new Error(
532
+ `Snapshot INSERT for ${match[1]} has ${columns.length} columns but ${values.length} values.`,
533
+ );
534
+ }
535
+
536
+ return {
537
+ table: match[1],
538
+ values: new Map(columns.map((column, index) => [column, values[index]])),
539
+ };
540
+ }
541
+
542
+ function readRequiredSqlString(insert, column, label) {
543
+ const value = parseSqlScalar(insert.values.get(column));
544
+ if (typeof value !== "string" || value.trim() === "") {
545
+ throw new Error(`Snapshot ${label} is missing required ${column}.`);
546
+ }
547
+ return value;
548
+ }
549
+
550
+ function readSqlNumber(insert, column, fallback, label) {
551
+ if (!insert.values.has(column)) {
552
+ return fallback;
553
+ }
554
+ const value = parseSqlScalar(insert.values.get(column));
555
+ if (value === null && fallback === null) {
556
+ return null;
557
+ }
558
+ const number = Number(value);
559
+ if (!Number.isFinite(number)) {
560
+ throw new Error(`Snapshot ${label} has invalid ${column}.`);
561
+ }
562
+ return number;
563
+ }
564
+
565
+ function formatSqlScalar(value) {
566
+ if (value === null) {
567
+ return "NULL";
568
+ }
569
+ if (typeof value === "number") {
570
+ return String(value);
571
+ }
572
+ return `'${escapeSqlString(value)}'`;
573
+ }
574
+
575
+ /**
576
+ * Upgrade a v1 snapshot dump from per-post Collection rows to the explicit
577
+ * v2 Thread model. The conversion happens entirely in memory so callers can
578
+ * fail before clearing any target tables.
579
+ *
580
+ * Memberships from every post in a Thread are unioned. Duplicate metadata
581
+ * uses MAX(created_at), MAX(pinned_at), and MIN(position), matching the live
582
+ * schema migration.
583
+ *
584
+ * @param {string} sql v1 snapshot SQL after site-id rewriting
585
+ * @returns {string} replayable v2 SQL containing `thread_collection` rows
586
+ * @example
587
+ * upgradeV1SnapshotSql(v1DbSql)
588
+ */
589
+ export function upgradeV1SnapshotSql(sql) {
590
+ const statements = splitSqlStatements(sql);
591
+ const postsById = new Map();
592
+ const collectionIds = new Set();
593
+
594
+ for (const statement of statements) {
595
+ const insert = parseInsertStatement(statement);
596
+ const table = insert?.table.toLowerCase();
597
+ if (table === "collection") {
598
+ const siteId = readRequiredSqlString(insert, "site_id", "collection row");
599
+ const collectionId = readRequiredSqlString(
600
+ insert,
601
+ "id",
602
+ "collection row",
425
603
  );
426
- if (legacySettingMatch) {
427
- return `INSERT INTO "site_setting" ("site_id", ${legacySettingMatch[1]}) VALUES('${escapeSqlString(siteId)}', ${legacySettingMatch[2]})`;
428
- }
604
+ collectionIds.add(`${siteId}\u0000${collectionId}`);
605
+ continue;
606
+ }
607
+ if (table !== "post") {
608
+ continue;
609
+ }
610
+ const siteId = readRequiredSqlString(insert, "site_id", "post row");
611
+ const postId = readRequiredSqlString(insert, "id", "post row");
612
+ const threadId = readRequiredSqlString(insert, "thread_id", "post row");
613
+ const replyToId = parseSqlScalar(insert.values.get("reply_to_id"));
614
+ if (replyToId !== null && typeof replyToId !== "string") {
615
+ throw new Error("Snapshot post row has invalid reply_to_id.");
616
+ }
617
+ postsById.set(`${siteId}\u0000${postId}`, {
618
+ id: postId,
619
+ replyToId,
620
+ threadId,
621
+ });
622
+ }
429
623
 
430
- let rewritten = normalized;
431
- for (const tableName of [
432
- "collection",
433
- "nav_item",
434
- "collection_directory_item",
435
- "post",
436
- "post_collection",
437
- "path_registry",
438
- "media",
439
- ]) {
440
- rewritten = prependSiteIdInsert(rewritten, tableName, siteId);
441
- }
624
+ const memberships = new Map();
625
+ let lastDependencyIndex = -1;
626
+ const retainedStatements = [];
442
627
 
443
- return rewritten;
444
- },
628
+ for (const statement of statements) {
629
+ const insert = parseInsertStatement(statement);
630
+ const table = insert?.table.toLowerCase();
631
+ if (table === "collection" || table === "post") {
632
+ lastDependencyIndex = retainedStatements.length;
633
+ }
634
+ if (table !== "post_collection") {
635
+ retainedStatements.push(statement.trim());
636
+ continue;
637
+ }
638
+
639
+ const siteId = readRequiredSqlString(
640
+ insert,
641
+ "site_id",
642
+ "post_collection row",
643
+ );
644
+ const postId = readRequiredSqlString(
645
+ insert,
646
+ "post_id",
647
+ "post_collection row",
648
+ );
649
+ const collectionId = readRequiredSqlString(
650
+ insert,
651
+ "collection_id",
652
+ "post_collection row",
653
+ );
654
+ const post = postsById.get(`${siteId}\u0000${postId}`);
655
+ if (!post) {
656
+ throw new Error(
657
+ `Snapshot post_collection row references missing post ${postId}; target content was not changed.`,
658
+ );
659
+ }
660
+ if (!collectionIds.has(`${siteId}\u0000${collectionId}`)) {
661
+ throw new Error(
662
+ `Snapshot post_collection row references missing Collection ${collectionId}; target content was not changed.`,
663
+ );
664
+ }
665
+
666
+ const threadId = post.threadId;
667
+ const root = postsById.get(`${siteId}\u0000${threadId}`);
668
+ if (!root) {
669
+ throw new Error(
670
+ `Snapshot post_collection row references missing Thread root ${threadId}; target content was not changed.`,
671
+ );
672
+ }
673
+ if (root.id !== root.threadId || root.replyToId !== null) {
674
+ throw new Error(
675
+ `Snapshot post_collection row references invalid Thread root ${threadId}; target content was not changed.`,
676
+ );
677
+ }
678
+
679
+ const createdAt = readSqlNumber(
680
+ insert,
681
+ "created_at",
682
+ 0,
683
+ "post_collection row",
684
+ );
685
+ const position = readSqlNumber(
686
+ insert,
687
+ "position",
688
+ 0,
689
+ "post_collection row",
690
+ );
691
+ const pinnedAt = readSqlNumber(
692
+ insert,
693
+ "pinned_at",
694
+ null,
695
+ "post_collection row",
696
+ );
697
+ const key = `${siteId}\u0000${threadId}\u0000${collectionId}`;
698
+ const current = memberships.get(key);
699
+ if (!current) {
700
+ memberships.set(key, {
701
+ siteId,
702
+ threadId,
703
+ collectionId,
704
+ createdAt,
705
+ position,
706
+ pinnedAt,
707
+ });
708
+ continue;
709
+ }
710
+
711
+ current.createdAt = Math.max(current.createdAt, createdAt);
712
+ current.position = Math.min(current.position, position);
713
+ if (pinnedAt !== null) {
714
+ current.pinnedAt =
715
+ current.pinnedAt === null
716
+ ? pinnedAt
717
+ : Math.max(current.pinnedAt, pinnedAt);
718
+ }
719
+ }
720
+
721
+ if (memberships.size === 0) {
722
+ return sql;
723
+ }
724
+
725
+ const upgradedStatements = [...memberships.values()].map(
726
+ (membership) =>
727
+ `INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at", "position", "pinned_at") VALUES(${[
728
+ membership.siteId,
729
+ membership.threadId,
730
+ membership.collectionId,
731
+ membership.createdAt,
732
+ membership.position,
733
+ membership.pinnedAt,
734
+ ]
735
+ .map(formatSqlScalar)
736
+ .join(", ")})`,
445
737
  );
738
+ retainedStatements.splice(lastDependencyIndex + 1, 0, ...upgradedStatements);
739
+ return `${retainedStatements.join(";\n")};\n`;
740
+ }
446
741
 
447
- return `${rewrittenStatements.join(";\n")};\n`;
742
+ /**
743
+ * Apply format-version compatibility rewrites to snapshot SQL.
744
+ *
745
+ * @param {string} sql site-scoped snapshot SQL
746
+ * @param {number} snapshotVersion version read from `meta.json`
747
+ * @returns {string} SQL ready for the current schema
748
+ * @example
749
+ * upgradeSnapshotSql(dbSql, meta.version)
750
+ */
751
+ export function upgradeSnapshotSql(sql, snapshotVersion) {
752
+ return snapshotVersion === 1 ? upgradeV1SnapshotSql(sql) : sql;
448
753
  }
449
754
 
450
755
  /**
@@ -459,11 +764,7 @@ export function rewriteLegacySnapshotSql(sql, siteId) {
459
764
  * the preflight check that runs before db.sql is applied.
460
765
  */
461
766
  export function extractMediaStorageKeysFromDumpSql(sql, sourceSiteId) {
462
- const uncommented = sql
463
- .split("\n")
464
- .filter((line) => !line.trimStart().startsWith("--"))
465
- .join("\n");
466
- const statements = splitSqlStatements(uncommented);
767
+ const statements = splitSqlStatements(sql);
467
768
  const keys = new Set();
468
769
 
469
770
  for (const statement of statements) {
@@ -12,7 +12,7 @@ const TABLE_EXPORT_ORDER = [
12
12
  "collection_directory_item",
13
13
  "api_token",
14
14
  "post",
15
- "post_collection",
15
+ "thread_collection",
16
16
  "path_registry",
17
17
  "media",
18
18
  ];