@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
|
@@ -840,6 +840,20 @@ async function loadSiteConfig(rootDir) {
|
|
|
840
840
|
params.show_header_avatar ?? jantData.show_header_avatar,
|
|
841
841
|
),
|
|
842
842
|
noindex: coerceBoolean(params.noindex ?? jantData.noindex),
|
|
843
|
+
public_api_enabled:
|
|
844
|
+
params.public_api_enabled === undefined &&
|
|
845
|
+
jantData.public_api_enabled === undefined
|
|
846
|
+
? true
|
|
847
|
+
: coerceBoolean(
|
|
848
|
+
params.public_api_enabled ?? jantData.public_api_enabled,
|
|
849
|
+
),
|
|
850
|
+
rss_feeds_enabled:
|
|
851
|
+
params.rss_feeds_enabled === undefined &&
|
|
852
|
+
jantData.rss_feeds_enabled === undefined
|
|
853
|
+
? true
|
|
854
|
+
: coerceBoolean(
|
|
855
|
+
params.rss_feeds_enabled ?? jantData.rss_feeds_enabled,
|
|
856
|
+
),
|
|
843
857
|
site_footer_markdown:
|
|
844
858
|
typeof jantData.site_footer_markdown === "string"
|
|
845
859
|
? jantData.site_footer_markdown
|
|
@@ -896,6 +910,8 @@ function buildSettingsUpdatesFromConfig(siteConfig, customCss = "") {
|
|
|
896
910
|
SITE_FOOTER: String(jant.site_footer_markdown || ""),
|
|
897
911
|
SHOW_JANT_BRANDING_ON_HOME: jant.show_jant_branding_on_home ? "true" : "",
|
|
898
912
|
NOINDEX: jant.noindex ? "true" : "",
|
|
913
|
+
PUBLIC_API_ENABLED: jant.public_api_enabled === false ? "false" : "true",
|
|
914
|
+
RSS_FEEDS_ENABLED: jant.rss_feeds_enabled === false ? "false" : "true",
|
|
899
915
|
SHOW_HEADER_AVATAR: jant.show_header_avatar ? "true" : "",
|
|
900
916
|
THEME: themeId && themeId !== defaultThemeId ? themeId : "",
|
|
901
917
|
FONT_THEME: fontThemeId && fontThemeId !== "default" ? fontThemeId : "",
|
|
@@ -1615,7 +1631,7 @@ async function walkHugoContent(rootDir) {
|
|
|
1615
1631
|
}
|
|
1616
1632
|
|
|
1617
1633
|
/**
|
|
1618
|
-
* Resolve the collection memberships listed in a bundle's top-level
|
|
1634
|
+
* Resolve the collection memberships listed in a post bundle's top-level
|
|
1619
1635
|
* `collections:` front-matter array into `{collectionId, createdAt,
|
|
1620
1636
|
* position, pinnedAt}` records the post-create APIs expect.
|
|
1621
1637
|
*
|
|
@@ -1634,14 +1650,16 @@ function resolveCollectionMemberships(frontMatter, collectionSlugToId) {
|
|
|
1634
1650
|
const id = collectionSlugToId.get(raw.slug);
|
|
1635
1651
|
if (!id) continue;
|
|
1636
1652
|
const entry = { collectionId: id };
|
|
1637
|
-
|
|
1638
|
-
|
|
1653
|
+
const createdAt = parseImportTimestamp(raw.collected_at);
|
|
1654
|
+
if (createdAt !== null) {
|
|
1655
|
+
entry.createdAt = createdAt;
|
|
1639
1656
|
}
|
|
1640
|
-
if (typeof raw.position === "number") {
|
|
1657
|
+
if (typeof raw.position === "number" && Number.isFinite(raw.position)) {
|
|
1641
1658
|
entry.position = raw.position;
|
|
1642
1659
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1660
|
+
const pinnedAt = parseImportTimestamp(raw.pinned_at);
|
|
1661
|
+
if (pinnedAt !== null) {
|
|
1662
|
+
entry.pinnedAt = pinnedAt;
|
|
1645
1663
|
}
|
|
1646
1664
|
entries.push(entry);
|
|
1647
1665
|
ids.push(id);
|
|
@@ -1649,6 +1667,59 @@ function resolveCollectionMemberships(frontMatter, collectionSlugToId) {
|
|
|
1649
1667
|
return { entries, ids };
|
|
1650
1668
|
}
|
|
1651
1669
|
|
|
1670
|
+
/**
|
|
1671
|
+
* Resolve one Thread's collection memberships from its root and every reply.
|
|
1672
|
+
*
|
|
1673
|
+
* Current exports write `collections:` only on the root bundle. Older exports
|
|
1674
|
+
* wrote it per post, so import must take the union before creating the root.
|
|
1675
|
+
* Duplicate memberships retain all historical information by taking the
|
|
1676
|
+
* greatest collected/pinned timestamps and the smallest position.
|
|
1677
|
+
*/
|
|
1678
|
+
function resolveThreadCollectionMemberships(rootBundle, collectionSlugToId) {
|
|
1679
|
+
const byCollectionId = new Map();
|
|
1680
|
+
const bundles = [rootBundle, ...(rootBundle.children ?? [])];
|
|
1681
|
+
|
|
1682
|
+
for (const bundle of bundles) {
|
|
1683
|
+
const { entries } = resolveCollectionMemberships(
|
|
1684
|
+
bundle.frontMatter,
|
|
1685
|
+
collectionSlugToId,
|
|
1686
|
+
);
|
|
1687
|
+
|
|
1688
|
+
for (const entry of entries) {
|
|
1689
|
+
const current = byCollectionId.get(entry.collectionId);
|
|
1690
|
+
if (!current) {
|
|
1691
|
+
byCollectionId.set(entry.collectionId, { ...entry });
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (entry.createdAt !== undefined) {
|
|
1696
|
+
current.createdAt =
|
|
1697
|
+
current.createdAt === undefined
|
|
1698
|
+
? entry.createdAt
|
|
1699
|
+
: Math.max(current.createdAt, entry.createdAt);
|
|
1700
|
+
}
|
|
1701
|
+
if (entry.position !== undefined) {
|
|
1702
|
+
current.position =
|
|
1703
|
+
current.position === undefined
|
|
1704
|
+
? entry.position
|
|
1705
|
+
: Math.min(current.position, entry.position);
|
|
1706
|
+
}
|
|
1707
|
+
if (entry.pinnedAt !== undefined) {
|
|
1708
|
+
current.pinnedAt =
|
|
1709
|
+
current.pinnedAt === undefined
|
|
1710
|
+
? entry.pinnedAt
|
|
1711
|
+
: Math.max(current.pinnedAt, entry.pinnedAt);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const entries = [...byCollectionId.values()];
|
|
1717
|
+
return {
|
|
1718
|
+
entries,
|
|
1719
|
+
ids: entries.map((entry) => entry.collectionId),
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1652
1723
|
function parseImportTimestamp(value) {
|
|
1653
1724
|
if (typeof value !== "string" || value.trim() === "") return null;
|
|
1654
1725
|
const timestamp = Date.parse(value);
|
|
@@ -1795,6 +1866,7 @@ export const __test__ = {
|
|
|
1795
1866
|
walkHugoContent,
|
|
1796
1867
|
mediaSpecFromJantMedia,
|
|
1797
1868
|
resolveCollectionMemberships,
|
|
1869
|
+
resolveThreadCollectionMemberships,
|
|
1798
1870
|
buildPostPayloadFromBundle,
|
|
1799
1871
|
shouldImportReplyQuietly,
|
|
1800
1872
|
};
|
|
@@ -2220,8 +2292,8 @@ export async function run(argv) {
|
|
|
2220
2292
|
}
|
|
2221
2293
|
}
|
|
2222
2294
|
|
|
2223
|
-
const memberships =
|
|
2224
|
-
|
|
2295
|
+
const memberships = resolveThreadCollectionMemberships(
|
|
2296
|
+
rootBundle,
|
|
2225
2297
|
collectionSlugToId,
|
|
2226
2298
|
);
|
|
2227
2299
|
const postData = buildPostPayloadFromBundle(rootBundle, {
|
|
@@ -2357,14 +2429,10 @@ export async function run(argv) {
|
|
|
2357
2429
|
if (textAttachment) replyAttachments.push(textAttachment);
|
|
2358
2430
|
}
|
|
2359
2431
|
|
|
2360
|
-
const replyMemberships = resolveCollectionMemberships(
|
|
2361
|
-
replyFm,
|
|
2362
|
-
collectionSlugToId,
|
|
2363
|
-
);
|
|
2364
2432
|
const replyData = buildPostPayloadFromBundle(replyBundle, {
|
|
2365
2433
|
bodyMarkdown: replyBody,
|
|
2366
2434
|
attachments: replyAttachments,
|
|
2367
|
-
memberships:
|
|
2435
|
+
memberships: { entries: [], ids: [] },
|
|
2368
2436
|
replyToId: threadTailId,
|
|
2369
2437
|
quietReply: shouldImportReplyQuietly(rootFm, replyFm),
|
|
2370
2438
|
});
|
package/bin/commands/migrate.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
applyNodeBackfills,
|
|
10
10
|
applyPgBackfills,
|
|
11
11
|
} from "../lib/migration-runner.js";
|
|
12
|
+
import { queryD1 } from "../lib/d1-query.js";
|
|
12
13
|
import { loadNodeRuntime } from "../lib/load-node-runtime.js";
|
|
13
14
|
import { loadNodeEnvFile } from "../lib/node-env.js";
|
|
14
15
|
import { openNodeSqlite, resolveDatabaseDialect } from "../lib/node-sqlite.js";
|
|
@@ -16,6 +17,10 @@ import {
|
|
|
16
17
|
bootstrapCliRuntime,
|
|
17
18
|
getCliRuntimeLabel,
|
|
18
19
|
} from "../lib/runtime-target.js";
|
|
20
|
+
import {
|
|
21
|
+
preflightThreadCollectionMigration,
|
|
22
|
+
verifyThreadCollectionMigration,
|
|
23
|
+
} from "../lib/thread-collection-migration.js";
|
|
19
24
|
|
|
20
25
|
export { loadNodeEnvFile };
|
|
21
26
|
|
|
@@ -167,6 +172,27 @@ async function logCliPgMigrationDebug(databaseUrl, phase) {
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
async function withNodeMigrationQuery(databaseDialect, databaseUrl, callback) {
|
|
176
|
+
if (databaseDialect === "pg") {
|
|
177
|
+
const pool = new Pool({ connectionString: databaseUrl });
|
|
178
|
+
try {
|
|
179
|
+
return await callback(async (sql) => (await pool.query(sql)).rows);
|
|
180
|
+
} finally {
|
|
181
|
+
await pool.end().catch(() => undefined);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { sqlite } = openNodeSqlite(process.env, {
|
|
186
|
+
createParentDir: true,
|
|
187
|
+
requireInitialized: false,
|
|
188
|
+
});
|
|
189
|
+
try {
|
|
190
|
+
return await callback((sql) => sqlite.prepare(sql).all());
|
|
191
|
+
} finally {
|
|
192
|
+
sqlite.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
170
196
|
export async function run(argv) {
|
|
171
197
|
const { values } = parseArgs({
|
|
172
198
|
args: argv,
|
|
@@ -256,6 +282,16 @@ export async function run(argv) {
|
|
|
256
282
|
}
|
|
257
283
|
|
|
258
284
|
if (runtime === "node") {
|
|
285
|
+
const threadCollectionPreflight = await withNodeMigrationQuery(
|
|
286
|
+
databaseDialect,
|
|
287
|
+
databaseUrl,
|
|
288
|
+
(query) =>
|
|
289
|
+
preflightThreadCollectionMigration({
|
|
290
|
+
dialect: databaseDialect === "pg" ? "pg" : "sqlite",
|
|
291
|
+
query,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
259
295
|
if (debugMigrate && databaseDialect === "pg") {
|
|
260
296
|
await logCliPgMigrationDebug(databaseUrl, "before");
|
|
261
297
|
}
|
|
@@ -270,6 +306,16 @@ export async function run(argv) {
|
|
|
270
306
|
throw error;
|
|
271
307
|
}
|
|
272
308
|
|
|
309
|
+
await withNodeMigrationQuery(databaseDialect, databaseUrl, (query) =>
|
|
310
|
+
verifyThreadCollectionMigration(
|
|
311
|
+
{
|
|
312
|
+
dialect: databaseDialect === "pg" ? "pg" : "sqlite",
|
|
313
|
+
query,
|
|
314
|
+
},
|
|
315
|
+
threadCollectionPreflight,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
|
|
273
319
|
if (debugMigrate && databaseDialect === "pg") {
|
|
274
320
|
await logCliPgMigrationDebug(databaseUrl, "after");
|
|
275
321
|
}
|
|
@@ -299,7 +345,16 @@ export async function run(argv) {
|
|
|
299
345
|
env: values.env,
|
|
300
346
|
persistTo: values["persist-to"],
|
|
301
347
|
};
|
|
348
|
+
const query = (sql) => queryD1(sql, runtime, options);
|
|
349
|
+
const threadCollectionPreflight = await preflightThreadCollectionMigration({
|
|
350
|
+
dialect: "sqlite",
|
|
351
|
+
query,
|
|
352
|
+
});
|
|
302
353
|
applyD1SchemaMigrations(runtime, options);
|
|
354
|
+
await verifyThreadCollectionMigration(
|
|
355
|
+
{ dialect: "sqlite", query },
|
|
356
|
+
threadCollectionPreflight,
|
|
357
|
+
);
|
|
303
358
|
applyD1Backfills(runtime, options);
|
|
304
359
|
}
|
|
305
360
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { resolveSiteUrl } from "../../lib/site-url.js";
|
|
3
|
+
|
|
4
|
+
const INTERNAL_ADMIN_TOKEN_ENV_VAR = "INTERNAL_ADMIN_TOKEN";
|
|
5
|
+
const DEFAULT_BATCH_SIZE = 50;
|
|
6
|
+
const MAX_BATCH_SIZE = 100;
|
|
7
|
+
|
|
8
|
+
function normalizeBaseUrl(value) {
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildRebuildUrl(siteUrl, siteId) {
|
|
14
|
+
const path = siteId
|
|
15
|
+
? `api/internal/sites/${encodeURIComponent(siteId)}/posts/body-html/rebuild`
|
|
16
|
+
: "api/internal/posts/body-html/rebuild";
|
|
17
|
+
return new URL(path, normalizeBaseUrl(siteUrl)).toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseBatchSize(rawLimit) {
|
|
21
|
+
if (rawLimit === undefined) return DEFAULT_BATCH_SIZE;
|
|
22
|
+
|
|
23
|
+
const limit = Number.parseInt(rawLimit, 10);
|
|
24
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_BATCH_SIZE) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Batch size must be an integer between 1 and ${MAX_BATCH_SIZE}.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return limit;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function requestBatch(url, token, { limit, cursor, dryRun }) {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${token}`,
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
limit,
|
|
41
|
+
...(cursor ? { cursor } : {}),
|
|
42
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const responseText = await response.text();
|
|
48
|
+
throw new Error(
|
|
49
|
+
`HTTP ${response.status}: ${responseText || response.statusText}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return response.json();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function logBatchResult(batchIndex, result) {
|
|
57
|
+
console.log(
|
|
58
|
+
` batch ${batchIndex}: processed=${result.processed} wouldRebuild=${result.wouldRebuild} rebuilt=${result.rebuilt} wouldUpgradeFootnotes=${result.wouldUpgradeFootnotes ?? 0} upgradedFootnotes=${result.upgradedFootnotes ?? 0} skipped=${result.skipped} conflicted=${result.conflicted} failed=${result.failed}`,
|
|
59
|
+
);
|
|
60
|
+
for (const failure of result.failures ?? []) {
|
|
61
|
+
console.error(` ${failure.postId}: ${failure.error}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function run(argv) {
|
|
66
|
+
const { values } = parseArgs({
|
|
67
|
+
args: argv,
|
|
68
|
+
options: {
|
|
69
|
+
config: { type: "string" },
|
|
70
|
+
"dry-run": { type: "boolean", default: false },
|
|
71
|
+
env: { type: "string" },
|
|
72
|
+
help: { type: "boolean", short: "h" },
|
|
73
|
+
limit: { type: "string" },
|
|
74
|
+
once: { type: "boolean", default: false },
|
|
75
|
+
site: { type: "string" },
|
|
76
|
+
token: { type: "string" },
|
|
77
|
+
url: { type: "string" },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (values.help) {
|
|
82
|
+
console.log("Usage: jant posts rebuild-html [--url <url>] [options]");
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log(
|
|
85
|
+
"Upgrade recognized legacy footnotes and rebuild stored post body HTML.",
|
|
86
|
+
);
|
|
87
|
+
console.log(
|
|
88
|
+
"The operation is site-scoped, cursor-paginated, and idempotent.",
|
|
89
|
+
);
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log("Options:");
|
|
92
|
+
console.log(" --url Target site or internal core URL");
|
|
93
|
+
console.log(
|
|
94
|
+
" --site Explicit managed site TypeID (host-based mode)",
|
|
95
|
+
);
|
|
96
|
+
console.log(
|
|
97
|
+
` --limit Batch size (default: ${DEFAULT_BATCH_SIZE}, max: ${MAX_BATCH_SIZE})`,
|
|
98
|
+
);
|
|
99
|
+
console.log(" --dry-run Report changes without writing them");
|
|
100
|
+
console.log(" --once Run one batch and exit");
|
|
101
|
+
console.log(" --token Internal admin token");
|
|
102
|
+
console.log(" --config Wrangler config file");
|
|
103
|
+
console.log(" --env Wrangler environment name");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const siteUrl = resolveSiteUrl({
|
|
108
|
+
url: values.url,
|
|
109
|
+
config: values.config,
|
|
110
|
+
env: values.env,
|
|
111
|
+
});
|
|
112
|
+
if (!siteUrl) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"posts rebuild-html requires --url or SITE_ORIGIN in the environment or wrangler.toml.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const token =
|
|
119
|
+
values.token?.trim() || process.env[INTERNAL_ADMIN_TOKEN_ENV_VAR]?.trim();
|
|
120
|
+
if (!token) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`posts rebuild-html requires --token or ${INTERNAL_ADMIN_TOKEN_ENV_VAR}.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const siteId = values.site?.trim();
|
|
127
|
+
const limit = parseBatchSize(values.limit);
|
|
128
|
+
const dryRun = values["dry-run"];
|
|
129
|
+
const rebuildUrl = buildRebuildUrl(siteUrl, siteId);
|
|
130
|
+
const scope = siteId ? `managed site ${siteId}` : siteUrl;
|
|
131
|
+
|
|
132
|
+
console.log(`${dryRun ? "Checking" : "Rebuilding"} post HTML for ${scope}`);
|
|
133
|
+
console.log(
|
|
134
|
+
` batch size: ${limit}${values.once ? " (single batch)" : ""}${dryRun ? " (dry run)" : ""}`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const totals = {
|
|
138
|
+
processed: 0,
|
|
139
|
+
wouldRebuild: 0,
|
|
140
|
+
rebuilt: 0,
|
|
141
|
+
wouldUpgradeFootnotes: 0,
|
|
142
|
+
upgradedFootnotes: 0,
|
|
143
|
+
skipped: 0,
|
|
144
|
+
conflicted: 0,
|
|
145
|
+
failed: 0,
|
|
146
|
+
};
|
|
147
|
+
let cursor;
|
|
148
|
+
let batchIndex = 0;
|
|
149
|
+
let targetVersion;
|
|
150
|
+
|
|
151
|
+
for (;;) {
|
|
152
|
+
batchIndex += 1;
|
|
153
|
+
const result = await requestBatch(rebuildUrl, token, {
|
|
154
|
+
limit,
|
|
155
|
+
cursor,
|
|
156
|
+
dryRun,
|
|
157
|
+
});
|
|
158
|
+
logBatchResult(batchIndex, result);
|
|
159
|
+
|
|
160
|
+
for (const key of Object.keys(totals)) {
|
|
161
|
+
totals[key] += result[key] ?? 0;
|
|
162
|
+
}
|
|
163
|
+
targetVersion = result.targetVersion;
|
|
164
|
+
|
|
165
|
+
if (values.once || result.done) break;
|
|
166
|
+
if (!result.nextCursor || result.nextCursor === cursor) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Server reported more work without advancing the cursor; aborting.",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
cursor = result.nextCursor;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log("");
|
|
175
|
+
console.log(
|
|
176
|
+
`Done. targetVersion=${targetVersion ?? "unknown"} processed=${totals.processed} wouldRebuild=${totals.wouldRebuild} rebuilt=${totals.rebuilt} wouldUpgradeFootnotes=${totals.wouldUpgradeFootnotes} upgradedFootnotes=${totals.upgradedFootnotes} skipped=${totals.skipped} conflicted=${totals.conflicted} failed=${totals.failed} batches=${batchIndex}`,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (totals.failed > 0 || totals.conflicted > 0) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
"Some rows were not rebuilt. Fix invalid bodies or rerun after concurrent edits settle.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
remapSnapshotObjectKey,
|
|
29
29
|
rewriteLegacySnapshotSql,
|
|
30
30
|
rewriteSnapshotSiteIdentifiers,
|
|
31
|
+
upgradeSnapshotSql,
|
|
31
32
|
validateSnapshotTargetSite,
|
|
32
33
|
} from "../../../lib/site-snapshot.js";
|
|
33
34
|
import {
|
|
@@ -69,7 +70,7 @@ async function createNodeImportContext() {
|
|
|
69
70
|
return nodeDatabase.query(sql);
|
|
70
71
|
},
|
|
71
72
|
async execute(sql) {
|
|
72
|
-
await nodeDatabase.
|
|
73
|
+
await nodeDatabase.executeAtomically(sql);
|
|
73
74
|
},
|
|
74
75
|
async uploadObject(key, filePath, contentType) {
|
|
75
76
|
if (!storage) {
|
|
@@ -334,6 +335,18 @@ export async function run(argv) {
|
|
|
334
335
|
join(materialized.rootDir, "db.sql"),
|
|
335
336
|
"utf-8",
|
|
336
337
|
);
|
|
338
|
+
const siteScopedDbSql = snapshotSite
|
|
339
|
+
? shouldRemapSite
|
|
340
|
+
? rewriteSnapshotSiteIdentifiers(
|
|
341
|
+
rawDbSql,
|
|
342
|
+
snapshotSite.id,
|
|
343
|
+
targetSite.id,
|
|
344
|
+
)
|
|
345
|
+
: rawDbSql
|
|
346
|
+
: rewriteLegacySnapshotSql(rawDbSql, targetSite.id);
|
|
347
|
+
// Upgrade compatibility SQL before touching target rows or storage.
|
|
348
|
+
// A malformed v1 post_collection reference therefore fails safely.
|
|
349
|
+
const dbSql = upgradeSnapshotSql(siteScopedDbSql, meta.version);
|
|
337
350
|
|
|
338
351
|
// Preflight: every storage_key/poster_key referenced by db.sql must have
|
|
339
352
|
// a corresponding file in objects/, or we'll end up with broken media
|
|
@@ -389,15 +402,6 @@ export async function run(argv) {
|
|
|
389
402
|
);
|
|
390
403
|
}
|
|
391
404
|
|
|
392
|
-
const dbSql = snapshotSite
|
|
393
|
-
? shouldRemapSite
|
|
394
|
-
? rewriteSnapshotSiteIdentifiers(
|
|
395
|
-
rawDbSql,
|
|
396
|
-
snapshotSite.id,
|
|
397
|
-
targetSite.id,
|
|
398
|
-
)
|
|
399
|
-
: rawDbSql
|
|
400
|
-
: rewriteLegacySnapshotSql(rawDbSql, targetSite.id);
|
|
401
405
|
await context.execute(`${buildReplaceSql(targetSite.id)}\n${dbSql}`);
|
|
402
406
|
|
|
403
407
|
const keysToDelete = [...currentKeys].filter(
|
package/bin/lib/d1-query.js
CHANGED
|
@@ -87,6 +87,23 @@ function extractJson(raw) {
|
|
|
87
87
|
return raw.slice(start);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function getWranglerExecutionOptions(options) {
|
|
91
|
+
const executionOptions = { ...options };
|
|
92
|
+
for (const key of [
|
|
93
|
+
"configPath",
|
|
94
|
+
"database",
|
|
95
|
+
"env",
|
|
96
|
+
"persistTo",
|
|
97
|
+
"quiet",
|
|
98
|
+
"retryAttempts",
|
|
99
|
+
"retryDelayMs",
|
|
100
|
+
"trackedExecution",
|
|
101
|
+
]) {
|
|
102
|
+
delete executionOptions[key];
|
|
103
|
+
}
|
|
104
|
+
return executionOptions;
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
function runWrangler(args, options = {}) {
|
|
91
108
|
const retryAttempts = Math.max(
|
|
92
109
|
1,
|
|
@@ -99,7 +116,7 @@ function runWrangler(args, options = {}) {
|
|
|
99
116
|
|
|
100
117
|
for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
|
|
101
118
|
try {
|
|
102
|
-
return runLocalWrangler(args, options);
|
|
119
|
+
return runLocalWrangler(args, getWranglerExecutionOptions(options));
|
|
103
120
|
} catch (error) {
|
|
104
121
|
const output = `${error.stdout ?? ""}${error.stderr ?? ""}`.trim();
|
|
105
122
|
const wranglerError = parseWranglerError(output);
|
|
@@ -135,7 +152,7 @@ export function executeD1(sql, runtime, options = {}) {
|
|
|
135
152
|
);
|
|
136
153
|
|
|
137
154
|
if (options.quiet) {
|
|
138
|
-
const output = runWrangler([...args, "--json"]);
|
|
155
|
+
const output = runWrangler([...args, "--json"], options);
|
|
139
156
|
const parsed = JSON.parse(extractJson(output));
|
|
140
157
|
const statements = Array.isArray(parsed) ? parsed : [parsed];
|
|
141
158
|
|
|
@@ -148,7 +165,7 @@ export function executeD1(sql, runtime, options = {}) {
|
|
|
148
165
|
return statements;
|
|
149
166
|
}
|
|
150
167
|
|
|
151
|
-
runWrangler(args, { stdio: "inherit" });
|
|
168
|
+
runWrangler(args, { ...options, stdio: "inherit" });
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
export function queryD1(sql, runtime, options = {}) {
|
|
@@ -164,6 +181,7 @@ export function queryD1(sql, runtime, options = {}) {
|
|
|
164
181
|
],
|
|
165
182
|
options,
|
|
166
183
|
),
|
|
184
|
+
options,
|
|
167
185
|
);
|
|
168
186
|
const parsed = JSON.parse(extractJson(output));
|
|
169
187
|
const statement = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
@@ -189,7 +207,7 @@ export function executeD1File(filePath, runtime, options = {}) {
|
|
|
189
207
|
);
|
|
190
208
|
|
|
191
209
|
if (options.quiet) {
|
|
192
|
-
const output = runWrangler([...args, "--json"]);
|
|
210
|
+
const output = runWrangler([...args, "--json"], options);
|
|
193
211
|
const parsed = JSON.parse(extractJson(output));
|
|
194
212
|
const statements = Array.isArray(parsed) ? parsed : [parsed];
|
|
195
213
|
|
|
@@ -202,5 +220,5 @@ export function executeD1File(filePath, runtime, options = {}) {
|
|
|
202
220
|
return statements;
|
|
203
221
|
}
|
|
204
222
|
|
|
205
|
-
runWrangler(args, { stdio: "inherit" });
|
|
223
|
+
runWrangler(args, { ...options, stdio: "inherit" });
|
|
206
224
|
}
|
|
@@ -183,11 +183,21 @@ export function createD1SqlRunner(runtime, options) {
|
|
|
183
183
|
executeD1(sql, runtime, { ...options, quiet: true });
|
|
184
184
|
},
|
|
185
185
|
executeTrackedFile(filePath, trackingSql) {
|
|
186
|
+
// A tracked schema/data migration is already atomic with its tracking
|
|
187
|
+
// insert. Do not let the lower-level Wrangler helper blindly replay a
|
|
188
|
+
// destructive batch after an ambiguous network response. The outer
|
|
189
|
+
// runner reconciles the tracking row instead.
|
|
190
|
+
const trackedOptions = {
|
|
191
|
+
...options,
|
|
192
|
+
quiet: true,
|
|
193
|
+
retryAttempts: 1,
|
|
194
|
+
};
|
|
195
|
+
|
|
186
196
|
if (trackedExecution === "segmented") {
|
|
187
197
|
for (const statement of readSqlStatements(filePath)) {
|
|
188
|
-
executeD1(statement, runtime,
|
|
198
|
+
executeD1(statement, runtime, trackedOptions);
|
|
189
199
|
}
|
|
190
|
-
executeD1(trackingSql, runtime,
|
|
200
|
+
executeD1(trackingSql, runtime, trackedOptions);
|
|
191
201
|
return;
|
|
192
202
|
}
|
|
193
203
|
|
|
@@ -200,7 +210,7 @@ export function createD1SqlRunner(runtime, options) {
|
|
|
200
210
|
tempPath,
|
|
201
211
|
`\n${readNormalizedSqlFile(filePath)}\n${trackingSql}\n`,
|
|
202
212
|
);
|
|
203
|
-
executeD1File(tempPath, runtime,
|
|
213
|
+
executeD1File(tempPath, runtime, trackedOptions);
|
|
204
214
|
} finally {
|
|
205
215
|
rmSync(tempDir, { recursive: true, force: true });
|
|
206
216
|
}
|
|
@@ -210,10 +220,7 @@ export function createD1SqlRunner(runtime, options) {
|
|
|
210
220
|
executeD1(
|
|
211
221
|
`\n${readNormalizedSqlFile(filePath)}\n${trackingSql}`,
|
|
212
222
|
runtime,
|
|
213
|
-
|
|
214
|
-
...options,
|
|
215
|
-
quiet: true,
|
|
216
|
-
},
|
|
223
|
+
trackedOptions,
|
|
217
224
|
);
|
|
218
225
|
},
|
|
219
226
|
query(sql) {
|
|
@@ -261,6 +268,21 @@ export function applyTrackedSqlFiles(runner, options) {
|
|
|
261
268
|
}
|
|
262
269
|
console.log(`[${index + 1}/${pendingFiles.length}] ${file.name} ✅`);
|
|
263
270
|
} catch (error) {
|
|
271
|
+
// The database may have committed the atomic batch even if the client
|
|
272
|
+
// lost its response. Re-read the tracking table before reporting a
|
|
273
|
+
// failure; a committed marker means replay would be both unnecessary and
|
|
274
|
+
// unsafe for CREATE/COPY/DROP migrations.
|
|
275
|
+
try {
|
|
276
|
+
if (new Set(listAppliedNames(runner, tableName)).has(file.name)) {
|
|
277
|
+
console.log(
|
|
278
|
+
`[${index + 1}/${pendingFiles.length}] ${file.name} ✅ (confirmed after interrupted response)`,
|
|
279
|
+
);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// Preserve the original execution failure when reconciliation itself
|
|
284
|
+
// cannot reach the database.
|
|
285
|
+
}
|
|
264
286
|
console.log(`[${index + 1}/${pendingFiles.length}] ${file.name} ❌`);
|
|
265
287
|
throw new Error(`Failed to apply ${file.name}: ${error.message}`, {
|
|
266
288
|
cause: error,
|
package/bin/lib/node-database.js
CHANGED
|
@@ -5,6 +5,15 @@ function hasExecute(db) {
|
|
|
5
5
|
return typeof db === "object" && db !== null && "execute" in db;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function hasTransaction(db) {
|
|
9
|
+
return (
|
|
10
|
+
typeof db === "object" &&
|
|
11
|
+
db !== null &&
|
|
12
|
+
"transaction" in db &&
|
|
13
|
+
typeof db.transaction === "function"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export async function openNodeDatabase(env = process.env) {
|
|
9
18
|
const { createNodeBindings } = await loadNodeRuntime();
|
|
10
19
|
const { bindings, close } = await createNodeBindings({
|
|
@@ -44,5 +53,28 @@ export async function openNodeDatabase(env = process.env) {
|
|
|
44
53
|
|
|
45
54
|
await database.db.run(sql);
|
|
46
55
|
},
|
|
56
|
+
async executeAtomically(sql) {
|
|
57
|
+
if (bindings.NODE_SQLITE) {
|
|
58
|
+
bindings.NODE_SQLITE.transaction(() => {
|
|
59
|
+
bindings.NODE_SQLITE.exec(sql);
|
|
60
|
+
})();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!hasTransaction(database.db)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Atomic SQL execution is unavailable for the ${database.dialect} Node database driver.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await database.db.transaction(async (transaction) => {
|
|
71
|
+
if (!hasExecute(transaction)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Atomic SQL execution is unavailable for the ${database.dialect} transaction driver.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
await transaction.execute(sql);
|
|
77
|
+
});
|
|
78
|
+
},
|
|
47
79
|
};
|
|
48
80
|
}
|