@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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const LEGACY_TABLE = "post_collection";
|
|
2
|
+
const TARGET_TABLE = "thread_collection";
|
|
3
|
+
|
|
4
|
+
function toCount(value, label) {
|
|
5
|
+
const count = Number(value ?? 0);
|
|
6
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`Thread Collection migration check returned an invalid ${label}: ${String(value)}.`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return count;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toBoolean(value) {
|
|
15
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readTableState(query, dialect) {
|
|
19
|
+
const rows = await query(
|
|
20
|
+
dialect === "pg"
|
|
21
|
+
? `
|
|
22
|
+
SELECT
|
|
23
|
+
to_regclass('public.${LEGACY_TABLE}') IS NOT NULL AS legacy_exists,
|
|
24
|
+
to_regclass('public.${TARGET_TABLE}') IS NOT NULL AS target_exists
|
|
25
|
+
`
|
|
26
|
+
: `
|
|
27
|
+
SELECT
|
|
28
|
+
EXISTS(
|
|
29
|
+
SELECT 1 FROM sqlite_master
|
|
30
|
+
WHERE type = 'table' AND name = '${LEGACY_TABLE}'
|
|
31
|
+
) AS legacy_exists,
|
|
32
|
+
EXISTS(
|
|
33
|
+
SELECT 1 FROM sqlite_master
|
|
34
|
+
WHERE type = 'table' AND name = '${TARGET_TABLE}'
|
|
35
|
+
) AS target_exists
|
|
36
|
+
`,
|
|
37
|
+
);
|
|
38
|
+
const row = rows[0] ?? {};
|
|
39
|
+
return {
|
|
40
|
+
legacyExists: toBoolean(row.legacy_exists),
|
|
41
|
+
targetExists: toBoolean(row.target_exists),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readSqlitePostColumns(query) {
|
|
46
|
+
const rows = await query(`PRAGMA table_info("post")`);
|
|
47
|
+
return new Set(rows.map((row) => String(row.name)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readPostgresPostColumns(query) {
|
|
51
|
+
const rows = await query(`
|
|
52
|
+
SELECT column_name
|
|
53
|
+
FROM information_schema.columns
|
|
54
|
+
WHERE table_schema = 'public' AND table_name = 'post'
|
|
55
|
+
`);
|
|
56
|
+
return new Set(rows.map((row) => String(row.column_name)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readPostColumns(query, dialect) {
|
|
60
|
+
return dialect === "pg"
|
|
61
|
+
? readPostgresPostColumns(query)
|
|
62
|
+
: readSqlitePostColumns(query);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasThreadColumns(columns) {
|
|
66
|
+
return (
|
|
67
|
+
columns.has("id") &&
|
|
68
|
+
columns.has("site_id") &&
|
|
69
|
+
columns.has("reply_to_id") &&
|
|
70
|
+
columns.has("thread_id")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readLegacyStats(query, postColumns) {
|
|
75
|
+
const retainedPostPredicate = postColumns.has("deleted_at")
|
|
76
|
+
? "post.deleted_at IS NULL"
|
|
77
|
+
: "1 = 1";
|
|
78
|
+
const softDeletedSourceExpression = postColumns.has("deleted_at")
|
|
79
|
+
? "CASE WHEN post.deleted_at IS NOT NULL THEN 1 ELSE 0 END"
|
|
80
|
+
: "0";
|
|
81
|
+
const rows = await query(`
|
|
82
|
+
SELECT
|
|
83
|
+
COUNT(*) AS source_count,
|
|
84
|
+
COALESCE(SUM(${softDeletedSourceExpression}), 0)
|
|
85
|
+
AS soft_deleted_source_count,
|
|
86
|
+
COALESCE(SUM(CASE WHEN site.id IS NULL THEN 1 ELSE 0 END), 0)
|
|
87
|
+
AS missing_site_count,
|
|
88
|
+
COALESCE(SUM(CASE WHEN post.id IS NULL THEN 1 ELSE 0 END), 0)
|
|
89
|
+
AS missing_post_count,
|
|
90
|
+
COALESCE(SUM(CASE WHEN collection.id IS NULL THEN 1 ELSE 0 END), 0)
|
|
91
|
+
AS missing_collection_count,
|
|
92
|
+
COALESCE(SUM(
|
|
93
|
+
CASE
|
|
94
|
+
WHEN post.id IS NOT NULL AND (
|
|
95
|
+
root.id IS NULL
|
|
96
|
+
OR root.reply_to_id IS NOT NULL
|
|
97
|
+
OR root.thread_id <> root.id
|
|
98
|
+
) THEN 1
|
|
99
|
+
ELSE 0
|
|
100
|
+
END
|
|
101
|
+
), 0) AS invalid_thread_count
|
|
102
|
+
FROM post_collection AS membership
|
|
103
|
+
LEFT JOIN site
|
|
104
|
+
ON site.id = membership.site_id
|
|
105
|
+
LEFT JOIN post
|
|
106
|
+
ON post.site_id = membership.site_id
|
|
107
|
+
AND post.id = membership.post_id
|
|
108
|
+
LEFT JOIN post AS root
|
|
109
|
+
ON root.site_id = post.site_id
|
|
110
|
+
AND root.id = post.thread_id
|
|
111
|
+
LEFT JOIN collection
|
|
112
|
+
ON collection.site_id = membership.site_id
|
|
113
|
+
AND collection.id = membership.collection_id
|
|
114
|
+
`);
|
|
115
|
+
const row = rows[0] ?? {};
|
|
116
|
+
const expectedRows = await query(`
|
|
117
|
+
SELECT COUNT(*) AS expected_count
|
|
118
|
+
FROM (
|
|
119
|
+
SELECT
|
|
120
|
+
membership.site_id,
|
|
121
|
+
post.thread_id,
|
|
122
|
+
membership.collection_id
|
|
123
|
+
FROM post_collection AS membership
|
|
124
|
+
INNER JOIN post
|
|
125
|
+
ON post.site_id = membership.site_id
|
|
126
|
+
AND post.id = membership.post_id
|
|
127
|
+
WHERE ${retainedPostPredicate}
|
|
128
|
+
GROUP BY
|
|
129
|
+
membership.site_id,
|
|
130
|
+
post.thread_id,
|
|
131
|
+
membership.collection_id
|
|
132
|
+
) AS thread_membership
|
|
133
|
+
`);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
sourceCount: toCount(row.source_count, "legacy row count"),
|
|
137
|
+
softDeletedSourceCount: toCount(
|
|
138
|
+
row.soft_deleted_source_count,
|
|
139
|
+
"soft-deleted legacy row count",
|
|
140
|
+
),
|
|
141
|
+
expectedCount: toCount(
|
|
142
|
+
expectedRows[0]?.expected_count,
|
|
143
|
+
"expected Thread membership count",
|
|
144
|
+
),
|
|
145
|
+
missingSiteCount: toCount(row.missing_site_count, "missing site count"),
|
|
146
|
+
missingPostCount: toCount(row.missing_post_count, "missing post count"),
|
|
147
|
+
missingCollectionCount: toCount(
|
|
148
|
+
row.missing_collection_count,
|
|
149
|
+
"missing Collection count",
|
|
150
|
+
),
|
|
151
|
+
invalidThreadCount: toCount(
|
|
152
|
+
row.invalid_thread_count,
|
|
153
|
+
"invalid Thread root count",
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readTargetStats(query) {
|
|
159
|
+
const rows = await query(`
|
|
160
|
+
SELECT
|
|
161
|
+
COUNT(*) AS target_count,
|
|
162
|
+
COALESCE(SUM(CASE WHEN site.id IS NULL THEN 1 ELSE 0 END), 0)
|
|
163
|
+
AS missing_site_count,
|
|
164
|
+
COALESCE(SUM(CASE WHEN root.id IS NULL THEN 1 ELSE 0 END), 0)
|
|
165
|
+
AS missing_thread_count,
|
|
166
|
+
COALESCE(SUM(CASE WHEN collection.id IS NULL THEN 1 ELSE 0 END), 0)
|
|
167
|
+
AS missing_collection_count,
|
|
168
|
+
COALESCE(SUM(
|
|
169
|
+
CASE
|
|
170
|
+
WHEN root.id IS NOT NULL AND (
|
|
171
|
+
root.reply_to_id IS NOT NULL
|
|
172
|
+
OR root.thread_id <> root.id
|
|
173
|
+
) THEN 1
|
|
174
|
+
ELSE 0
|
|
175
|
+
END
|
|
176
|
+
), 0) AS invalid_thread_count
|
|
177
|
+
FROM thread_collection AS membership
|
|
178
|
+
LEFT JOIN site
|
|
179
|
+
ON site.id = membership.site_id
|
|
180
|
+
LEFT JOIN post AS root
|
|
181
|
+
ON root.site_id = membership.site_id
|
|
182
|
+
AND root.id = membership.thread_id
|
|
183
|
+
LEFT JOIN collection
|
|
184
|
+
ON collection.site_id = membership.site_id
|
|
185
|
+
AND collection.id = membership.collection_id
|
|
186
|
+
`);
|
|
187
|
+
const row = rows[0] ?? {};
|
|
188
|
+
return {
|
|
189
|
+
targetCount: toCount(row.target_count, "migrated row count"),
|
|
190
|
+
missingSiteCount: toCount(row.missing_site_count, "missing site count"),
|
|
191
|
+
missingThreadCount: toCount(
|
|
192
|
+
row.missing_thread_count,
|
|
193
|
+
"missing Thread count",
|
|
194
|
+
),
|
|
195
|
+
missingCollectionCount: toCount(
|
|
196
|
+
row.missing_collection_count,
|
|
197
|
+
"missing Collection count",
|
|
198
|
+
),
|
|
199
|
+
invalidThreadCount: toCount(
|
|
200
|
+
row.invalid_thread_count,
|
|
201
|
+
"invalid Thread root count",
|
|
202
|
+
),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatInvalidLegacyStats(stats) {
|
|
207
|
+
return [
|
|
208
|
+
`missing sites=${stats.missingSiteCount}`,
|
|
209
|
+
`missing posts=${stats.missingPostCount}`,
|
|
210
|
+
`missing Collections=${stats.missingCollectionCount}`,
|
|
211
|
+
`invalid Thread roots=${stats.invalidThreadCount}`,
|
|
212
|
+
].join(", ");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function assertLegacyStats(stats) {
|
|
216
|
+
if (
|
|
217
|
+
stats.missingSiteCount > 0 ||
|
|
218
|
+
stats.missingPostCount > 0 ||
|
|
219
|
+
stats.missingCollectionCount > 0 ||
|
|
220
|
+
stats.invalidThreadCount > 0
|
|
221
|
+
) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Thread Collection migration preflight failed (${formatInvalidLegacyStats(stats)}). No migration was applied. Repair or restore the legacy data, then run jant migrate again.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function assertTargetStats(stats, expectedCount) {
|
|
229
|
+
const invalidReferences =
|
|
230
|
+
stats.missingSiteCount > 0 ||
|
|
231
|
+
stats.missingThreadCount > 0 ||
|
|
232
|
+
stats.missingCollectionCount > 0 ||
|
|
233
|
+
stats.invalidThreadCount > 0;
|
|
234
|
+
const countMismatch =
|
|
235
|
+
expectedCount !== undefined && stats.targetCount !== expectedCount;
|
|
236
|
+
|
|
237
|
+
if (invalidReferences || countMismatch) {
|
|
238
|
+
const expected =
|
|
239
|
+
expectedCount === undefined ? "not recorded" : String(expectedCount);
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Thread Collection post-migration verification failed (expected rows=${expected}, actual rows=${stats.targetCount}, missing sites=${stats.missingSiteCount}, missing Threads=${stats.missingThreadCount}, missing Collections=${stats.missingCollectionCount}, invalid Thread roots=${stats.invalidThreadCount}). The schema migration may already be committed; keep writes stopped and restore or inspect the database before retrying.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Validate legacy Collection membership before the destructive table cutover.
|
|
248
|
+
*
|
|
249
|
+
* @param {{ dialect: "pg" | "sqlite", log?: (message: string) => void, query: (sql: string) => unknown[] | Promise<unknown[]> }} options
|
|
250
|
+
* @returns {Promise<{ expectedCount?: number, phase: "already-migrated" | "deferred" | "fresh" | "legacy", sourceCount?: number }>}
|
|
251
|
+
*/
|
|
252
|
+
export async function preflightThreadCollectionMigration(options) {
|
|
253
|
+
const log = options.log ?? console.log;
|
|
254
|
+
const state = await readTableState(options.query, options.dialect);
|
|
255
|
+
|
|
256
|
+
if (state.legacyExists && state.targetExists) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
"Thread Collection migration preflight found both post_collection and thread_collection. No migration was applied. Inspect the migration journal and database state before retrying.",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (state.targetExists) {
|
|
263
|
+
const stats = await readTargetStats(options.query);
|
|
264
|
+
assertTargetStats(stats, undefined);
|
|
265
|
+
log(
|
|
266
|
+
`Thread Collection migration already applied (${stats.targetCount} memberships verified).`,
|
|
267
|
+
);
|
|
268
|
+
return { phase: "already-migrated" };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!state.legacyExists) {
|
|
272
|
+
return { phase: "fresh" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const postColumns = await readPostColumns(options.query, options.dialect);
|
|
276
|
+
if (!hasThreadColumns(postColumns)) {
|
|
277
|
+
log(
|
|
278
|
+
"Thread Collection migration preflight deferred until earlier schema migrations add Thread columns.",
|
|
279
|
+
);
|
|
280
|
+
return { phase: "deferred" };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const stats = await readLegacyStats(options.query, postColumns);
|
|
284
|
+
assertLegacyStats(stats);
|
|
285
|
+
const softDeletedMembershipLabel =
|
|
286
|
+
stats.softDeletedSourceCount === 1 ? "membership" : "memberships";
|
|
287
|
+
const anticipatedDeletion =
|
|
288
|
+
stats.softDeletedSourceCount > 0
|
|
289
|
+
? `; ${stats.softDeletedSourceCount} soft-deleted post ${softDeletedMembershipLabel} will be removed by an earlier migration`
|
|
290
|
+
: "";
|
|
291
|
+
log(
|
|
292
|
+
`Thread Collection migration preflight passed (${stats.sourceCount} post memberships -> ${stats.expectedCount} Thread memberships${anticipatedDeletion}).`,
|
|
293
|
+
);
|
|
294
|
+
log(
|
|
295
|
+
"Thread Collection cutover requires writes to remain stopped until migration, application deployment, and verification finish.",
|
|
296
|
+
);
|
|
297
|
+
return {
|
|
298
|
+
phase: "legacy",
|
|
299
|
+
expectedCount: stats.expectedCount,
|
|
300
|
+
sourceCount: stats.sourceCount,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Verify the replacement table after the formal schema migrator returns.
|
|
306
|
+
*
|
|
307
|
+
* @param {{ dialect: "pg" | "sqlite", log?: (message: string) => void, query: (sql: string) => unknown[] | Promise<unknown[]> }} options
|
|
308
|
+
* @param {{ expectedCount?: number, phase: "already-migrated" | "deferred" | "fresh" | "legacy", sourceCount?: number }} preflight
|
|
309
|
+
* @returns {Promise<void>}
|
|
310
|
+
*/
|
|
311
|
+
export async function verifyThreadCollectionMigration(options, preflight) {
|
|
312
|
+
const log = options.log ?? console.log;
|
|
313
|
+
const state = await readTableState(options.query, options.dialect);
|
|
314
|
+
|
|
315
|
+
if (state.legacyExists || !state.targetExists) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Thread Collection post-migration verification failed (post_collection exists=${state.legacyExists}, thread_collection exists=${state.targetExists}). The schema migration may already be partially applied; keep writes stopped and inspect the database before retrying.`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const stats = await readTargetStats(options.query);
|
|
322
|
+
assertTargetStats(stats, preflight.expectedCount);
|
|
323
|
+
log(
|
|
324
|
+
`Thread Collection post-migration verification passed (${stats.targetCount} memberships).`,
|
|
325
|
+
);
|
|
326
|
+
}
|