@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.
- package/bin/commands/import-site.js +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
package/bin/lib/site-snapshot.js
CHANGED
|
@@ -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 =
|
|
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
|
-
"
|
|
14
|
+
"thread_collection",
|
|
14
15
|
"path_registry",
|
|
15
16
|
"media",
|
|
16
17
|
];
|
|
17
18
|
|
|
18
19
|
export const SNAPSHOT_CLEAR_TABLES = [
|
|
19
|
-
"
|
|
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
|
-
|
|
87
|
+
thread_collection: `
|
|
85
88
|
SELECT *
|
|
86
|
-
FROM "
|
|
89
|
+
FROM "thread_collection"
|
|
87
90
|
WHERE "site_id" = ?1
|
|
88
|
-
ORDER BY "created_at", "
|
|
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
|
|
212
|
+
if (!SUPPORTED_SNAPSHOT_VERSIONS.includes(meta.version)) {
|
|
210
213
|
throw new Error(
|
|
211
|
-
`Unsupported snapshot version: expected ${
|
|
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
|
|
382
|
+
let state = "sql";
|
|
380
383
|
|
|
381
384
|
for (let index = 0; index < sql.length; index += 1) {
|
|
382
385
|
const char = sql[index];
|
|
383
|
-
|
|
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 (
|
|
407
|
+
if (char === "'" && sql[index + 1] === "'") {
|
|
386
408
|
current += "'";
|
|
387
409
|
index += 1;
|
|
388
410
|
continue;
|
|
389
411
|
}
|
|
390
|
-
|
|
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 === "
|
|
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
|
|
416
|
-
.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|