@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,364 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
preflightThreadCollectionMigration,
|
|
5
|
+
verifyThreadCollectionMigration,
|
|
6
|
+
} from "../../../bin/lib/thread-collection-migration.js";
|
|
7
|
+
|
|
8
|
+
function createQuery(sqlite: Database.Database) {
|
|
9
|
+
return (sql: string) => sqlite.prepare(sql).all();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createDialectQuery(
|
|
13
|
+
sqlite: Database.Database,
|
|
14
|
+
dialect: "pg" | "sqlite",
|
|
15
|
+
) {
|
|
16
|
+
if (dialect === "sqlite") return createQuery(sqlite);
|
|
17
|
+
|
|
18
|
+
return (sql: string) => {
|
|
19
|
+
if (sql.includes("to_regclass('public.post_collection')")) {
|
|
20
|
+
const tableExists = (name: string) =>
|
|
21
|
+
sqlite
|
|
22
|
+
.prepare(
|
|
23
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
24
|
+
)
|
|
25
|
+
.get(name) !== undefined;
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
legacy_exists: tableExists("post_collection"),
|
|
29
|
+
target_exists: tableExists("thread_collection"),
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (sql.includes("information_schema.columns")) {
|
|
35
|
+
return sqlite
|
|
36
|
+
.prepare(`PRAGMA table_info("post")`)
|
|
37
|
+
.all()
|
|
38
|
+
.map((row) => ({
|
|
39
|
+
column_name: String((row as { name: unknown }).name),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return sqlite.prepare(sql).all();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createDomainTables(sqlite: Database.Database) {
|
|
48
|
+
sqlite.exec(`
|
|
49
|
+
CREATE TABLE site (
|
|
50
|
+
id TEXT PRIMARY KEY
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE post (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
site_id TEXT NOT NULL,
|
|
56
|
+
reply_to_id TEXT,
|
|
57
|
+
thread_id TEXT NOT NULL
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE collection (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
site_id TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createLegacyTable(sqlite: Database.Database) {
|
|
68
|
+
sqlite.exec(`
|
|
69
|
+
CREATE TABLE post_collection (
|
|
70
|
+
site_id TEXT NOT NULL,
|
|
71
|
+
post_id TEXT NOT NULL,
|
|
72
|
+
collection_id TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createTargetTable(sqlite: Database.Database) {
|
|
78
|
+
sqlite.exec(`
|
|
79
|
+
CREATE TABLE thread_collection (
|
|
80
|
+
site_id TEXT NOT NULL,
|
|
81
|
+
thread_id TEXT NOT NULL,
|
|
82
|
+
collection_id TEXT NOT NULL,
|
|
83
|
+
PRIMARY KEY (site_id, thread_id, collection_id)
|
|
84
|
+
);
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function seedLegacyThread(sqlite: Database.Database) {
|
|
89
|
+
sqlite.exec(`
|
|
90
|
+
INSERT INTO site (id) VALUES ('site-1');
|
|
91
|
+
INSERT INTO post (id, site_id, reply_to_id, thread_id) VALUES
|
|
92
|
+
('root', 'site-1', NULL, 'root'),
|
|
93
|
+
('child-1', 'site-1', 'root', 'root'),
|
|
94
|
+
('child-2', 'site-1', 'child-1', 'root');
|
|
95
|
+
INSERT INTO collection (id, site_id) VALUES
|
|
96
|
+
('shared', 'site-1'),
|
|
97
|
+
('child-only', 'site-1');
|
|
98
|
+
INSERT INTO post_collection (site_id, post_id, collection_id) VALUES
|
|
99
|
+
('site-1', 'root', 'shared'),
|
|
100
|
+
('site-1', 'child-1', 'shared'),
|
|
101
|
+
('site-1', 'child-2', 'shared'),
|
|
102
|
+
('site-1', 'child-1', 'child-only');
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function seedSoftDeletedLegacyMembership(sqlite: Database.Database) {
|
|
107
|
+
sqlite.exec(`
|
|
108
|
+
ALTER TABLE post ADD COLUMN deleted_at INTEGER;
|
|
109
|
+
`);
|
|
110
|
+
seedLegacyThread(sqlite);
|
|
111
|
+
sqlite.exec(`
|
|
112
|
+
INSERT INTO post (id, site_id, reply_to_id, thread_id, deleted_at)
|
|
113
|
+
VALUES ('deleted-root', 'site-1', NULL, 'deleted-root', 100);
|
|
114
|
+
INSERT INTO collection (id, site_id)
|
|
115
|
+
VALUES ('deleted-only', 'site-1');
|
|
116
|
+
INSERT INTO post_collection (site_id, post_id, collection_id)
|
|
117
|
+
VALUES ('site-1', 'deleted-root', 'deleted-only');
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function applySoftDeleteAndThreadCollectionCutover(
|
|
122
|
+
sqlite: Database.Database,
|
|
123
|
+
options: { loseChildOnlyMembership?: boolean } = {},
|
|
124
|
+
) {
|
|
125
|
+
sqlite.exec(`
|
|
126
|
+
DELETE FROM post_collection
|
|
127
|
+
WHERE post_id IN (
|
|
128
|
+
SELECT id FROM post WHERE deleted_at IS NOT NULL
|
|
129
|
+
);
|
|
130
|
+
DELETE FROM post WHERE deleted_at IS NOT NULL;
|
|
131
|
+
`);
|
|
132
|
+
createTargetTable(sqlite);
|
|
133
|
+
sqlite.exec(`
|
|
134
|
+
INSERT INTO thread_collection (site_id, thread_id, collection_id)
|
|
135
|
+
SELECT membership.site_id, post.thread_id, membership.collection_id
|
|
136
|
+
FROM post_collection AS membership
|
|
137
|
+
INNER JOIN post
|
|
138
|
+
ON post.site_id = membership.site_id
|
|
139
|
+
AND post.id = membership.post_id
|
|
140
|
+
${options.loseChildOnlyMembership ? "WHERE membership.collection_id <> 'child-only'" : ""}
|
|
141
|
+
GROUP BY membership.site_id, post.thread_id, membership.collection_id;
|
|
142
|
+
DROP TABLE post_collection;
|
|
143
|
+
`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("Thread Collection migration guard", () => {
|
|
147
|
+
it("verifies a fresh database after schema migration", async () => {
|
|
148
|
+
const sqlite = new Database(":memory:");
|
|
149
|
+
const query = createQuery(sqlite);
|
|
150
|
+
|
|
151
|
+
await expect(
|
|
152
|
+
preflightThreadCollectionMigration({
|
|
153
|
+
dialect: "sqlite",
|
|
154
|
+
log: vi.fn(),
|
|
155
|
+
query,
|
|
156
|
+
}),
|
|
157
|
+
).resolves.toEqual({ phase: "fresh" });
|
|
158
|
+
|
|
159
|
+
createDomainTables(sqlite);
|
|
160
|
+
createTargetTable(sqlite);
|
|
161
|
+
|
|
162
|
+
await expect(
|
|
163
|
+
verifyThreadCollectionMigration(
|
|
164
|
+
{ dialect: "sqlite", log: vi.fn(), query },
|
|
165
|
+
{ phase: "fresh" },
|
|
166
|
+
),
|
|
167
|
+
).resolves.toBeUndefined();
|
|
168
|
+
|
|
169
|
+
sqlite.close();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("records and verifies the lossless union before allowing repeat runs", async () => {
|
|
173
|
+
const sqlite = new Database(":memory:");
|
|
174
|
+
createDomainTables(sqlite);
|
|
175
|
+
createLegacyTable(sqlite);
|
|
176
|
+
seedLegacyThread(sqlite);
|
|
177
|
+
const query = createQuery(sqlite);
|
|
178
|
+
|
|
179
|
+
const preflight = await preflightThreadCollectionMigration({
|
|
180
|
+
dialect: "sqlite",
|
|
181
|
+
log: vi.fn(),
|
|
182
|
+
query,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(preflight).toEqual({
|
|
186
|
+
expectedCount: 2,
|
|
187
|
+
phase: "legacy",
|
|
188
|
+
sourceCount: 4,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
createTargetTable(sqlite);
|
|
192
|
+
sqlite.exec(`
|
|
193
|
+
INSERT INTO thread_collection (site_id, thread_id, collection_id)
|
|
194
|
+
SELECT membership.site_id, post.thread_id, membership.collection_id
|
|
195
|
+
FROM post_collection AS membership
|
|
196
|
+
INNER JOIN post
|
|
197
|
+
ON post.site_id = membership.site_id
|
|
198
|
+
AND post.id = membership.post_id
|
|
199
|
+
GROUP BY membership.site_id, post.thread_id, membership.collection_id;
|
|
200
|
+
DROP TABLE post_collection;
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
verifyThreadCollectionMigration(
|
|
205
|
+
{ dialect: "sqlite", log: vi.fn(), query },
|
|
206
|
+
preflight,
|
|
207
|
+
),
|
|
208
|
+
).resolves.toBeUndefined();
|
|
209
|
+
|
|
210
|
+
const repeatPreflight = await preflightThreadCollectionMigration({
|
|
211
|
+
dialect: "sqlite",
|
|
212
|
+
log: vi.fn(),
|
|
213
|
+
query,
|
|
214
|
+
});
|
|
215
|
+
expect(repeatPreflight).toEqual({ phase: "already-migrated" });
|
|
216
|
+
await expect(
|
|
217
|
+
verifyThreadCollectionMigration(
|
|
218
|
+
{ dialect: "sqlite", log: vi.fn(), query },
|
|
219
|
+
repeatPreflight,
|
|
220
|
+
),
|
|
221
|
+
).resolves.toBeUndefined();
|
|
222
|
+
|
|
223
|
+
sqlite.close();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("stops before mutation when legacy memberships reference missing data", async () => {
|
|
227
|
+
const sqlite = new Database(":memory:");
|
|
228
|
+
createDomainTables(sqlite);
|
|
229
|
+
createLegacyTable(sqlite);
|
|
230
|
+
sqlite.exec(`
|
|
231
|
+
INSERT INTO site (id) VALUES ('site-1');
|
|
232
|
+
INSERT INTO collection (id, site_id) VALUES ('collection-1', 'site-1');
|
|
233
|
+
INSERT INTO post_collection (site_id, post_id, collection_id)
|
|
234
|
+
VALUES ('site-1', 'missing-post', 'collection-1');
|
|
235
|
+
`);
|
|
236
|
+
|
|
237
|
+
await expect(
|
|
238
|
+
preflightThreadCollectionMigration({
|
|
239
|
+
dialect: "sqlite",
|
|
240
|
+
log: vi.fn(),
|
|
241
|
+
query: createQuery(sqlite),
|
|
242
|
+
}),
|
|
243
|
+
).rejects.toThrow("missing posts=1");
|
|
244
|
+
expect(
|
|
245
|
+
sqlite
|
|
246
|
+
.prepare(
|
|
247
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'post_collection'",
|
|
248
|
+
)
|
|
249
|
+
.get(),
|
|
250
|
+
).toEqual({ name: "post_collection" });
|
|
251
|
+
|
|
252
|
+
sqlite.close();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("rejects ambiguous and count-mismatched cutover states", async () => {
|
|
256
|
+
const sqlite = new Database(":memory:");
|
|
257
|
+
createDomainTables(sqlite);
|
|
258
|
+
createLegacyTable(sqlite);
|
|
259
|
+
seedLegacyThread(sqlite);
|
|
260
|
+
createTargetTable(sqlite);
|
|
261
|
+
const query = createQuery(sqlite);
|
|
262
|
+
|
|
263
|
+
await expect(
|
|
264
|
+
preflightThreadCollectionMigration({
|
|
265
|
+
dialect: "sqlite",
|
|
266
|
+
log: vi.fn(),
|
|
267
|
+
query,
|
|
268
|
+
}),
|
|
269
|
+
).rejects.toThrow("both post_collection and thread_collection");
|
|
270
|
+
|
|
271
|
+
sqlite.exec(`
|
|
272
|
+
DROP TABLE thread_collection;
|
|
273
|
+
`);
|
|
274
|
+
const preflight = await preflightThreadCollectionMigration({
|
|
275
|
+
dialect: "sqlite",
|
|
276
|
+
log: vi.fn(),
|
|
277
|
+
query,
|
|
278
|
+
});
|
|
279
|
+
createTargetTable(sqlite);
|
|
280
|
+
sqlite.exec(`
|
|
281
|
+
INSERT INTO thread_collection (site_id, thread_id, collection_id)
|
|
282
|
+
VALUES ('site-1', 'root', 'shared');
|
|
283
|
+
DROP TABLE post_collection;
|
|
284
|
+
`);
|
|
285
|
+
|
|
286
|
+
await expect(
|
|
287
|
+
verifyThreadCollectionMigration(
|
|
288
|
+
{ dialect: "sqlite", log: vi.fn(), query },
|
|
289
|
+
preflight,
|
|
290
|
+
),
|
|
291
|
+
).rejects.toThrow("expected rows=2, actual rows=1");
|
|
292
|
+
|
|
293
|
+
sqlite.close();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it.each(["sqlite", "pg"] as const)(
|
|
297
|
+
"allows a %s legacy upgrade to discard collected soft-deleted posts",
|
|
298
|
+
async (dialect) => {
|
|
299
|
+
const sqlite = new Database(":memory:");
|
|
300
|
+
createDomainTables(sqlite);
|
|
301
|
+
createLegacyTable(sqlite);
|
|
302
|
+
seedSoftDeletedLegacyMembership(sqlite);
|
|
303
|
+
const query = createDialectQuery(sqlite, dialect);
|
|
304
|
+
const log = vi.fn();
|
|
305
|
+
|
|
306
|
+
const preflight = await preflightThreadCollectionMigration({
|
|
307
|
+
dialect,
|
|
308
|
+
log,
|
|
309
|
+
query,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(preflight).toEqual({
|
|
313
|
+
expectedCount: 2,
|
|
314
|
+
phase: "legacy",
|
|
315
|
+
sourceCount: 5,
|
|
316
|
+
});
|
|
317
|
+
expect(log).toHaveBeenCalledWith(
|
|
318
|
+
expect.stringContaining(
|
|
319
|
+
"1 soft-deleted post membership will be removed",
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
applySoftDeleteAndThreadCollectionCutover(sqlite);
|
|
324
|
+
|
|
325
|
+
await expect(
|
|
326
|
+
verifyThreadCollectionMigration(
|
|
327
|
+
{ dialect, log: vi.fn(), query },
|
|
328
|
+
preflight,
|
|
329
|
+
),
|
|
330
|
+
).resolves.toBeUndefined();
|
|
331
|
+
|
|
332
|
+
sqlite.close();
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
it.each(["sqlite", "pg"] as const)(
|
|
337
|
+
"still detects a real %s membership loss after anticipated soft deletes",
|
|
338
|
+
async (dialect) => {
|
|
339
|
+
const sqlite = new Database(":memory:");
|
|
340
|
+
createDomainTables(sqlite);
|
|
341
|
+
createLegacyTable(sqlite);
|
|
342
|
+
seedSoftDeletedLegacyMembership(sqlite);
|
|
343
|
+
const query = createDialectQuery(sqlite, dialect);
|
|
344
|
+
const preflight = await preflightThreadCollectionMigration({
|
|
345
|
+
dialect,
|
|
346
|
+
log: vi.fn(),
|
|
347
|
+
query,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
applySoftDeleteAndThreadCollectionCutover(sqlite, {
|
|
351
|
+
loseChildOnlyMembership: true,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await expect(
|
|
355
|
+
verifyThreadCollectionMigration(
|
|
356
|
+
{ dialect, log: vi.fn(), query },
|
|
357
|
+
preflight,
|
|
358
|
+
),
|
|
359
|
+
).rejects.toThrow("expected rows=2, actual rows=1");
|
|
360
|
+
|
|
361
|
+
sqlite.close();
|
|
362
|
+
},
|
|
363
|
+
);
|
|
364
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
CREATE TABLE `thread_collection` (
|
|
2
|
+
`site_id` text NOT NULL,
|
|
3
|
+
`thread_id` text NOT NULL,
|
|
4
|
+
`collection_id` text NOT NULL,
|
|
5
|
+
`created_at` integer NOT NULL,
|
|
6
|
+
`position` integer DEFAULT 0 NOT NULL,
|
|
7
|
+
`pinned_at` integer,
|
|
8
|
+
PRIMARY KEY(`site_id`, `thread_id`, `collection_id`),
|
|
9
|
+
FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
10
|
+
FOREIGN KEY (`thread_id`) REFERENCES `post`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
11
|
+
FOREIGN KEY (`collection_id`) REFERENCES `collection`(`id`) ON UPDATE no action ON DELETE cascade
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
CREATE INDEX `idx_thread_collection_site_collection_id` ON `thread_collection` (`site_id`,`collection_id`);--> statement-breakpoint
|
|
15
|
+
CREATE INDEX `idx_thread_collection_site_collection_created_thread` ON `thread_collection` (`site_id`,`collection_id`,`created_at`,`thread_id`);--> statement-breakpoint
|
|
16
|
+
INSERT INTO `thread_collection` (
|
|
17
|
+
`site_id`,
|
|
18
|
+
`thread_id`,
|
|
19
|
+
`collection_id`,
|
|
20
|
+
`created_at`,
|
|
21
|
+
`position`,
|
|
22
|
+
`pinned_at`
|
|
23
|
+
)
|
|
24
|
+
SELECT
|
|
25
|
+
`post_collection`.`site_id`,
|
|
26
|
+
`post`.`thread_id`,
|
|
27
|
+
`post_collection`.`collection_id`,
|
|
28
|
+
MAX(`post_collection`.`created_at`),
|
|
29
|
+
MIN(`post_collection`.`position`),
|
|
30
|
+
MAX(`post_collection`.`pinned_at`)
|
|
31
|
+
FROM `post_collection`
|
|
32
|
+
INNER JOIN `post`
|
|
33
|
+
ON `post`.`site_id` = `post_collection`.`site_id`
|
|
34
|
+
AND `post`.`id` = `post_collection`.`post_id`
|
|
35
|
+
GROUP BY
|
|
36
|
+
`post_collection`.`site_id`,
|
|
37
|
+
`post`.`thread_id`,
|
|
38
|
+
`post_collection`.`collection_id`;--> statement-breakpoint
|
|
39
|
+
DROP TABLE `post_collection`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `post` ADD `body_html_version` integer DEFAULT 1 NOT NULL;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
2
|
+
CREATE TABLE `__new_nav_item` (
|
|
3
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
4
|
+
`site_id` text NOT NULL,
|
|
5
|
+
`type` text DEFAULT 'link' NOT NULL,
|
|
6
|
+
`system_key` text,
|
|
7
|
+
`collection_id` text,
|
|
8
|
+
`post_id` text,
|
|
9
|
+
`label` text NOT NULL,
|
|
10
|
+
`url` text NOT NULL,
|
|
11
|
+
`placement` text DEFAULT 'header' NOT NULL,
|
|
12
|
+
`position` text DEFAULT 'a0' NOT NULL,
|
|
13
|
+
`created_at` integer NOT NULL,
|
|
14
|
+
`updated_at` integer NOT NULL,
|
|
15
|
+
FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
16
|
+
FOREIGN KEY (`collection_id`) REFERENCES `collection`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
17
|
+
FOREIGN KEY (`post_id`) REFERENCES `post`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
18
|
+
CONSTRAINT "chk_nav_item_type" CHECK("__new_nav_item"."type" IN ('link', 'system', 'collection', 'page')),
|
|
19
|
+
CONSTRAINT "chk_nav_item_placement" CHECK("__new_nav_item"."placement" IN ('header', 'more')),
|
|
20
|
+
CONSTRAINT "chk_nav_item_shape" CHECK((
|
|
21
|
+
"__new_nav_item"."type" = 'link'
|
|
22
|
+
AND "__new_nav_item"."system_key" IS NULL
|
|
23
|
+
AND "__new_nav_item"."collection_id" IS NULL
|
|
24
|
+
AND "__new_nav_item"."post_id" IS NULL
|
|
25
|
+
) OR (
|
|
26
|
+
"__new_nav_item"."type" = 'system'
|
|
27
|
+
AND "__new_nav_item"."system_key" IS NOT NULL
|
|
28
|
+
AND "__new_nav_item"."collection_id" IS NULL
|
|
29
|
+
AND "__new_nav_item"."post_id" IS NULL
|
|
30
|
+
) OR (
|
|
31
|
+
"__new_nav_item"."type" = 'collection'
|
|
32
|
+
AND "__new_nav_item"."system_key" IS NULL
|
|
33
|
+
AND "__new_nav_item"."collection_id" IS NOT NULL
|
|
34
|
+
AND "__new_nav_item"."post_id" IS NULL
|
|
35
|
+
) OR (
|
|
36
|
+
"__new_nav_item"."type" = 'page'
|
|
37
|
+
AND "__new_nav_item"."system_key" IS NULL
|
|
38
|
+
AND "__new_nav_item"."collection_id" IS NULL
|
|
39
|
+
AND "__new_nav_item"."post_id" IS NOT NULL
|
|
40
|
+
))
|
|
41
|
+
);
|
|
42
|
+
--> statement-breakpoint
|
|
43
|
+
INSERT INTO `__new_nav_item`("id", "site_id", "type", "system_key", "collection_id", "post_id", "label", "url", "placement", "position", "created_at", "updated_at") SELECT "id", "site_id", "type", "system_key", "collection_id", NULL, "label", "url", "placement", "position", "created_at", "updated_at" FROM `nav_item`;--> statement-breakpoint
|
|
44
|
+
DROP TABLE `nav_item`;--> statement-breakpoint
|
|
45
|
+
ALTER TABLE `__new_nav_item` RENAME TO `nav_item`;--> statement-breakpoint
|
|
46
|
+
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
|
47
|
+
CREATE UNIQUE INDEX `uq_nav_item_site_position` ON `nav_item` (`site_id`,`position`);--> statement-breakpoint
|
|
48
|
+
CREATE UNIQUE INDEX `uq_nav_item_site_system_key` ON `nav_item` (`site_id`,`system_key`) WHERE "nav_item"."system_key" IS NOT NULL;--> statement-breakpoint
|
|
49
|
+
CREATE UNIQUE INDEX `uq_nav_item_site_collection_id` ON `nav_item` (`site_id`,`collection_id`) WHERE "nav_item"."collection_id" IS NOT NULL;--> statement-breakpoint
|
|
50
|
+
CREATE UNIQUE INDEX `uq_nav_item_site_post_id` ON `nav_item` (`site_id`,`post_id`) WHERE "nav_item"."post_id" IS NOT NULL;
|