@jant/core 0.6.14 → 0.6.16
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 +88 -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-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- 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__/mise-config.test.ts +22 -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 +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -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 +82 -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__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- 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 +177 -95
- 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 +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -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/post-refresh.ts +135 -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__/link-input-rules.test.ts +121 -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/link-input-rules.ts +4 -4
- 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 +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -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-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -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/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -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/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -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 +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- 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__/feed.test.ts +168 -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 +24 -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__/slug.test.ts +18 -0
- 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__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -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/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -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 +47 -8
- 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 +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -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__/palette.test.ts +44 -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 +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -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 +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -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__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -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 +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -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 +224 -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 +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -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 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- 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 +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- 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/ArchivePage.tsx +131 -24
- 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 +70 -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 +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -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
|
@@ -176,6 +176,41 @@
|
|
|
176
176
|
"when": 1781807044981,
|
|
177
177
|
"tag": "0024_high_violations",
|
|
178
178
|
"breakpoints": true
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"idx": 25,
|
|
182
|
+
"version": "7",
|
|
183
|
+
"when": 1784131458842,
|
|
184
|
+
"tag": "0025_moaning_epoch",
|
|
185
|
+
"breakpoints": true
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"idx": 26,
|
|
189
|
+
"version": "7",
|
|
190
|
+
"when": 1784343834845,
|
|
191
|
+
"tag": "0026_talented_killer_shrike",
|
|
192
|
+
"breakpoints": true
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"idx": 27,
|
|
196
|
+
"version": "7",
|
|
197
|
+
"when": 1784436805462,
|
|
198
|
+
"tag": "0027_short_cannonball",
|
|
199
|
+
"breakpoints": true
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"idx": 28,
|
|
203
|
+
"version": "7",
|
|
204
|
+
"when": 1784531392184,
|
|
205
|
+
"tag": "0028_perfect_sharon_carter",
|
|
206
|
+
"breakpoints": true
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"idx": 29,
|
|
210
|
+
"version": "7",
|
|
211
|
+
"when": 1786008671529,
|
|
212
|
+
"tag": "0029_rare_hairball",
|
|
213
|
+
"breakpoints": true
|
|
179
214
|
}
|
|
180
215
|
]
|
|
181
216
|
}
|
package/src/db/pg/schema.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
customType,
|
|
20
20
|
} from "drizzle-orm/pg-core";
|
|
21
21
|
import { sql } from "drizzle-orm";
|
|
22
|
+
import { SYSTEM_NAV_KEY_VALUES } from "../../types/constants.js";
|
|
22
23
|
|
|
23
24
|
const SITE_STATUSES = ["active", "suspended"] as const;
|
|
24
25
|
const SITE_DOMAIN_KINDS = ["primary", "alias"] as const;
|
|
@@ -27,16 +28,8 @@ const FORMATS = ["note", "link", "quote"] as const;
|
|
|
27
28
|
const STATUSES = ["draft", "published"] as const;
|
|
28
29
|
const VISIBILITIES = ["public", "latest_hidden", "private"] as const;
|
|
29
30
|
const COLLECTION_SORT_ORDERS = ["newest", "oldest", "rating_desc"] as const;
|
|
30
|
-
const NAV_ITEM_TYPES = ["link", "system", "collection"] as const;
|
|
31
|
+
const NAV_ITEM_TYPES = ["link", "system", "collection", "page"] as const;
|
|
31
32
|
const NAV_ITEM_PLACEMENTS = ["header", "more"] as const;
|
|
32
|
-
const SYSTEM_NAV_KEYS = [
|
|
33
|
-
"latest",
|
|
34
|
-
"featured",
|
|
35
|
-
"collections",
|
|
36
|
-
"archive",
|
|
37
|
-
"rss",
|
|
38
|
-
"settings",
|
|
39
|
-
] as const;
|
|
40
33
|
const UPLOAD_SESSION_STATES = [
|
|
41
34
|
"pending",
|
|
42
35
|
"uploaded",
|
|
@@ -167,6 +160,7 @@ export const posts = pgTable(
|
|
|
167
160
|
url: text("url"),
|
|
168
161
|
body: text("body"),
|
|
169
162
|
bodyHtml: text("body_html"),
|
|
163
|
+
bodyHtmlVersion: integer("body_html_version").notNull().default(1),
|
|
170
164
|
bodyText: text("body_text"),
|
|
171
165
|
quoteText: text("quote_text"),
|
|
172
166
|
summary: text("summary"),
|
|
@@ -185,8 +179,23 @@ export const posts = pgTable(
|
|
|
185
179
|
previewProvider: text("preview_provider"),
|
|
186
180
|
replyToId: text("reply_to_id"),
|
|
187
181
|
threadId: text("thread_id").notNull(),
|
|
182
|
+
/**
|
|
183
|
+
* The author added this reply without announcing it. Persisted so both
|
|
184
|
+
* activity timestamps below stay recomputable from the rows alone.
|
|
185
|
+
* Always false on Thread roots.
|
|
186
|
+
*/
|
|
187
|
+
quietReply: boolean("quiet_reply").notNull().default(false),
|
|
188
188
|
publishedAt: integer("published_at"),
|
|
189
|
+
/**
|
|
190
|
+
* Root only: newest published_at in the Thread, excluding quiet replies —
|
|
191
|
+
* "last announced". Drives Latest and the feeds.
|
|
192
|
+
*/
|
|
189
193
|
lastActivityAt: integer("last_activity_at"),
|
|
194
|
+
/**
|
|
195
|
+
* Root only: newest published_at in the Thread, quiet replies included —
|
|
196
|
+
* "last changed". Drives the archive's updated sort.
|
|
197
|
+
*/
|
|
198
|
+
threadUpdatedAt: integer("thread_updated_at"),
|
|
190
199
|
createdAt: integer("created_at").notNull(),
|
|
191
200
|
updatedAt: integer("updated_at").notNull(),
|
|
192
201
|
},
|
|
@@ -234,6 +243,9 @@ export const posts = pgTable(
|
|
|
234
243
|
index("idx_post_site_root_published_activity")
|
|
235
244
|
.on(table.siteId, table.lastActivityAt, table.id)
|
|
236
245
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
246
|
+
index("idx_post_site_root_thread_updated")
|
|
247
|
+
.on(table.siteId, table.threadUpdatedAt, table.id)
|
|
248
|
+
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
237
249
|
index("idx_post_site_root_draft_updated")
|
|
238
250
|
.on(table.siteId, table.updatedAt, table.id)
|
|
239
251
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'draft'`),
|
|
@@ -242,8 +254,8 @@ export const posts = pgTable(
|
|
|
242
254
|
.where(
|
|
243
255
|
sql`${table.replyToId} IS NOT NULL AND ${table.status} = 'published'`,
|
|
244
256
|
),
|
|
245
|
-
index("
|
|
246
|
-
.on(table.siteId, table.
|
|
257
|
+
index("idx_post_site_featured_thread_published")
|
|
258
|
+
.on(table.siteId, table.threadId, table.publishedAt, table.id)
|
|
247
259
|
.where(
|
|
248
260
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
249
261
|
),
|
|
@@ -591,16 +603,16 @@ export const collectionDirectoryItems = pgTable(
|
|
|
591
603
|
);
|
|
592
604
|
|
|
593
605
|
// =============================================================================
|
|
594
|
-
//
|
|
606
|
+
// Thread-Collection Junction Table (M:N)
|
|
595
607
|
// =============================================================================
|
|
596
608
|
|
|
597
|
-
export const
|
|
598
|
-
"
|
|
609
|
+
export const threadCollections = pgTable(
|
|
610
|
+
"thread_collection",
|
|
599
611
|
{
|
|
600
612
|
siteId: text("site_id")
|
|
601
613
|
.notNull()
|
|
602
614
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
603
|
-
|
|
615
|
+
threadId: text("thread_id")
|
|
604
616
|
.notNull()
|
|
605
617
|
.references(() => posts.id, { onDelete: "cascade" }),
|
|
606
618
|
collectionId: text("collection_id")
|
|
@@ -611,16 +623,16 @@ export const postCollections = pgTable(
|
|
|
611
623
|
pinnedAt: integer("pinned_at"),
|
|
612
624
|
},
|
|
613
625
|
(table) => [
|
|
614
|
-
primaryKey({ columns: [table.siteId, table.
|
|
615
|
-
index("
|
|
626
|
+
primaryKey({ columns: [table.siteId, table.threadId, table.collectionId] }),
|
|
627
|
+
index("idx_thread_collection_site_collection_id").on(
|
|
616
628
|
table.siteId,
|
|
617
629
|
table.collectionId,
|
|
618
630
|
),
|
|
619
|
-
index("
|
|
631
|
+
index("idx_thread_collection_site_collection_created_thread").on(
|
|
620
632
|
table.siteId,
|
|
621
633
|
table.collectionId,
|
|
622
634
|
table.createdAt,
|
|
623
|
-
table.
|
|
635
|
+
table.threadId,
|
|
624
636
|
),
|
|
625
637
|
],
|
|
626
638
|
);
|
|
@@ -642,11 +654,14 @@ export const navItems = pgTable(
|
|
|
642
654
|
.notNull()
|
|
643
655
|
.default("link"),
|
|
644
656
|
systemKey: text("system_key", {
|
|
645
|
-
enum:
|
|
657
|
+
enum: SYSTEM_NAV_KEY_VALUES,
|
|
646
658
|
}),
|
|
647
659
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
648
660
|
onDelete: "cascade",
|
|
649
661
|
}),
|
|
662
|
+
postId: text("post_id").references(() => posts.id, {
|
|
663
|
+
onDelete: "cascade",
|
|
664
|
+
}),
|
|
650
665
|
label: text("label").notNull(),
|
|
651
666
|
url: text("url").notNull(),
|
|
652
667
|
placement: text("placement", {
|
|
@@ -673,14 +688,22 @@ export const navItems = pgTable(
|
|
|
673
688
|
${table.type} = 'link'
|
|
674
689
|
AND ${table.systemKey} IS NULL
|
|
675
690
|
AND ${table.collectionId} IS NULL
|
|
691
|
+
AND ${table.postId} IS NULL
|
|
676
692
|
) OR (
|
|
677
693
|
${table.type} = 'system'
|
|
678
694
|
AND ${table.systemKey} IS NOT NULL
|
|
679
695
|
AND ${table.collectionId} IS NULL
|
|
696
|
+
AND ${table.postId} IS NULL
|
|
680
697
|
) OR (
|
|
681
698
|
${table.type} = 'collection'
|
|
682
699
|
AND ${table.systemKey} IS NULL
|
|
683
700
|
AND ${table.collectionId} IS NOT NULL
|
|
701
|
+
AND ${table.postId} IS NULL
|
|
702
|
+
) OR (
|
|
703
|
+
${table.type} = 'page'
|
|
704
|
+
AND ${table.systemKey} IS NULL
|
|
705
|
+
AND ${table.collectionId} IS NULL
|
|
706
|
+
AND ${table.postId} IS NOT NULL
|
|
684
707
|
)`,
|
|
685
708
|
),
|
|
686
709
|
uniqueIndex("uq_nav_item_site_position").on(table.siteId, table.position),
|
|
@@ -690,6 +713,9 @@ export const navItems = pgTable(
|
|
|
690
713
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
691
714
|
.on(table.siteId, table.collectionId)
|
|
692
715
|
.where(sql`${table.collectionId} IS NOT NULL`),
|
|
716
|
+
uniqueIndex("uq_nav_item_site_post_id")
|
|
717
|
+
.on(table.siteId, table.postId)
|
|
718
|
+
.where(sql`${table.postId} IS NOT NULL`),
|
|
693
719
|
],
|
|
694
720
|
);
|
|
695
721
|
|
|
@@ -1,25 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "demo-current",
|
|
3
|
-
"baseMigrationTag": "
|
|
3
|
+
"baseMigrationTag": "0026_absent_rhodey",
|
|
4
4
|
"seedPath": "./demo-current.sql",
|
|
5
5
|
"assertions": [
|
|
6
6
|
{
|
|
7
7
|
"name": "published posts exist",
|
|
8
8
|
"sql": "SELECT COUNT(*) AS count FROM post",
|
|
9
9
|
"column": "count",
|
|
10
|
-
"
|
|
10
|
+
"equals": 3
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "collections exist",
|
|
14
14
|
"sql": "SELECT COUNT(*) AS count FROM collection",
|
|
15
15
|
"column": "count",
|
|
16
|
-
"
|
|
16
|
+
"equals": 2
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "fts index populated",
|
|
20
20
|
"sql": "SELECT COUNT(*) AS count FROM post_fts",
|
|
21
21
|
"column": "count",
|
|
22
|
-
"
|
|
22
|
+
"equals": 3
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "thread collection union preserves both collections",
|
|
26
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection",
|
|
27
|
+
"column": "count",
|
|
28
|
+
"equals": 2
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "shared collection metadata is aggregated across the thread",
|
|
32
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection WHERE site_id = 'sit_01kme7j7j1f4m7s2k8r5c9t0qb' AND thread_id = 'pst_01kme7ja2mf4m7s2k8r5c9t0qf' AND collection_id = 'col_01kme7j8f9f4m7s2k8r5c9t0qc' AND created_at = 1774200040 AND position = 1 AND pinned_at = 1774200500",
|
|
33
|
+
"column": "count",
|
|
34
|
+
"equals": 1
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "child-only collection becomes thread membership",
|
|
38
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection WHERE site_id = 'sit_01kme7j7j1f4m7s2k8r5c9t0qb' AND thread_id = 'pst_01kme7ja2mf4m7s2k8r5c9t0qf' AND collection_id = 'col_01kme7j8f9f4m7s2k8r5c9t0qd' AND created_at = 1774200060 AND position = 4 AND pinned_at = 1774200600",
|
|
39
|
+
"column": "count",
|
|
40
|
+
"equals": 1
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "thread collection targets are roots",
|
|
44
|
+
"sql": "SELECT COUNT(*) AS count FROM thread_collection tc INNER JOIN post root ON root.site_id = tc.site_id AND root.id = tc.thread_id WHERE root.reply_to_id IS NOT NULL OR root.thread_id <> root.id",
|
|
45
|
+
"column": "count",
|
|
46
|
+
"equals": 0
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "legacy post collection table is removed",
|
|
50
|
+
"sql": "SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name = 'post_collection'",
|
|
51
|
+
"column": "count",
|
|
52
|
+
"equals": 0
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "foreign keys remain valid",
|
|
56
|
+
"sql": "PRAGMA foreign_key_check",
|
|
57
|
+
"rowCount": 0
|
|
23
58
|
}
|
|
24
59
|
]
|
|
25
60
|
}
|
|
@@ -31,6 +31,24 @@ INSERT INTO "collection" (
|
|
|
31
31
|
1774200010
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
INSERT INTO "collection" (
|
|
35
|
+
"id",
|
|
36
|
+
"site_id",
|
|
37
|
+
"title",
|
|
38
|
+
"description",
|
|
39
|
+
"sort_order",
|
|
40
|
+
"created_at",
|
|
41
|
+
"updated_at"
|
|
42
|
+
) VALUES (
|
|
43
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qd',
|
|
44
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
45
|
+
'Child-only Notes',
|
|
46
|
+
'Collection assigned only to a reply before the Thread migration.',
|
|
47
|
+
'newest',
|
|
48
|
+
1774200011,
|
|
49
|
+
1774200011
|
|
50
|
+
);
|
|
51
|
+
|
|
34
52
|
INSERT INTO "nav_item" (
|
|
35
53
|
"id",
|
|
36
54
|
"site_id",
|
|
@@ -149,18 +167,115 @@ INSERT INTO "post" (
|
|
|
149
167
|
1774200020
|
|
150
168
|
);
|
|
151
169
|
|
|
152
|
-
INSERT INTO "
|
|
170
|
+
INSERT INTO "post" (
|
|
171
|
+
"id",
|
|
153
172
|
"site_id",
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
173
|
+
"format",
|
|
174
|
+
"status",
|
|
175
|
+
"visibility",
|
|
176
|
+
"title",
|
|
177
|
+
"body",
|
|
178
|
+
"body_html",
|
|
179
|
+
"body_text",
|
|
180
|
+
"reply_to_id",
|
|
181
|
+
"thread_id",
|
|
182
|
+
"published_at",
|
|
183
|
+
"last_activity_at",
|
|
184
|
+
"created_at",
|
|
185
|
+
"updated_at"
|
|
157
186
|
) VALUES (
|
|
187
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
158
188
|
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
189
|
+
'note',
|
|
190
|
+
'published',
|
|
191
|
+
NULL,
|
|
192
|
+
'First rehearsal reply',
|
|
193
|
+
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"First migration rehearsal reply."}]}]}',
|
|
194
|
+
'<p>First migration rehearsal reply.</p>',
|
|
195
|
+
'First migration rehearsal reply.',
|
|
159
196
|
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
160
|
-
'
|
|
161
|
-
|
|
197
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
198
|
+
1774200030,
|
|
199
|
+
1774200030,
|
|
200
|
+
1774200030,
|
|
201
|
+
1774200030
|
|
162
202
|
);
|
|
163
203
|
|
|
204
|
+
INSERT INTO "post" (
|
|
205
|
+
"id",
|
|
206
|
+
"site_id",
|
|
207
|
+
"format",
|
|
208
|
+
"status",
|
|
209
|
+
"visibility",
|
|
210
|
+
"title",
|
|
211
|
+
"body",
|
|
212
|
+
"body_html",
|
|
213
|
+
"body_text",
|
|
214
|
+
"reply_to_id",
|
|
215
|
+
"thread_id",
|
|
216
|
+
"published_at",
|
|
217
|
+
"last_activity_at",
|
|
218
|
+
"created_at",
|
|
219
|
+
"updated_at"
|
|
220
|
+
) VALUES (
|
|
221
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qi',
|
|
222
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
223
|
+
'note',
|
|
224
|
+
'published',
|
|
225
|
+
NULL,
|
|
226
|
+
'Second rehearsal reply',
|
|
227
|
+
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Second migration rehearsal reply."}]}]}',
|
|
228
|
+
'<p>Second migration rehearsal reply.</p>',
|
|
229
|
+
'Second migration rehearsal reply.',
|
|
230
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
231
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
232
|
+
1774200040,
|
|
233
|
+
1774200040,
|
|
234
|
+
1774200040,
|
|
235
|
+
1774200040
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
INSERT INTO "post_collection" (
|
|
239
|
+
"site_id",
|
|
240
|
+
"post_id",
|
|
241
|
+
"collection_id",
|
|
242
|
+
"created_at",
|
|
243
|
+
"position",
|
|
244
|
+
"pinned_at"
|
|
245
|
+
) VALUES
|
|
246
|
+
(
|
|
247
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
248
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qf',
|
|
249
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qc',
|
|
250
|
+
1774200021,
|
|
251
|
+
7,
|
|
252
|
+
1774200300
|
|
253
|
+
),
|
|
254
|
+
(
|
|
255
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
256
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
257
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qc',
|
|
258
|
+
1774200040,
|
|
259
|
+
5,
|
|
260
|
+
NULL
|
|
261
|
+
),
|
|
262
|
+
(
|
|
263
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
264
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qi',
|
|
265
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qc',
|
|
266
|
+
1774200030,
|
|
267
|
+
1,
|
|
268
|
+
1774200500
|
|
269
|
+
),
|
|
270
|
+
(
|
|
271
|
+
'sit_01kme7j7j1f4m7s2k8r5c9t0qb',
|
|
272
|
+
'pst_01kme7ja2mf4m7s2k8r5c9t0qh',
|
|
273
|
+
'col_01kme7j8f9f4m7s2k8r5c9t0qd',
|
|
274
|
+
1774200060,
|
|
275
|
+
4,
|
|
276
|
+
1774200600
|
|
277
|
+
);
|
|
278
|
+
|
|
164
279
|
INSERT INTO "path_registry" (
|
|
165
280
|
"id",
|
|
166
281
|
"site_id",
|
package/src/db/schema.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
check,
|
|
16
16
|
} from "drizzle-orm/sqlite-core";
|
|
17
17
|
import { sql } from "drizzle-orm";
|
|
18
|
+
import { SYSTEM_NAV_KEY_VALUES } from "../types/constants.js";
|
|
18
19
|
|
|
19
20
|
const SITE_STATUSES = ["active", "suspended"] as const;
|
|
20
21
|
const SITE_DOMAIN_KINDS = ["primary", "alias"] as const;
|
|
@@ -23,16 +24,8 @@ const FORMATS = ["note", "link", "quote"] as const;
|
|
|
23
24
|
const STATUSES = ["draft", "published"] as const;
|
|
24
25
|
const VISIBILITIES = ["public", "latest_hidden", "private"] as const;
|
|
25
26
|
const COLLECTION_SORT_ORDERS = ["newest", "oldest", "rating_desc"] as const;
|
|
26
|
-
const NAV_ITEM_TYPES = ["link", "system", "collection"] as const;
|
|
27
|
+
const NAV_ITEM_TYPES = ["link", "system", "collection", "page"] as const;
|
|
27
28
|
const NAV_ITEM_PLACEMENTS = ["header", "more"] as const;
|
|
28
|
-
const SYSTEM_NAV_KEYS = [
|
|
29
|
-
"latest",
|
|
30
|
-
"featured",
|
|
31
|
-
"collections",
|
|
32
|
-
"archive",
|
|
33
|
-
"rss",
|
|
34
|
-
"settings",
|
|
35
|
-
] as const;
|
|
36
29
|
const UPLOAD_SESSION_STATES = [
|
|
37
30
|
"pending",
|
|
38
31
|
"uploaded",
|
|
@@ -161,6 +154,7 @@ export const posts = sqliteTable(
|
|
|
161
154
|
url: text("url"),
|
|
162
155
|
body: text("body"),
|
|
163
156
|
bodyHtml: text("body_html"),
|
|
157
|
+
bodyHtmlVersion: integer("body_html_version").notNull().default(1),
|
|
164
158
|
bodyText: text("body_text"),
|
|
165
159
|
quoteText: text("quote_text"),
|
|
166
160
|
summary: text("summary"),
|
|
@@ -170,8 +164,25 @@ export const posts = sqliteTable(
|
|
|
170
164
|
previewProvider: text("preview_provider"),
|
|
171
165
|
replyToId: text("reply_to_id"),
|
|
172
166
|
threadId: text("thread_id").notNull(),
|
|
167
|
+
/**
|
|
168
|
+
* The author added this reply without announcing it. Persisted so both
|
|
169
|
+
* activity timestamps below stay recomputable from the rows alone.
|
|
170
|
+
* Always false on Thread roots.
|
|
171
|
+
*/
|
|
172
|
+
quietReply: integer("quiet_reply", { mode: "boolean" })
|
|
173
|
+
.notNull()
|
|
174
|
+
.default(false),
|
|
173
175
|
publishedAt: integer("published_at"),
|
|
176
|
+
/**
|
|
177
|
+
* Root only: newest published_at in the Thread, excluding quiet replies —
|
|
178
|
+
* "last announced". Drives Latest and the feeds.
|
|
179
|
+
*/
|
|
174
180
|
lastActivityAt: integer("last_activity_at"),
|
|
181
|
+
/**
|
|
182
|
+
* Root only: newest published_at in the Thread, quiet replies included —
|
|
183
|
+
* "last changed". Drives the archive's updated sort.
|
|
184
|
+
*/
|
|
185
|
+
threadUpdatedAt: integer("thread_updated_at"),
|
|
175
186
|
createdAt: integer("created_at").notNull(),
|
|
176
187
|
updatedAt: integer("updated_at").notNull(),
|
|
177
188
|
},
|
|
@@ -219,6 +230,9 @@ export const posts = sqliteTable(
|
|
|
219
230
|
index("idx_post_site_root_published_activity")
|
|
220
231
|
.on(table.siteId, table.lastActivityAt, table.id)
|
|
221
232
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
233
|
+
index("idx_post_site_root_thread_updated")
|
|
234
|
+
.on(table.siteId, table.threadUpdatedAt, table.id)
|
|
235
|
+
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
222
236
|
index("idx_post_site_root_draft_updated")
|
|
223
237
|
.on(table.siteId, table.updatedAt, table.id)
|
|
224
238
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'draft'`),
|
|
@@ -227,8 +241,8 @@ export const posts = sqliteTable(
|
|
|
227
241
|
.where(
|
|
228
242
|
sql`${table.replyToId} IS NOT NULL AND ${table.status} = 'published'`,
|
|
229
243
|
),
|
|
230
|
-
index("
|
|
231
|
-
.on(table.siteId, table.
|
|
244
|
+
index("idx_post_site_featured_thread_published")
|
|
245
|
+
.on(table.siteId, table.threadId, table.publishedAt, table.id)
|
|
232
246
|
.where(
|
|
233
247
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
234
248
|
),
|
|
@@ -571,16 +585,16 @@ export const collectionDirectoryItems = sqliteTable(
|
|
|
571
585
|
);
|
|
572
586
|
|
|
573
587
|
// =============================================================================
|
|
574
|
-
//
|
|
588
|
+
// Thread-Collection Junction Table (M:N)
|
|
575
589
|
// =============================================================================
|
|
576
590
|
|
|
577
|
-
export const
|
|
578
|
-
"
|
|
591
|
+
export const threadCollections = sqliteTable(
|
|
592
|
+
"thread_collection",
|
|
579
593
|
{
|
|
580
594
|
siteId: text("site_id")
|
|
581
595
|
.notNull()
|
|
582
596
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
583
|
-
|
|
597
|
+
threadId: text("thread_id")
|
|
584
598
|
.notNull()
|
|
585
599
|
.references(() => posts.id, { onDelete: "cascade" }),
|
|
586
600
|
collectionId: text("collection_id")
|
|
@@ -591,16 +605,16 @@ export const postCollections = sqliteTable(
|
|
|
591
605
|
pinnedAt: integer("pinned_at"),
|
|
592
606
|
},
|
|
593
607
|
(table) => [
|
|
594
|
-
primaryKey({ columns: [table.siteId, table.
|
|
595
|
-
index("
|
|
608
|
+
primaryKey({ columns: [table.siteId, table.threadId, table.collectionId] }),
|
|
609
|
+
index("idx_thread_collection_site_collection_id").on(
|
|
596
610
|
table.siteId,
|
|
597
611
|
table.collectionId,
|
|
598
612
|
),
|
|
599
|
-
index("
|
|
613
|
+
index("idx_thread_collection_site_collection_created_thread").on(
|
|
600
614
|
table.siteId,
|
|
601
615
|
table.collectionId,
|
|
602
616
|
table.createdAt,
|
|
603
|
-
table.
|
|
617
|
+
table.threadId,
|
|
604
618
|
),
|
|
605
619
|
],
|
|
606
620
|
);
|
|
@@ -622,11 +636,14 @@ export const navItems = sqliteTable(
|
|
|
622
636
|
.notNull()
|
|
623
637
|
.default("link"),
|
|
624
638
|
systemKey: text("system_key", {
|
|
625
|
-
enum:
|
|
639
|
+
enum: SYSTEM_NAV_KEY_VALUES,
|
|
626
640
|
}),
|
|
627
641
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
628
642
|
onDelete: "cascade",
|
|
629
643
|
}),
|
|
644
|
+
postId: text("post_id").references(() => posts.id, {
|
|
645
|
+
onDelete: "cascade",
|
|
646
|
+
}),
|
|
630
647
|
label: text("label").notNull(),
|
|
631
648
|
url: text("url").notNull(),
|
|
632
649
|
placement: text("placement", {
|
|
@@ -653,14 +670,22 @@ export const navItems = sqliteTable(
|
|
|
653
670
|
${table.type} = 'link'
|
|
654
671
|
AND ${table.systemKey} IS NULL
|
|
655
672
|
AND ${table.collectionId} IS NULL
|
|
673
|
+
AND ${table.postId} IS NULL
|
|
656
674
|
) OR (
|
|
657
675
|
${table.type} = 'system'
|
|
658
676
|
AND ${table.systemKey} IS NOT NULL
|
|
659
677
|
AND ${table.collectionId} IS NULL
|
|
678
|
+
AND ${table.postId} IS NULL
|
|
660
679
|
) OR (
|
|
661
680
|
${table.type} = 'collection'
|
|
662
681
|
AND ${table.systemKey} IS NULL
|
|
663
682
|
AND ${table.collectionId} IS NOT NULL
|
|
683
|
+
AND ${table.postId} IS NULL
|
|
684
|
+
) OR (
|
|
685
|
+
${table.type} = 'page'
|
|
686
|
+
AND ${table.systemKey} IS NULL
|
|
687
|
+
AND ${table.collectionId} IS NULL
|
|
688
|
+
AND ${table.postId} IS NOT NULL
|
|
664
689
|
)`,
|
|
665
690
|
),
|
|
666
691
|
uniqueIndex("uq_nav_item_site_position").on(table.siteId, table.position),
|
|
@@ -670,6 +695,9 @@ export const navItems = sqliteTable(
|
|
|
670
695
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
671
696
|
.on(table.siteId, table.collectionId)
|
|
672
697
|
.where(sql`${table.collectionId} IS NOT NULL`),
|
|
698
|
+
uniqueIndex("uq_nav_item_site_post_id")
|
|
699
|
+
.on(table.siteId, table.postId)
|
|
700
|
+
.where(sql`${table.postId} IS NOT NULL`),
|
|
673
701
|
],
|
|
674
702
|
);
|
|
675
703
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread activity — one definition, shared by every surface that orders by it.
|
|
3
|
+
*
|
|
4
|
+
* A Thread's activity is when it last **gained a post**. Editing an existing
|
|
5
|
+
* post is not activity: otherwise fixing a typo would drag a years-old thread
|
|
6
|
+
* back to the top of Latest and of every collection that holds it.
|
|
7
|
+
*
|
|
8
|
+
* This lived as three separate copies (the collection thread sort, the
|
|
9
|
+
* collection directory, and the Hugo export). One of them was changed in #144
|
|
10
|
+
* to treat a newer `updated_at` as activity and the others were not, so the
|
|
11
|
+
* same thread ranked differently depending on which page you were looking at,
|
|
12
|
+
* and the drift went unnoticed for months. Adding a fourth copy is the failure
|
|
13
|
+
* mode this module exists to prevent — call it instead.
|
|
14
|
+
*
|
|
15
|
+
* Two flavors of "last activity" exist, and they differ only in how they treat
|
|
16
|
+
* quiet replies:
|
|
17
|
+
*
|
|
18
|
+
* - `last_activity_at` — quiet replies excluded. "Last announced." Latest, the
|
|
19
|
+
* feeds, and collection ordering read this.
|
|
20
|
+
* - `thread_updated_at` — quiet replies included. "Last changed." The
|
|
21
|
+
* archive's updated sort reads this.
|
|
22
|
+
*
|
|
23
|
+
* Both are maintained on the Thread root by the post service; everything here
|
|
24
|
+
* only reads them, with fallbacks for rows that predate the columns.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { sql, type SQL, type SQLWrapper } from "drizzle-orm";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Activity timestamp for a Thread root row, in SQL.
|
|
31
|
+
*
|
|
32
|
+
* Pass column references for a query whose rows are already Thread roots, or
|
|
33
|
+
* raw fragments (`sql.raw("root.last_activity_at")`) when reaching a root
|
|
34
|
+
* through a correlated subquery.
|
|
35
|
+
*
|
|
36
|
+
* **Correlated-subquery warning:** Drizzle only qualifies interpolated columns
|
|
37
|
+
* with their table name when the query has a join. In a single-table
|
|
38
|
+
* `select()`, `${posts.id}` renders as a bare `"id"`, which inside a subquery
|
|
39
|
+
* binds to the subquery's own alias instead of correlating to the outer row —
|
|
40
|
+
* silently, with no error. Write the outer table out (`"post"."id"`) in that
|
|
41
|
+
* case rather than interpolating.
|
|
42
|
+
*
|
|
43
|
+
* @param root - The root's three timestamp columns, most authoritative first
|
|
44
|
+
* @returns COALESCE expression usable in ORDER BY, GROUP BY, or a projection
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* // Rows are already Thread roots (joined on thread_collection.thread_id):
|
|
49
|
+
* const activityAt = buildRootActivityExpr({
|
|
50
|
+
* lastActivityAt: posts.lastActivityAt,
|
|
51
|
+
* publishedAt: posts.publishedAt,
|
|
52
|
+
* updatedAt: posts.updatedAt,
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function buildRootActivityExpr(root: {
|
|
57
|
+
lastActivityAt: SQLWrapper;
|
|
58
|
+
publishedAt: SQLWrapper;
|
|
59
|
+
updatedAt: SQLWrapper;
|
|
60
|
+
}): SQL<number> {
|
|
61
|
+
return sql<number>`COALESCE(
|
|
62
|
+
${root.lastActivityAt},
|
|
63
|
+
${root.publishedAt},
|
|
64
|
+
${root.updatedAt}
|
|
65
|
+
)`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The Thread root's timestamp columns, named for a correlated subquery alias. */
|
|
69
|
+
export function rootActivityColumns(alias: string): {
|
|
70
|
+
lastActivityAt: SQLWrapper;
|
|
71
|
+
publishedAt: SQLWrapper;
|
|
72
|
+
updatedAt: SQLWrapper;
|
|
73
|
+
} {
|
|
74
|
+
return {
|
|
75
|
+
lastActivityAt: sql.raw(`${alias}.last_activity_at`),
|
|
76
|
+
publishedAt: sql.raw(`${alias}.published_at`),
|
|
77
|
+
updatedAt: sql.raw(`${alias}.updated_at`),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Activity timestamp for an already-loaded Thread root, in TypeScript.
|
|
83
|
+
*
|
|
84
|
+
* The same COALESCE order as {@link buildRootActivityExpr}, for callers that
|
|
85
|
+
* hold hydrated posts rather than a query builder.
|
|
86
|
+
*
|
|
87
|
+
* @param root - A Thread root post
|
|
88
|
+
* @returns Unix timestamp (seconds) of the root's last activity
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const activityAt = getRootActivityAt(threadRoot);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function getRootActivityAt(root: {
|
|
96
|
+
lastActivityAt?: number | null;
|
|
97
|
+
publishedAt?: number | null;
|
|
98
|
+
updatedAt: number;
|
|
99
|
+
}): number {
|
|
100
|
+
return root.lastActivityAt ?? root.publishedAt ?? root.updatedAt;
|
|
101
|
+
}
|