@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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Bindings } from "../types.js";
|
|
2
2
|
import { getConfiguredSingleSitePathPrefix } from "./env.js";
|
|
3
|
+
import { POST_BODY_HTML_VERSION } from "./post-body-html.js";
|
|
4
|
+
import { isRssFeedPath } from "./feed-path.js";
|
|
3
5
|
import { stripSitePathPrefix } from "./url.js";
|
|
4
6
|
|
|
5
7
|
const TRACKING_QUERY_PARAMS = [
|
|
@@ -21,6 +23,7 @@ const TRACKING_QUERY_PARAMS = [
|
|
|
21
23
|
|
|
22
24
|
const AUTH_COOKIE_PATTERN =
|
|
23
25
|
/(?:^|;\s*)(?:__Host-)?(?:__Secure-)?better-auth[.-][^=]+=/;
|
|
26
|
+
const BODY_HTML_CACHE_VERSION_PARAM = "__jant_body_html";
|
|
24
27
|
|
|
25
28
|
type ExecutionContextLike = {
|
|
26
29
|
waitUntil(promise: Promise<unknown>): void;
|
|
@@ -50,13 +53,7 @@ export interface WorkerResponseCacheOptions {
|
|
|
50
53
|
* @returns `true` when the request should be eligible for Worker response caching
|
|
51
54
|
*/
|
|
52
55
|
export function isWorkerResponseCachePath(path: string): boolean {
|
|
53
|
-
if (path
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (path === "/archive/feed" || path.startsWith("/archive/feed/")) {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
56
|
+
if (isRssFeedPath(path)) return true;
|
|
60
57
|
|
|
61
58
|
if (path === "/sitemap.xml" || path === "/robots.txt") {
|
|
62
59
|
return true;
|
|
@@ -66,16 +63,6 @@ export function isWorkerResponseCachePath(path: string): boolean {
|
|
|
66
63
|
return true;
|
|
67
64
|
}
|
|
68
65
|
|
|
69
|
-
if (
|
|
70
|
-
path.endsWith("/feed") &&
|
|
71
|
-
!path.startsWith("/api/") &&
|
|
72
|
-
!path.startsWith("/settings") &&
|
|
73
|
-
!path.startsWith("/compose") &&
|
|
74
|
-
!path.startsWith("/_")
|
|
75
|
-
) {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
66
|
return false;
|
|
80
67
|
}
|
|
81
68
|
|
|
@@ -83,7 +70,9 @@ export function isWorkerResponseCachePath(path: string): boolean {
|
|
|
83
70
|
* Normalize a public request URL for Worker cache lookup.
|
|
84
71
|
*
|
|
85
72
|
* Tracking parameters are removed so equivalent feed requests converge on one
|
|
86
|
-
* cache entry while functional filters like `format` remain intact.
|
|
73
|
+
* cache entry while functional filters like `format` remain intact. A private
|
|
74
|
+
* body-HTML contract version invalidates cached feed markup across renderer
|
|
75
|
+
* deployments; it is only used for Cache API lookup and never forwarded.
|
|
87
76
|
*
|
|
88
77
|
* @param requestUrl - Full public request URL
|
|
89
78
|
* @returns Normalized cache key URL
|
|
@@ -95,6 +84,10 @@ export function normalizeWorkerCacheKeyUrl(requestUrl: string): string {
|
|
|
95
84
|
url.searchParams.delete(param);
|
|
96
85
|
}
|
|
97
86
|
|
|
87
|
+
url.searchParams.set(
|
|
88
|
+
BODY_HTML_CACHE_VERSION_PARAM,
|
|
89
|
+
String(POST_BODY_HTML_VERSION),
|
|
90
|
+
);
|
|
98
91
|
url.searchParams.sort();
|
|
99
92
|
return url.toString();
|
|
100
93
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../__tests__/helpers/app.js";
|
|
3
|
+
import { isRssFeedPath } from "../../lib/feed-path.js";
|
|
4
|
+
import {
|
|
5
|
+
requirePublicApiAccess,
|
|
6
|
+
requirePublicApiEnabled,
|
|
7
|
+
requireRssFeedsEnabled,
|
|
8
|
+
} from "../public-content-access.js";
|
|
9
|
+
|
|
10
|
+
describe("public content access policies", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
"/feed",
|
|
13
|
+
"/feed/latest",
|
|
14
|
+
"/feed/atom.xml",
|
|
15
|
+
"/latest/feed",
|
|
16
|
+
"/featured/feed/atom.xml",
|
|
17
|
+
"/archive/feed",
|
|
18
|
+
"/reading/feed",
|
|
19
|
+
"/settings-notes/feed",
|
|
20
|
+
"/collections/reading+movies/feed",
|
|
21
|
+
])("recognizes %s as a feed path", (path) => {
|
|
22
|
+
expect(isRssFeedPath(path)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it.each([
|
|
26
|
+
"/",
|
|
27
|
+
"/archive",
|
|
28
|
+
"/api/example/feed",
|
|
29
|
+
"/settings/feed",
|
|
30
|
+
"/compose/feed",
|
|
31
|
+
"/_/theme/feed",
|
|
32
|
+
])("does not treat %s as a feed path", (path) => {
|
|
33
|
+
expect(isRssFeedPath(path)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("requires authentication for JSON reads when public access is off", async () => {
|
|
37
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
38
|
+
await services.settings.set("PUBLIC_API_ENABLED", "false");
|
|
39
|
+
app.get("/api/data", requirePublicApiAccess(), (c) => c.json({ ok: true }));
|
|
40
|
+
|
|
41
|
+
const response = await app.request("/api/data");
|
|
42
|
+
|
|
43
|
+
expect(response.status).toBe(401);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns 404 for the dedicated public API even with a session", async () => {
|
|
47
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
48
|
+
await services.settings.set("PUBLIC_API_ENABLED", "false");
|
|
49
|
+
app.get("/api/public/data", requirePublicApiEnabled(), (c) =>
|
|
50
|
+
c.json({ ok: true }),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const response = await app.request("/api/public/data");
|
|
54
|
+
|
|
55
|
+
expect(response.status).toBe(404);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("preserves session access when public JSON reads are off", async () => {
|
|
59
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
60
|
+
await services.settings.set("PUBLIC_API_ENABLED", "false");
|
|
61
|
+
app.get("/api/data", requirePublicApiAccess(), (c) => c.json({ ok: true }));
|
|
62
|
+
|
|
63
|
+
const response = await app.request("/api/data");
|
|
64
|
+
|
|
65
|
+
expect(response.status).toBe(200);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("preserves Bearer-token access when public JSON reads are off", async () => {
|
|
69
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
70
|
+
await services.settings.set("PUBLIC_API_ENABLED", "false");
|
|
71
|
+
const { plaintext } = await services.apiTokens.create("Test client");
|
|
72
|
+
app.get("/api/data", requirePublicApiAccess(), (c) => c.json({ ok: true }));
|
|
73
|
+
|
|
74
|
+
const response = await app.request("/api/data", {
|
|
75
|
+
headers: { Authorization: `Bearer ${plaintext}` },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns 404 for feed paths when feed publishing is off", async () => {
|
|
82
|
+
const { app, services } = createTestApp();
|
|
83
|
+
await services.settings.set("RSS_FEEDS_ENABLED", "false");
|
|
84
|
+
app.use("*", requireRssFeedsEnabled());
|
|
85
|
+
app.get("*", (c) => c.text("page"));
|
|
86
|
+
|
|
87
|
+
expect((await app.request("/settings-notes/feed")).status).toBe(404);
|
|
88
|
+
expect((await app.request("/settings/feed")).status).toBe(200);
|
|
89
|
+
expect((await app.request("/settings-notes")).status).toBe(200);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/middleware/config.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { buildThemeStyle, resolveBuiltinTheme } from "../lib/theme.js";
|
|
19
19
|
import {
|
|
20
20
|
BUILTIN_FONT_THEMES,
|
|
21
|
-
|
|
21
|
+
getCjkFontCssVariables,
|
|
22
22
|
getFontThemeCssVariables,
|
|
23
23
|
} from "../ui/font-themes.js";
|
|
24
24
|
|
|
@@ -52,7 +52,7 @@ export function withConfig(): MiddlewareHandler<Env> {
|
|
|
52
52
|
(f) => f.id === appConfig.fontThemeId,
|
|
53
53
|
);
|
|
54
54
|
const fontOverrides = {
|
|
55
|
-
...
|
|
55
|
+
...getCjkFontCssVariables(appConfig.siteLanguage, appConfig.cjkSerifFont),
|
|
56
56
|
...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
|
|
57
57
|
};
|
|
58
58
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public machine-readable content access policies.
|
|
3
|
+
*
|
|
4
|
+
* These guards run after configuration has been resolved. They keep public
|
|
5
|
+
* HTML rendering independent from the optional JSON API and Atom surfaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MiddlewareHandler } from "hono";
|
|
9
|
+
import type { Bindings } from "../types.js";
|
|
10
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
11
|
+
import { isRssFeedPath } from "../lib/feed-path.js";
|
|
12
|
+
import { requireAuthApi } from "./auth.js";
|
|
13
|
+
|
|
14
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Return 404 when the dedicated public JSON API is disabled.
|
|
18
|
+
*
|
|
19
|
+
* Unlike shared read endpoints used by the dashboard, `/api/public/*` has an
|
|
20
|
+
* authenticated alternative under `/api/posts`, so session and token auth do
|
|
21
|
+
* not bypass this switch.
|
|
22
|
+
*
|
|
23
|
+
* @returns Hono middleware that makes the public API unavailable when disabled
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* app.use("/api/public/*", requirePublicApiEnabled());
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function requirePublicApiEnabled(): MiddlewareHandler<Env> {
|
|
30
|
+
return async (c, next) => {
|
|
31
|
+
if (
|
|
32
|
+
(c.req.method !== "GET" && c.req.method !== "HEAD") ||
|
|
33
|
+
c.var.appConfig.publicApiEnabled
|
|
34
|
+
) {
|
|
35
|
+
return next();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return c.notFound();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Require normal authentication for shared JSON reads when public access is off.
|
|
44
|
+
*
|
|
45
|
+
* @returns Hono middleware that preserves session and Bearer-token access
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* app.get("/api/search", requirePublicApiAccess(), searchHandler);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function requirePublicApiAccess(): MiddlewareHandler<Env> {
|
|
52
|
+
const requireAuthentication = requireAuthApi();
|
|
53
|
+
|
|
54
|
+
return async (c, next) => {
|
|
55
|
+
if (
|
|
56
|
+
(c.req.method !== "GET" && c.req.method !== "HEAD") ||
|
|
57
|
+
c.var.appConfig.publicApiEnabled
|
|
58
|
+
) {
|
|
59
|
+
return next();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return requireAuthentication(c, next);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Return 404 for Atom endpoints when feed publishing is disabled.
|
|
68
|
+
*
|
|
69
|
+
* @returns Hono middleware that leaves non-feed requests unchanged
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* app.use("*", requireRssFeedsEnabled());
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function requireRssFeedsEnabled(): MiddlewareHandler<Env> {
|
|
76
|
+
return async (c, next) => {
|
|
77
|
+
if (
|
|
78
|
+
(c.req.method !== "GET" && c.req.method !== "HEAD") ||
|
|
79
|
+
c.var.appConfig.rssFeedsEnabled ||
|
|
80
|
+
!isRssFeedPath(new URL(c.req.url).pathname)
|
|
81
|
+
) {
|
|
82
|
+
return next();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return c.notFound();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -11,6 +11,8 @@ describe("jant export", () => {
|
|
|
11
11
|
const tempDirs: string[] = [];
|
|
12
12
|
const originalEnv = process.env.DATABASE_URL;
|
|
13
13
|
const siteId = "sit_01jpyz1h3v4m7s2k8r5c9t0qbd";
|
|
14
|
+
const collectionId = "col_01jpyz1q6s4m8v2k5t9c3b7qdh";
|
|
15
|
+
const postId = "pst_01jpyz20kt5n9r3k8t6c4d2qhf";
|
|
14
16
|
|
|
15
17
|
afterEach(async () => {
|
|
16
18
|
if (originalEnv === undefined) {
|
|
@@ -44,6 +46,15 @@ describe("jant export", () => {
|
|
|
44
46
|
|
|
45
47
|
INSERT INTO "site_setting" ("site_id", "key", "value", "updated_at")
|
|
46
48
|
VALUES ('${siteId}', 'SITE_NAME', 'Test Site', 1773753605);
|
|
49
|
+
|
|
50
|
+
INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at")
|
|
51
|
+
VALUES ('${collectionId}', '${siteId}', 'Ideas', 'newest', 1773753606, 1773753606);
|
|
52
|
+
|
|
53
|
+
INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
|
|
54
|
+
VALUES ('${postId}', '${siteId}', 'note', 'published', 'public', '${postId}', 1773753607, 1773753607);
|
|
55
|
+
|
|
56
|
+
INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at", "position", "pinned_at")
|
|
57
|
+
VALUES ('${siteId}', '${postId}', '${collectionId}', 1773753608, 3, NULL);
|
|
47
58
|
`);
|
|
48
59
|
} finally {
|
|
49
60
|
sqlite.close();
|
|
@@ -58,6 +69,9 @@ describe("jant export", () => {
|
|
|
58
69
|
expect(output).toContain(
|
|
59
70
|
`INSERT INTO "site_setting" ("site_id", "key", "value", "updated_at") VALUES('${siteId}', 'SITE_NAME', 'Test Site', 1773753605);`,
|
|
60
71
|
);
|
|
72
|
+
expect(output).toContain(
|
|
73
|
+
`INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${siteId}', '${postId}', '${collectionId}', 1773753608, 3, NULL);`,
|
|
74
|
+
);
|
|
61
75
|
expect(output).not.toContain("__drizzle_migrations");
|
|
62
76
|
expect(logSpy).toHaveBeenCalledWith(
|
|
63
77
|
`Exported Node database to ${exportPath}`,
|
|
@@ -83,6 +83,12 @@ describe("jant migrate", () => {
|
|
|
83
83
|
vi.doMock("../../../bin/lib/load-node-runtime.js", () => ({
|
|
84
84
|
loadNodeRuntime: vi.fn(async () => ({ migrate })),
|
|
85
85
|
}));
|
|
86
|
+
vi.doMock("../../../bin/lib/thread-collection-migration.js", () => ({
|
|
87
|
+
preflightThreadCollectionMigration: vi.fn(async () => ({
|
|
88
|
+
phase: "fresh",
|
|
89
|
+
})),
|
|
90
|
+
verifyThreadCollectionMigration: vi.fn(async () => undefined),
|
|
91
|
+
}));
|
|
86
92
|
|
|
87
93
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
88
94
|
|
|
@@ -16,6 +16,8 @@ const SNAPSHOT_COLLECTION_ID = "col_01jpyy08bc4w2h8r7m3q5t9kdn";
|
|
|
16
16
|
const SNAPSHOT_NAV_ID = "nav_01jpyy0gqv4m7r2k8s5c1t9bdh";
|
|
17
17
|
const SNAPSHOT_DIRECTORY_ITEM_ID = "cdi_01jpyy0r6s3m8v1k5t9q2b4gcn";
|
|
18
18
|
const SNAPSHOT_POST_ID = "pst_01jpyy18fh4w2m7r8k3c5t9qdn";
|
|
19
|
+
const SNAPSHOT_REPLY_ID = "pst_01jpyy1e6s4m8v2k5t9c3b7qdh";
|
|
20
|
+
const SNAPSHOT_SECOND_COLLECTION_ID = "col_01jpyy0c4s7m8r1k5t9b3q6dgh";
|
|
19
21
|
const SNAPSHOT_PATH_ID = "pth_01jpyy1k2v6m4s8r1t5c9b3qgh";
|
|
20
22
|
const SNAPSHOT_MEDIA_ID = "med_01jpyy1vxh4m7s2k8r5c9t3qbn";
|
|
21
23
|
const SNAPSHOT_AVATAR_MEDIA_ID = "med_01jpyy1zs6m4v8r2k5t9c3b7qh";
|
|
@@ -145,7 +147,7 @@ describe("jant site snapshot export/import", () => {
|
|
|
145
147
|
'${SNAPSHOT_POST_ID}', 1774009200, 1774009200, 1774009200, 1774009200
|
|
146
148
|
);
|
|
147
149
|
|
|
148
|
-
INSERT INTO "
|
|
150
|
+
INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at")
|
|
149
151
|
VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_COLLECTION_ID}', 1774009200);
|
|
150
152
|
|
|
151
153
|
INSERT INTO "path_registry" (
|
|
@@ -240,7 +242,7 @@ describe("jant site snapshot export/import", () => {
|
|
|
240
242
|
);
|
|
241
243
|
expect(meta).toEqual({
|
|
242
244
|
format: "jant-site-snapshot",
|
|
243
|
-
version:
|
|
245
|
+
version: 2,
|
|
244
246
|
dialect: "sqlite",
|
|
245
247
|
site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
|
|
246
248
|
});
|
|
@@ -281,6 +283,16 @@ describe("jant site snapshot export/import", () => {
|
|
|
281
283
|
poster_key: SNAPSHOT_POSTER_KEY,
|
|
282
284
|
});
|
|
283
285
|
|
|
286
|
+
const collectionMembership = verifySqlite
|
|
287
|
+
.prepare(
|
|
288
|
+
`SELECT "thread_id", "collection_id" FROM "thread_collection" WHERE "site_id" = '${SNAPSHOT_SITE_ID}'`,
|
|
289
|
+
)
|
|
290
|
+
.get();
|
|
291
|
+
expect(collectionMembership).toEqual({
|
|
292
|
+
thread_id: SNAPSHOT_POST_ID,
|
|
293
|
+
collection_id: SNAPSHOT_COLLECTION_ID,
|
|
294
|
+
});
|
|
295
|
+
|
|
284
296
|
const siteName = verifySqlite
|
|
285
297
|
.prepare(
|
|
286
298
|
`SELECT "value" FROM "site_setting" WHERE "site_id" = '${SNAPSHOT_SITE_ID}' AND "key" = 'SITE_NAME'`,
|
|
@@ -339,6 +351,197 @@ describe("jant site snapshot export/import", () => {
|
|
|
339
351
|
);
|
|
340
352
|
});
|
|
341
353
|
|
|
354
|
+
it("imports v1 per-post Collection rows as a Thread union", async () => {
|
|
355
|
+
const root = await mkdtemp(join(tmpdir(), "jant-site-snapshot-v1-"));
|
|
356
|
+
tempDirs.push(root);
|
|
357
|
+
const targetDbPath = join(root, "target.sqlite");
|
|
358
|
+
const snapshotPath = join(root, "snapshot");
|
|
359
|
+
await migrate({ DATABASE_URL: `file:${targetDbPath}` } as Bindings);
|
|
360
|
+
|
|
361
|
+
const sqlite = new Database(targetDbPath);
|
|
362
|
+
try {
|
|
363
|
+
sqlite.exec(`
|
|
364
|
+
INSERT INTO "site" ("id", "key", "status", "created_at", "updated_at")
|
|
365
|
+
VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_SITE_KEY}', 'active', 1, 1);
|
|
366
|
+
`);
|
|
367
|
+
} finally {
|
|
368
|
+
sqlite.close();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await mkdir(snapshotPath, { recursive: true });
|
|
372
|
+
await writeFile(
|
|
373
|
+
join(snapshotPath, "meta.json"),
|
|
374
|
+
JSON.stringify({
|
|
375
|
+
format: "jant-site-snapshot",
|
|
376
|
+
version: 1,
|
|
377
|
+
dialect: "sqlite",
|
|
378
|
+
site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
await writeFile(
|
|
382
|
+
join(snapshotPath, "db.sql"),
|
|
383
|
+
`
|
|
384
|
+
INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at") VALUES('${SNAPSHOT_COLLECTION_ID}', '${SNAPSHOT_SITE_ID}', 'Ideas', 'newest', 10, 10);
|
|
385
|
+
INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at") VALUES('${SNAPSHOT_SECOND_COLLECTION_ID}', '${SNAPSHOT_SITE_ID}', 'Walks', 'newest', 10, 10);
|
|
386
|
+
INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "reply_to_id", "thread_id", "created_at", "updated_at") VALUES('${SNAPSHOT_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', NULL, '${SNAPSHOT_POST_ID}', 10, 10);
|
|
387
|
+
INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "reply_to_id", "thread_id", "created_at", "updated_at") VALUES('${SNAPSHOT_REPLY_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_POST_ID}', 11, 11);
|
|
388
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_COLLECTION_ID}', 20, 5, 30);
|
|
389
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_REPLY_ID}', '${SNAPSHOT_COLLECTION_ID}', 40, 2, 35);
|
|
390
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_REPLY_ID}', '${SNAPSHOT_SECOND_COLLECTION_ID}', 25, 7, NULL);
|
|
391
|
+
`,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, join(root, "target-media"));
|
|
395
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
396
|
+
const { run: runImport } =
|
|
397
|
+
await import("../../../bin/commands/site/snapshot/import.js");
|
|
398
|
+
await runImport(["--path", snapshotPath, "--replace"]);
|
|
399
|
+
|
|
400
|
+
const verifySqlite = new Database(targetDbPath, { readonly: true });
|
|
401
|
+
try {
|
|
402
|
+
const rows = verifySqlite
|
|
403
|
+
.prepare(
|
|
404
|
+
`SELECT "thread_id", "collection_id", "created_at", "position", "pinned_at" FROM "thread_collection" ORDER BY "collection_id"`,
|
|
405
|
+
)
|
|
406
|
+
.all();
|
|
407
|
+
expect(rows).toEqual([
|
|
408
|
+
{
|
|
409
|
+
thread_id: SNAPSHOT_POST_ID,
|
|
410
|
+
collection_id: SNAPSHOT_COLLECTION_ID,
|
|
411
|
+
created_at: 40,
|
|
412
|
+
position: 2,
|
|
413
|
+
pinned_at: 35,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
thread_id: SNAPSHOT_POST_ID,
|
|
417
|
+
collection_id: SNAPSHOT_SECOND_COLLECTION_ID,
|
|
418
|
+
created_at: 25,
|
|
419
|
+
position: 7,
|
|
420
|
+
pinned_at: null,
|
|
421
|
+
},
|
|
422
|
+
]);
|
|
423
|
+
} finally {
|
|
424
|
+
verifySqlite.close();
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("rejects malformed v1 memberships before clearing target content", async () => {
|
|
429
|
+
const root = await mkdtemp(
|
|
430
|
+
join(tmpdir(), "jant-site-snapshot-v1-preflight-"),
|
|
431
|
+
);
|
|
432
|
+
tempDirs.push(root);
|
|
433
|
+
const targetDbPath = join(root, "target.sqlite");
|
|
434
|
+
const snapshotPath = join(root, "snapshot");
|
|
435
|
+
await migrate({ DATABASE_URL: `file:${targetDbPath}` } as Bindings);
|
|
436
|
+
|
|
437
|
+
const sqlite = new Database(targetDbPath);
|
|
438
|
+
try {
|
|
439
|
+
sqlite.exec(`
|
|
440
|
+
INSERT INTO "site" ("id", "key", "status", "created_at", "updated_at")
|
|
441
|
+
VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_SITE_KEY}', 'active', 1, 1);
|
|
442
|
+
INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
|
|
443
|
+
VALUES ('${SNAPSHOT_OLD_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_OLD_POST_ID}', 1, 1);
|
|
444
|
+
`);
|
|
445
|
+
} finally {
|
|
446
|
+
sqlite.close();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
await mkdir(snapshotPath, { recursive: true });
|
|
450
|
+
await writeFile(
|
|
451
|
+
join(snapshotPath, "meta.json"),
|
|
452
|
+
JSON.stringify({
|
|
453
|
+
format: "jant-site-snapshot",
|
|
454
|
+
version: 1,
|
|
455
|
+
dialect: "sqlite",
|
|
456
|
+
site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
await writeFile(
|
|
460
|
+
join(snapshotPath, "db.sql"),
|
|
461
|
+
`INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('${SNAPSHOT_SITE_ID}', 'pst_missing', '${SNAPSHOT_COLLECTION_ID}', 10, 0, NULL);`,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, join(root, "target-media"));
|
|
465
|
+
const { run: runImport } =
|
|
466
|
+
await import("../../../bin/commands/site/snapshot/import.js");
|
|
467
|
+
await expect(
|
|
468
|
+
runImport(["--path", snapshotPath, "--replace"]),
|
|
469
|
+
).rejects.toThrow(/references missing post pst_missing/);
|
|
470
|
+
|
|
471
|
+
const verifySqlite = new Database(targetDbPath, { readonly: true });
|
|
472
|
+
try {
|
|
473
|
+
expect(
|
|
474
|
+
verifySqlite
|
|
475
|
+
.prepare(
|
|
476
|
+
`SELECT COUNT(*) FROM "post" WHERE "id" = '${SNAPSHOT_OLD_POST_ID}'`,
|
|
477
|
+
)
|
|
478
|
+
.pluck()
|
|
479
|
+
.get(),
|
|
480
|
+
).toBe(1);
|
|
481
|
+
} finally {
|
|
482
|
+
verifySqlite.close();
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("rolls back a Node snapshot replacement when SQL replay fails", async () => {
|
|
487
|
+
const root = await mkdtemp(
|
|
488
|
+
join(tmpdir(), "jant-site-snapshot-atomic-replace-"),
|
|
489
|
+
);
|
|
490
|
+
tempDirs.push(root);
|
|
491
|
+
const targetDbPath = join(root, "target.sqlite");
|
|
492
|
+
const snapshotPath = join(root, "snapshot");
|
|
493
|
+
await migrate({ DATABASE_URL: `file:${targetDbPath}` } as Bindings);
|
|
494
|
+
|
|
495
|
+
const sqlite = new Database(targetDbPath);
|
|
496
|
+
try {
|
|
497
|
+
sqlite.exec(`
|
|
498
|
+
INSERT INTO "site" ("id", "key", "status", "created_at", "updated_at")
|
|
499
|
+
VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_SITE_KEY}', 'active', 1, 1);
|
|
500
|
+
INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
|
|
501
|
+
VALUES ('${SNAPSHOT_OLD_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_OLD_POST_ID}', 1, 1);
|
|
502
|
+
`);
|
|
503
|
+
} finally {
|
|
504
|
+
sqlite.close();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
await mkdir(snapshotPath, { recursive: true });
|
|
508
|
+
await writeFile(
|
|
509
|
+
join(snapshotPath, "meta.json"),
|
|
510
|
+
JSON.stringify({
|
|
511
|
+
format: "jant-site-snapshot",
|
|
512
|
+
version: 2,
|
|
513
|
+
dialect: "sqlite",
|
|
514
|
+
site: { id: SNAPSHOT_SITE_ID, key: SNAPSHOT_SITE_KEY },
|
|
515
|
+
}),
|
|
516
|
+
);
|
|
517
|
+
await writeFile(
|
|
518
|
+
join(snapshotPath, "db.sql"),
|
|
519
|
+
`
|
|
520
|
+
INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "thread_id", "created_at", "updated_at")
|
|
521
|
+
VALUES ('${SNAPSHOT_POST_ID}', '${SNAPSHOT_SITE_ID}', 'note', 'published', 'public', '${SNAPSHOT_POST_ID}', 2, 2);
|
|
522
|
+
INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at")
|
|
523
|
+
VALUES ('${SNAPSHOT_SITE_ID}', '${SNAPSHOT_POST_ID}', '${SNAPSHOT_COLLECTION_ID}', 2);
|
|
524
|
+
`,
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
useLocalSnapshotRuntime(`file:${targetDbPath}`, join(root, "target-media"));
|
|
528
|
+
const { run: runImport } =
|
|
529
|
+
await import("../../../bin/commands/site/snapshot/import.js");
|
|
530
|
+
await expect(
|
|
531
|
+
runImport(["--path", snapshotPath, "--replace"]),
|
|
532
|
+
).rejects.toThrow(/FOREIGN KEY constraint failed/);
|
|
533
|
+
|
|
534
|
+
const verifySqlite = new Database(targetDbPath, { readonly: true });
|
|
535
|
+
try {
|
|
536
|
+
const postIds = verifySqlite
|
|
537
|
+
.prepare(`SELECT "id" FROM "post" WHERE "site_id" = ? ORDER BY "id"`)
|
|
538
|
+
.all(SNAPSHOT_SITE_ID);
|
|
539
|
+
expect(postIds).toEqual([{ id: SNAPSHOT_OLD_POST_ID }]);
|
|
540
|
+
} finally {
|
|
541
|
+
verifySqlite.close();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
342
545
|
it("skips downloading storage objects when --skip-objects is passed", async () => {
|
|
343
546
|
const root = await mkdtemp(
|
|
344
547
|
join(tmpdir(), "jant-site-snapshot-skip-objects-"),
|