@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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collection Service (v2)
|
|
3
3
|
*
|
|
4
|
-
* Manages collections.
|
|
4
|
+
* Manages collections. Threads belong to collections through the
|
|
5
|
+
* `thread_collection` junction table (M:N).
|
|
5
6
|
* Collection directory ordering is managed through the collection_directory_item
|
|
6
7
|
* table with fractional indexing.
|
|
7
8
|
*/
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
supportsDrizzleTransaction,
|
|
15
16
|
} from "../db/index.js";
|
|
16
17
|
import type { DatabaseDialect } from "../db/dialect.js";
|
|
18
|
+
import { buildRootActivityExpr } from "../db/thread-activity.js";
|
|
17
19
|
import {
|
|
18
20
|
sqliteSchemaBundle,
|
|
19
21
|
type DatabaseSchema,
|
|
@@ -33,7 +35,11 @@ import type {
|
|
|
33
35
|
UpdateCollectionDirectoryEntry,
|
|
34
36
|
CollectionSortOrder,
|
|
35
37
|
} from "../types.js";
|
|
36
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
ConflictError,
|
|
40
|
+
NotFoundError,
|
|
41
|
+
ValidationError,
|
|
42
|
+
} from "../lib/errors.js";
|
|
37
43
|
import {
|
|
38
44
|
createPathService,
|
|
39
45
|
toCollectionPath,
|
|
@@ -91,7 +97,7 @@ export interface CollectionService {
|
|
|
91
97
|
): Promise<ResolvedCollectionSelection | null>;
|
|
92
98
|
list(): Promise<Collection[]>;
|
|
93
99
|
listDirectoryData(): Promise<CollectionsDirectoryData>;
|
|
94
|
-
/** List collections sorted by most recent
|
|
100
|
+
/** List collections sorted by most recent Thread addition (for compose dialog) */
|
|
95
101
|
listByRecentActivity(): Promise<Collection[]>;
|
|
96
102
|
create(data: CreateCollection): Promise<Collection>;
|
|
97
103
|
update(id: string, data: UpdateCollection): Promise<Collection | null>;
|
|
@@ -115,16 +121,16 @@ export interface CollectionService {
|
|
|
115
121
|
after: string | null,
|
|
116
122
|
before: string | null,
|
|
117
123
|
): Promise<CollectionDirectoryEntry | null>;
|
|
118
|
-
/** Get
|
|
119
|
-
|
|
124
|
+
/** Get Thread count per collection */
|
|
125
|
+
getThreadCounts(): Promise<Map<string, number>>;
|
|
120
126
|
/**
|
|
121
|
-
* Add a
|
|
127
|
+
* Add a Thread to a collection. Optional metadata lets the caller set the
|
|
122
128
|
* per-row `createdAt` / `position` / `pinnedAt` explicitly; omitted
|
|
123
129
|
* fields default to `now()` / append-at-end / `null`.
|
|
124
130
|
*/
|
|
125
|
-
|
|
131
|
+
addThread(
|
|
126
132
|
collectionId: string,
|
|
127
|
-
|
|
133
|
+
threadId: string,
|
|
128
134
|
opts?: {
|
|
129
135
|
createdAt?: number;
|
|
130
136
|
position?: number;
|
|
@@ -132,13 +138,13 @@ export interface CollectionService {
|
|
|
132
138
|
},
|
|
133
139
|
): Promise<void>;
|
|
134
140
|
/**
|
|
135
|
-
* Replace all collection memberships of a
|
|
141
|
+
* Replace all collection memberships of a Thread in one transaction,
|
|
136
142
|
* preserving each entry's `createdAt` / `position` / `pinnedAt`.
|
|
137
143
|
* Used by the Hugo import path so round-tripping through
|
|
138
144
|
* `jant site export | jant site import` is lossless.
|
|
139
145
|
*/
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
syncThreadCollectionsWithMeta(
|
|
147
|
+
threadId: string,
|
|
142
148
|
entries: {
|
|
143
149
|
collectionId: string;
|
|
144
150
|
createdAt?: number;
|
|
@@ -146,39 +152,39 @@ export interface CollectionService {
|
|
|
146
152
|
pinnedAt?: number | null;
|
|
147
153
|
}[],
|
|
148
154
|
): Promise<void>;
|
|
149
|
-
/** Remove a
|
|
150
|
-
|
|
151
|
-
/** Pin a
|
|
152
|
-
|
|
153
|
-
/** Unpin a
|
|
154
|
-
|
|
155
|
-
/** Get pinned
|
|
156
|
-
|
|
157
|
-
/** Get
|
|
155
|
+
/** Remove a Thread from a collection */
|
|
156
|
+
removeThread(collectionId: string, threadId: string): Promise<void>;
|
|
157
|
+
/** Pin a Thread within a collection */
|
|
158
|
+
pinThread(collectionId: string, threadId: string): Promise<void>;
|
|
159
|
+
/** Unpin a Thread within a collection */
|
|
160
|
+
unpinThread(collectionId: string, threadId: string): Promise<void>;
|
|
161
|
+
/** Get pinned Thread IDs for given collections */
|
|
162
|
+
getPinnedThreadIds(collectionIds: string[]): Promise<Set<string>>;
|
|
163
|
+
/** Get the Thread-level collections shared by a post and its siblings */
|
|
158
164
|
getCollectionsByPostId(postId: string): Promise<Collection[]>;
|
|
159
|
-
/** Batch
|
|
165
|
+
/** Batch project Thread-level collections onto multiple posts */
|
|
160
166
|
getCollectionsByPostIds(
|
|
161
167
|
postIds: string[],
|
|
162
168
|
): Promise<Map<string, Collection[]>>;
|
|
163
169
|
/**
|
|
164
|
-
* Batch get the set of collection IDs each
|
|
170
|
+
* Batch get the set of collection IDs each Thread is pinned in.
|
|
165
171
|
*
|
|
166
172
|
* Used by the Hugo export to surface per-collection pins so the static
|
|
167
|
-
* collection page can sort pinned
|
|
173
|
+
* collection page can sort pinned Threads to the top, mirroring the live
|
|
168
174
|
* site behavior.
|
|
169
175
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
getCollectionPinsByThreadIds(
|
|
177
|
+
threadIds: string[],
|
|
172
178
|
): Promise<Map<string, Set<string>>>;
|
|
173
179
|
/**
|
|
174
|
-
* Batch fetch the full per-entry collection metadata for each
|
|
180
|
+
* Batch fetch the full per-entry collection metadata for each Thread: the
|
|
175
181
|
* `createdAt` timestamp, `position`, and per-collection `pinnedAt`.
|
|
176
182
|
*
|
|
177
183
|
* Used by the Hugo export to emit lossless `collections` front-matter
|
|
178
184
|
* entries so round-tripping through `jant site export | jant site import`
|
|
179
185
|
* preserves per-entry state.
|
|
180
186
|
*/
|
|
181
|
-
|
|
187
|
+
getCollectionEntriesByThreadIds(threadIds: string[]): Promise<
|
|
182
188
|
Map<
|
|
183
189
|
string,
|
|
184
190
|
{
|
|
@@ -189,10 +195,13 @@ export interface CollectionService {
|
|
|
189
195
|
}[]
|
|
190
196
|
>
|
|
191
197
|
>;
|
|
192
|
-
/** Get all
|
|
193
|
-
|
|
194
|
-
/** Sync a
|
|
195
|
-
|
|
198
|
+
/** Get all Thread IDs in a collection */
|
|
199
|
+
getThreadIds(collectionId: string): Promise<string[]>;
|
|
200
|
+
/** Sync a Thread's collection memberships (replace all with given IDs) */
|
|
201
|
+
syncThreadCollections(
|
|
202
|
+
threadId: string,
|
|
203
|
+
collectionIds: string[],
|
|
204
|
+
): Promise<void>;
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
export function createCollectionService(
|
|
@@ -207,7 +216,7 @@ export function createCollectionService(
|
|
|
207
216
|
collections,
|
|
208
217
|
pathRegistry,
|
|
209
218
|
collectionDirectoryItems: directoryItemsTable,
|
|
210
|
-
|
|
219
|
+
threadCollections,
|
|
211
220
|
posts,
|
|
212
221
|
navItems,
|
|
213
222
|
} = databaseSchema;
|
|
@@ -373,6 +382,17 @@ export function createCollectionService(
|
|
|
373
382
|
return rows.length > 0;
|
|
374
383
|
}
|
|
375
384
|
|
|
385
|
+
async function resolveThreadRootId(postId: string): Promise<string> {
|
|
386
|
+
const rows = await db
|
|
387
|
+
.select({ threadId: posts.threadId })
|
|
388
|
+
.from(posts)
|
|
389
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.id, postId)))
|
|
390
|
+
.limit(1);
|
|
391
|
+
const threadId = rows[0]?.threadId;
|
|
392
|
+
if (!threadId) throw new NotFoundError("Thread");
|
|
393
|
+
return threadId;
|
|
394
|
+
}
|
|
395
|
+
|
|
376
396
|
async function getDirectoryMovePosition(
|
|
377
397
|
id: string,
|
|
378
398
|
afterId: string | null,
|
|
@@ -429,22 +449,25 @@ export function createCollectionService(
|
|
|
429
449
|
async function listDirectoryCollections(): Promise<
|
|
430
450
|
CollectionDirectoryCollection[]
|
|
431
451
|
> {
|
|
432
|
-
|
|
452
|
+
// `posts` is joined on thread_collections.thread_id, so each row is
|
|
453
|
+
// already the Thread root — the definition applies to it directly.
|
|
454
|
+
const threadActivityAt = buildRootActivityExpr({
|
|
455
|
+
lastActivityAt: posts.lastActivityAt,
|
|
456
|
+
publishedAt: posts.publishedAt,
|
|
457
|
+
updatedAt: posts.updatedAt,
|
|
458
|
+
});
|
|
459
|
+
const threadCount = sql<number>`
|
|
433
460
|
CAST(COUNT(
|
|
434
461
|
CASE
|
|
435
462
|
WHEN ${posts.id} IS NOT NULL THEN 1
|
|
436
463
|
END
|
|
437
464
|
) AS INTEGER)
|
|
438
|
-
`.as("
|
|
465
|
+
`.as("thread_count");
|
|
439
466
|
const recentActivityAt = sql<number | null>`
|
|
440
467
|
MAX(
|
|
441
468
|
CASE
|
|
442
469
|
WHEN ${posts.id} IS NOT NULL
|
|
443
|
-
THEN
|
|
444
|
-
${posts.lastActivityAt},
|
|
445
|
-
${posts.publishedAt},
|
|
446
|
-
${posts.updatedAt}
|
|
447
|
-
)
|
|
470
|
+
THEN ${threadActivityAt}
|
|
448
471
|
END
|
|
449
472
|
)
|
|
450
473
|
`.as("recent_activity_at");
|
|
@@ -452,20 +475,23 @@ export function createCollectionService(
|
|
|
452
475
|
const rows = await db
|
|
453
476
|
.select({
|
|
454
477
|
collection: collections,
|
|
455
|
-
|
|
478
|
+
threadCount,
|
|
456
479
|
recentActivityAt,
|
|
457
480
|
})
|
|
458
481
|
.from(collections)
|
|
459
482
|
.leftJoin(
|
|
460
|
-
|
|
483
|
+
threadCollections,
|
|
461
484
|
and(
|
|
462
|
-
eq(
|
|
463
|
-
eq(collections.id,
|
|
485
|
+
eq(threadCollections.siteId, siteId),
|
|
486
|
+
eq(collections.id, threadCollections.collectionId),
|
|
464
487
|
),
|
|
465
488
|
)
|
|
466
489
|
.leftJoin(
|
|
467
490
|
posts,
|
|
468
|
-
and(
|
|
491
|
+
and(
|
|
492
|
+
eq(posts.siteId, threadCollections.siteId),
|
|
493
|
+
eq(threadCollections.threadId, posts.id),
|
|
494
|
+
),
|
|
469
495
|
)
|
|
470
496
|
.where(eq(collections.siteId, siteId))
|
|
471
497
|
.groupBy(collections.id)
|
|
@@ -484,7 +510,7 @@ export function createCollectionService(
|
|
|
484
510
|
|
|
485
511
|
return {
|
|
486
512
|
...toCollection(row.collection, slug),
|
|
487
|
-
|
|
513
|
+
threadCount: row.threadCount,
|
|
488
514
|
recentActivityAt: row.recentActivityAt ?? row.collection.updatedAt,
|
|
489
515
|
};
|
|
490
516
|
})
|
|
@@ -616,21 +642,21 @@ export function createCollectionService(
|
|
|
616
642
|
async listByRecentActivity() {
|
|
617
643
|
const lastAddedAt = sql<
|
|
618
644
|
number | null
|
|
619
|
-
>`MAX(${
|
|
645
|
+
>`MAX(${threadCollections.createdAt})`.as("last_added_at");
|
|
620
646
|
const rows = await db
|
|
621
647
|
.select({ collection: collections, lastAddedAt })
|
|
622
648
|
.from(collections)
|
|
623
649
|
.leftJoin(
|
|
624
|
-
|
|
650
|
+
threadCollections,
|
|
625
651
|
and(
|
|
626
|
-
eq(
|
|
627
|
-
eq(collections.id,
|
|
652
|
+
eq(threadCollections.siteId, siteId),
|
|
653
|
+
eq(collections.id, threadCollections.collectionId),
|
|
628
654
|
),
|
|
629
655
|
)
|
|
630
656
|
.where(eq(collections.siteId, siteId))
|
|
631
657
|
.groupBy(collections.id)
|
|
632
658
|
.orderBy(
|
|
633
|
-
desc(sql`COALESCE(MAX(${
|
|
659
|
+
desc(sql`COALESCE(MAX(${threadCollections.createdAt}), 0)`),
|
|
634
660
|
desc(collections.createdAt),
|
|
635
661
|
);
|
|
636
662
|
return hydrateCollections(rows.map((row) => row.collection));
|
|
@@ -814,11 +840,11 @@ export function createCollectionService(
|
|
|
814
840
|
|
|
815
841
|
async delete(id) {
|
|
816
842
|
await db
|
|
817
|
-
.delete(
|
|
843
|
+
.delete(threadCollections)
|
|
818
844
|
.where(
|
|
819
845
|
and(
|
|
820
|
-
eq(
|
|
821
|
-
eq(
|
|
846
|
+
eq(threadCollections.siteId, siteId),
|
|
847
|
+
eq(threadCollections.collectionId, id),
|
|
822
848
|
),
|
|
823
849
|
);
|
|
824
850
|
await db
|
|
@@ -1041,19 +1067,15 @@ export function createCollectionService(
|
|
|
1041
1067
|
throw new Error("Failed to assign a unique directory item position");
|
|
1042
1068
|
},
|
|
1043
1069
|
|
|
1044
|
-
async
|
|
1070
|
+
async getThreadCounts() {
|
|
1045
1071
|
const rows = await db
|
|
1046
1072
|
.select({
|
|
1047
|
-
collectionId:
|
|
1073
|
+
collectionId: threadCollections.collectionId,
|
|
1048
1074
|
count: sql<number>`CAST(count(*) AS INTEGER)`.as("count"),
|
|
1049
1075
|
})
|
|
1050
|
-
.from(
|
|
1051
|
-
.
|
|
1052
|
-
|
|
1053
|
-
sql`post.id = ${postCollections.postId} AND post.site_id = ${siteId}`,
|
|
1054
|
-
)
|
|
1055
|
-
.where(eq(postCollections.siteId, siteId))
|
|
1056
|
-
.groupBy(postCollections.collectionId);
|
|
1076
|
+
.from(threadCollections)
|
|
1077
|
+
.where(eq(threadCollections.siteId, siteId))
|
|
1078
|
+
.groupBy(threadCollections.collectionId);
|
|
1057
1079
|
|
|
1058
1080
|
const counts = new Map<string, number>();
|
|
1059
1081
|
for (const row of rows) {
|
|
@@ -1062,24 +1084,25 @@ export function createCollectionService(
|
|
|
1062
1084
|
return counts;
|
|
1063
1085
|
},
|
|
1064
1086
|
|
|
1065
|
-
async
|
|
1087
|
+
async addThread(collectionId, postId, opts) {
|
|
1088
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1066
1089
|
const [maxRow] = await db
|
|
1067
1090
|
.select({
|
|
1068
|
-
maxPos: sql<number>`COALESCE(MAX(${
|
|
1091
|
+
maxPos: sql<number>`COALESCE(MAX(${threadCollections.position}), -1)`,
|
|
1069
1092
|
})
|
|
1070
|
-
.from(
|
|
1093
|
+
.from(threadCollections)
|
|
1071
1094
|
.where(
|
|
1072
1095
|
and(
|
|
1073
|
-
eq(
|
|
1074
|
-
eq(
|
|
1096
|
+
eq(threadCollections.siteId, siteId),
|
|
1097
|
+
eq(threadCollections.threadId, threadId),
|
|
1075
1098
|
),
|
|
1076
1099
|
);
|
|
1077
1100
|
const nextPosition = (maxRow?.maxPos ?? -1) + 1;
|
|
1078
1101
|
await db
|
|
1079
|
-
.insert(
|
|
1102
|
+
.insert(threadCollections)
|
|
1080
1103
|
.values({
|
|
1081
1104
|
siteId,
|
|
1082
|
-
|
|
1105
|
+
threadId,
|
|
1083
1106
|
collectionId,
|
|
1084
1107
|
createdAt: opts?.createdAt ?? now(),
|
|
1085
1108
|
position: opts?.position ?? nextPosition,
|
|
@@ -1088,12 +1111,12 @@ export function createCollectionService(
|
|
|
1088
1111
|
.onConflictDoNothing();
|
|
1089
1112
|
},
|
|
1090
1113
|
|
|
1091
|
-
async
|
|
1114
|
+
async syncThreadCollectionsWithMeta(threadId, entries) {
|
|
1092
1115
|
const seen = new Set<string>();
|
|
1093
1116
|
const timestamp = now();
|
|
1094
1117
|
const insertValues: {
|
|
1095
1118
|
siteId: string;
|
|
1096
|
-
|
|
1119
|
+
threadId: string;
|
|
1097
1120
|
collectionId: string;
|
|
1098
1121
|
createdAt: number;
|
|
1099
1122
|
position: number;
|
|
@@ -1105,7 +1128,7 @@ export function createCollectionService(
|
|
|
1105
1128
|
seen.add(entry.collectionId);
|
|
1106
1129
|
insertValues.push({
|
|
1107
1130
|
siteId,
|
|
1108
|
-
|
|
1131
|
+
threadId,
|
|
1109
1132
|
collectionId: entry.collectionId,
|
|
1110
1133
|
createdAt: entry.createdAt ?? timestamp,
|
|
1111
1134
|
position: entry.position ?? fallbackPosition,
|
|
@@ -1115,11 +1138,11 @@ export function createCollectionService(
|
|
|
1115
1138
|
}
|
|
1116
1139
|
|
|
1117
1140
|
const deleteQuery = db
|
|
1118
|
-
.delete(
|
|
1141
|
+
.delete(threadCollections)
|
|
1119
1142
|
.where(
|
|
1120
1143
|
and(
|
|
1121
|
-
eq(
|
|
1122
|
-
eq(
|
|
1144
|
+
eq(threadCollections.siteId, siteId),
|
|
1145
|
+
eq(threadCollections.threadId, threadId),
|
|
1123
1146
|
),
|
|
1124
1147
|
);
|
|
1125
1148
|
|
|
@@ -1127,7 +1150,7 @@ export function createCollectionService(
|
|
|
1127
1150
|
const writeQueries = [];
|
|
1128
1151
|
writeQueries.push(deleteQuery);
|
|
1129
1152
|
if (insertValues.length > 0) {
|
|
1130
|
-
writeQueries.push(db.insert(
|
|
1153
|
+
writeQueries.push(db.insert(threadCollections).values(insertValues));
|
|
1131
1154
|
}
|
|
1132
1155
|
await db.batch(
|
|
1133
1156
|
writeQueries as [
|
|
@@ -1140,88 +1163,101 @@ export function createCollectionService(
|
|
|
1140
1163
|
|
|
1141
1164
|
await db.transaction(async (tx) => {
|
|
1142
1165
|
await tx
|
|
1143
|
-
.delete(
|
|
1166
|
+
.delete(threadCollections)
|
|
1144
1167
|
.where(
|
|
1145
1168
|
and(
|
|
1146
|
-
eq(
|
|
1147
|
-
eq(
|
|
1169
|
+
eq(threadCollections.siteId, siteId),
|
|
1170
|
+
eq(threadCollections.threadId, threadId),
|
|
1148
1171
|
),
|
|
1149
1172
|
);
|
|
1150
1173
|
if (insertValues.length > 0) {
|
|
1151
|
-
await tx.insert(
|
|
1174
|
+
await tx.insert(threadCollections).values(insertValues);
|
|
1152
1175
|
}
|
|
1153
1176
|
});
|
|
1154
1177
|
},
|
|
1155
1178
|
|
|
1156
|
-
async
|
|
1179
|
+
async removeThread(collectionId, postId) {
|
|
1180
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1157
1181
|
await db
|
|
1158
|
-
.delete(
|
|
1182
|
+
.delete(threadCollections)
|
|
1159
1183
|
.where(
|
|
1160
1184
|
and(
|
|
1161
|
-
eq(
|
|
1162
|
-
eq(
|
|
1163
|
-
eq(
|
|
1185
|
+
eq(threadCollections.siteId, siteId),
|
|
1186
|
+
eq(threadCollections.threadId, threadId),
|
|
1187
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1164
1188
|
),
|
|
1165
1189
|
);
|
|
1166
1190
|
},
|
|
1167
1191
|
|
|
1168
|
-
async
|
|
1192
|
+
async pinThread(collectionId, postId) {
|
|
1193
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1169
1194
|
await db
|
|
1170
|
-
.update(
|
|
1195
|
+
.update(threadCollections)
|
|
1171
1196
|
.set({ pinnedAt: now() })
|
|
1172
1197
|
.where(
|
|
1173
1198
|
and(
|
|
1174
|
-
eq(
|
|
1175
|
-
eq(
|
|
1176
|
-
eq(
|
|
1199
|
+
eq(threadCollections.siteId, siteId),
|
|
1200
|
+
eq(threadCollections.threadId, threadId),
|
|
1201
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1177
1202
|
),
|
|
1178
1203
|
);
|
|
1179
1204
|
},
|
|
1180
1205
|
|
|
1181
|
-
async
|
|
1206
|
+
async unpinThread(collectionId, postId) {
|
|
1207
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1182
1208
|
await db
|
|
1183
|
-
.update(
|
|
1209
|
+
.update(threadCollections)
|
|
1184
1210
|
.set({ pinnedAt: null })
|
|
1185
1211
|
.where(
|
|
1186
1212
|
and(
|
|
1187
|
-
eq(
|
|
1188
|
-
eq(
|
|
1189
|
-
eq(
|
|
1213
|
+
eq(threadCollections.siteId, siteId),
|
|
1214
|
+
eq(threadCollections.threadId, threadId),
|
|
1215
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1190
1216
|
),
|
|
1191
1217
|
);
|
|
1192
1218
|
},
|
|
1193
1219
|
|
|
1194
|
-
async
|
|
1220
|
+
async getPinnedThreadIds(collectionIds) {
|
|
1195
1221
|
if (collectionIds.length === 0) return new Set<string>();
|
|
1196
1222
|
const rows = await db
|
|
1197
|
-
.select({
|
|
1198
|
-
.from(
|
|
1223
|
+
.select({ threadId: threadCollections.threadId })
|
|
1224
|
+
.from(threadCollections)
|
|
1199
1225
|
.where(
|
|
1200
1226
|
and(
|
|
1201
|
-
eq(
|
|
1202
|
-
inArray(
|
|
1203
|
-
sql`${
|
|
1227
|
+
eq(threadCollections.siteId, siteId),
|
|
1228
|
+
inArray(threadCollections.collectionId, collectionIds),
|
|
1229
|
+
sql`${threadCollections.pinnedAt} IS NOT NULL`,
|
|
1204
1230
|
),
|
|
1205
1231
|
);
|
|
1206
|
-
return new Set(rows.map((
|
|
1232
|
+
return new Set(rows.map((row) => row.threadId));
|
|
1207
1233
|
},
|
|
1208
1234
|
|
|
1209
1235
|
async getCollectionsByPostId(postId) {
|
|
1210
1236
|
const rows = await db
|
|
1211
1237
|
.select({ collection: collections })
|
|
1212
|
-
.from(
|
|
1238
|
+
.from(posts)
|
|
1239
|
+
.innerJoin(
|
|
1240
|
+
threadCollections,
|
|
1241
|
+
and(
|
|
1242
|
+
eq(threadCollections.siteId, posts.siteId),
|
|
1243
|
+
eq(threadCollections.threadId, posts.threadId),
|
|
1244
|
+
),
|
|
1245
|
+
)
|
|
1213
1246
|
.innerJoin(
|
|
1214
1247
|
collections,
|
|
1215
|
-
eq(
|
|
1248
|
+
eq(threadCollections.collectionId, collections.id),
|
|
1216
1249
|
)
|
|
1217
1250
|
.where(
|
|
1218
1251
|
and(
|
|
1219
|
-
eq(
|
|
1252
|
+
eq(posts.siteId, siteId),
|
|
1220
1253
|
eq(collections.siteId, siteId),
|
|
1221
|
-
eq(
|
|
1254
|
+
eq(posts.id, postId),
|
|
1222
1255
|
),
|
|
1223
1256
|
)
|
|
1224
|
-
.orderBy(
|
|
1257
|
+
.orderBy(
|
|
1258
|
+
asc(threadCollections.position),
|
|
1259
|
+
asc(threadCollections.createdAt),
|
|
1260
|
+
);
|
|
1225
1261
|
|
|
1226
1262
|
return hydrateCollections(rows.map((row) => row.collection));
|
|
1227
1263
|
},
|
|
@@ -1230,27 +1266,38 @@ export function createCollectionService(
|
|
|
1230
1266
|
const result = new Map<string, Collection[]>();
|
|
1231
1267
|
if (postIds.length === 0) return result;
|
|
1232
1268
|
|
|
1233
|
-
const
|
|
1269
|
+
const postRows = await batchQueryRows(postIds, (chunk) =>
|
|
1234
1270
|
db
|
|
1235
1271
|
.select({
|
|
1236
|
-
postId:
|
|
1272
|
+
postId: posts.id,
|
|
1273
|
+
threadId: posts.threadId,
|
|
1274
|
+
})
|
|
1275
|
+
.from(posts)
|
|
1276
|
+
.where(and(eq(posts.siteId, siteId), inArray(posts.id, chunk))),
|
|
1277
|
+
);
|
|
1278
|
+
const threadIds = [...new Set(postRows.map((row) => row.threadId))];
|
|
1279
|
+
if (threadIds.length === 0) return result;
|
|
1280
|
+
const rows = await batchQueryRows(threadIds, (chunk) =>
|
|
1281
|
+
db
|
|
1282
|
+
.select({
|
|
1283
|
+
threadId: threadCollections.threadId,
|
|
1237
1284
|
collection: collections,
|
|
1238
1285
|
})
|
|
1239
|
-
.from(
|
|
1286
|
+
.from(threadCollections)
|
|
1240
1287
|
.innerJoin(
|
|
1241
1288
|
collections,
|
|
1242
|
-
eq(
|
|
1289
|
+
eq(threadCollections.collectionId, collections.id),
|
|
1243
1290
|
)
|
|
1244
1291
|
.where(
|
|
1245
1292
|
and(
|
|
1246
|
-
eq(
|
|
1293
|
+
eq(threadCollections.siteId, siteId),
|
|
1247
1294
|
eq(collections.siteId, siteId),
|
|
1248
|
-
inArray(
|
|
1295
|
+
inArray(threadCollections.threadId, chunk),
|
|
1249
1296
|
),
|
|
1250
1297
|
)
|
|
1251
1298
|
.orderBy(
|
|
1252
|
-
asc(
|
|
1253
|
-
asc(
|
|
1299
|
+
asc(threadCollections.position),
|
|
1300
|
+
asc(threadCollections.createdAt),
|
|
1254
1301
|
),
|
|
1255
1302
|
);
|
|
1256
1303
|
|
|
@@ -1259,61 +1306,69 @@ export function createCollectionService(
|
|
|
1259
1306
|
collectionRows.map((row) => row.id),
|
|
1260
1307
|
);
|
|
1261
1308
|
|
|
1309
|
+
const collectionsByThread = new Map<string, Collection[]>();
|
|
1262
1310
|
for (const row of rows) {
|
|
1263
1311
|
const slug = slugMap.get(row.collection.id);
|
|
1264
1312
|
if (!slug) continue;
|
|
1265
|
-
const existing =
|
|
1313
|
+
const existing = collectionsByThread.get(row.threadId) ?? [];
|
|
1266
1314
|
existing.push(toCollection(row.collection, slug));
|
|
1267
|
-
|
|
1315
|
+
collectionsByThread.set(row.threadId, existing);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
for (const row of postRows) {
|
|
1319
|
+
const threadMemberships = collectionsByThread.get(row.threadId);
|
|
1320
|
+
if (threadMemberships) {
|
|
1321
|
+
result.set(row.postId, threadMemberships);
|
|
1322
|
+
}
|
|
1268
1323
|
}
|
|
1269
1324
|
|
|
1270
1325
|
return result;
|
|
1271
1326
|
},
|
|
1272
1327
|
|
|
1273
|
-
async
|
|
1328
|
+
async getCollectionPinsByThreadIds(threadIds) {
|
|
1274
1329
|
const result = new Map<string, Set<string>>();
|
|
1275
|
-
if (
|
|
1330
|
+
if (threadIds.length === 0) return result;
|
|
1276
1331
|
|
|
1277
|
-
const rows = await batchQueryRows(
|
|
1332
|
+
const rows = await batchQueryRows(threadIds, (chunk) =>
|
|
1278
1333
|
db
|
|
1279
1334
|
.select({
|
|
1280
|
-
|
|
1281
|
-
collectionId:
|
|
1335
|
+
threadId: threadCollections.threadId,
|
|
1336
|
+
collectionId: threadCollections.collectionId,
|
|
1282
1337
|
})
|
|
1283
|
-
.from(
|
|
1338
|
+
.from(threadCollections)
|
|
1284
1339
|
.where(
|
|
1285
1340
|
and(
|
|
1286
|
-
eq(
|
|
1287
|
-
inArray(
|
|
1288
|
-
sql`${
|
|
1341
|
+
eq(threadCollections.siteId, siteId),
|
|
1342
|
+
inArray(threadCollections.threadId, chunk),
|
|
1343
|
+
sql`${threadCollections.pinnedAt} IS NOT NULL`,
|
|
1289
1344
|
),
|
|
1290
1345
|
),
|
|
1291
1346
|
);
|
|
1292
1347
|
|
|
1293
1348
|
for (const row of rows) {
|
|
1294
|
-
const existing = result.get(row.
|
|
1349
|
+
const existing = result.get(row.threadId) ?? new Set<string>();
|
|
1295
1350
|
existing.add(row.collectionId);
|
|
1296
|
-
result.set(row.
|
|
1351
|
+
result.set(row.threadId, existing);
|
|
1297
1352
|
}
|
|
1298
1353
|
|
|
1299
1354
|
return result;
|
|
1300
1355
|
},
|
|
1301
1356
|
|
|
1302
|
-
async
|
|
1357
|
+
async getThreadIds(collectionId) {
|
|
1303
1358
|
const rows = await db
|
|
1304
|
-
.select({
|
|
1305
|
-
.from(
|
|
1359
|
+
.select({ threadId: threadCollections.threadId })
|
|
1360
|
+
.from(threadCollections)
|
|
1306
1361
|
.where(
|
|
1307
1362
|
and(
|
|
1308
|
-
eq(
|
|
1309
|
-
eq(
|
|
1363
|
+
eq(threadCollections.siteId, siteId),
|
|
1364
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1310
1365
|
),
|
|
1311
1366
|
);
|
|
1312
1367
|
|
|
1313
|
-
return rows.map((row) => row.
|
|
1368
|
+
return rows.map((row) => row.threadId);
|
|
1314
1369
|
},
|
|
1315
1370
|
|
|
1316
|
-
async
|
|
1371
|
+
async getCollectionEntriesByThreadIds(threadIds) {
|
|
1317
1372
|
const result = new Map<
|
|
1318
1373
|
string,
|
|
1319
1374
|
{
|
|
@@ -1323,58 +1378,59 @@ export function createCollectionService(
|
|
|
1323
1378
|
pinnedAt: number | null;
|
|
1324
1379
|
}[]
|
|
1325
1380
|
>();
|
|
1326
|
-
if (
|
|
1381
|
+
if (threadIds.length === 0) return result;
|
|
1327
1382
|
|
|
1328
|
-
const rows = await batchQueryRows(
|
|
1383
|
+
const rows = await batchQueryRows(threadIds, (chunk) =>
|
|
1329
1384
|
db
|
|
1330
1385
|
.select({
|
|
1331
|
-
|
|
1332
|
-
collectionId:
|
|
1333
|
-
createdAt:
|
|
1334
|
-
position:
|
|
1335
|
-
pinnedAt:
|
|
1386
|
+
threadId: threadCollections.threadId,
|
|
1387
|
+
collectionId: threadCollections.collectionId,
|
|
1388
|
+
createdAt: threadCollections.createdAt,
|
|
1389
|
+
position: threadCollections.position,
|
|
1390
|
+
pinnedAt: threadCollections.pinnedAt,
|
|
1336
1391
|
})
|
|
1337
|
-
.from(
|
|
1392
|
+
.from(threadCollections)
|
|
1338
1393
|
.where(
|
|
1339
1394
|
and(
|
|
1340
|
-
eq(
|
|
1341
|
-
inArray(
|
|
1395
|
+
eq(threadCollections.siteId, siteId),
|
|
1396
|
+
inArray(threadCollections.threadId, chunk),
|
|
1342
1397
|
),
|
|
1343
1398
|
)
|
|
1344
1399
|
.orderBy(
|
|
1345
|
-
asc(
|
|
1346
|
-
asc(
|
|
1400
|
+
asc(threadCollections.position),
|
|
1401
|
+
asc(threadCollections.createdAt),
|
|
1347
1402
|
),
|
|
1348
1403
|
);
|
|
1349
1404
|
|
|
1350
1405
|
for (const row of rows) {
|
|
1351
|
-
const existing = result.get(row.
|
|
1406
|
+
const existing = result.get(row.threadId) ?? [];
|
|
1352
1407
|
existing.push({
|
|
1353
1408
|
collectionId: row.collectionId,
|
|
1354
1409
|
createdAt: row.createdAt,
|
|
1355
1410
|
position: row.position,
|
|
1356
1411
|
pinnedAt: row.pinnedAt,
|
|
1357
1412
|
});
|
|
1358
|
-
result.set(row.
|
|
1413
|
+
result.set(row.threadId, existing);
|
|
1359
1414
|
}
|
|
1360
1415
|
|
|
1361
1416
|
return result;
|
|
1362
1417
|
},
|
|
1363
1418
|
|
|
1364
|
-
async
|
|
1419
|
+
async syncThreadCollections(threadId, collectionIds) {
|
|
1365
1420
|
const nextCollectionIds = [...new Set(collectionIds)];
|
|
1366
1421
|
|
|
1367
1422
|
// Fetch existing rows to preserve createdAt for retained collections
|
|
1368
1423
|
const existingRows = await db
|
|
1369
1424
|
.select({
|
|
1370
|
-
collectionId:
|
|
1371
|
-
createdAt:
|
|
1425
|
+
collectionId: threadCollections.collectionId,
|
|
1426
|
+
createdAt: threadCollections.createdAt,
|
|
1427
|
+
pinnedAt: threadCollections.pinnedAt,
|
|
1372
1428
|
})
|
|
1373
|
-
.from(
|
|
1429
|
+
.from(threadCollections)
|
|
1374
1430
|
.where(
|
|
1375
1431
|
and(
|
|
1376
|
-
eq(
|
|
1377
|
-
eq(
|
|
1432
|
+
eq(threadCollections.siteId, siteId),
|
|
1433
|
+
eq(threadCollections.threadId, threadId),
|
|
1378
1434
|
),
|
|
1379
1435
|
);
|
|
1380
1436
|
|
|
@@ -1382,25 +1438,26 @@ export function createCollectionService(
|
|
|
1382
1438
|
return;
|
|
1383
1439
|
}
|
|
1384
1440
|
|
|
1385
|
-
const
|
|
1386
|
-
existingRows.map((
|
|
1441
|
+
const existingEntries = new Map(
|
|
1442
|
+
existingRows.map((row) => [row.collectionId, row]),
|
|
1387
1443
|
);
|
|
1388
1444
|
const timestamp = now();
|
|
1389
1445
|
const insertValues = nextCollectionIds.map((collectionId, index) => ({
|
|
1390
1446
|
siteId,
|
|
1391
|
-
|
|
1447
|
+
threadId,
|
|
1392
1448
|
collectionId,
|
|
1393
|
-
createdAt:
|
|
1449
|
+
createdAt: existingEntries.get(collectionId)?.createdAt ?? timestamp,
|
|
1394
1450
|
position: index,
|
|
1451
|
+
pinnedAt: existingEntries.get(collectionId)?.pinnedAt ?? null,
|
|
1395
1452
|
}));
|
|
1396
1453
|
|
|
1397
1454
|
// Delete all and re-insert to preserve user-specified ordering
|
|
1398
1455
|
const deleteQuery = db
|
|
1399
|
-
.delete(
|
|
1456
|
+
.delete(threadCollections)
|
|
1400
1457
|
.where(
|
|
1401
1458
|
and(
|
|
1402
|
-
eq(
|
|
1403
|
-
eq(
|
|
1459
|
+
eq(threadCollections.siteId, siteId),
|
|
1460
|
+
eq(threadCollections.threadId, threadId),
|
|
1404
1461
|
),
|
|
1405
1462
|
);
|
|
1406
1463
|
|
|
@@ -1408,7 +1465,7 @@ export function createCollectionService(
|
|
|
1408
1465
|
const writeQueries = [];
|
|
1409
1466
|
writeQueries.push(deleteQuery);
|
|
1410
1467
|
if (insertValues.length > 0) {
|
|
1411
|
-
writeQueries.push(db.insert(
|
|
1468
|
+
writeQueries.push(db.insert(threadCollections).values(insertValues));
|
|
1412
1469
|
}
|
|
1413
1470
|
await db.batch(
|
|
1414
1471
|
writeQueries as [
|
|
@@ -1421,15 +1478,15 @@ export function createCollectionService(
|
|
|
1421
1478
|
|
|
1422
1479
|
await db.transaction(async (tx) => {
|
|
1423
1480
|
await tx
|
|
1424
|
-
.delete(
|
|
1481
|
+
.delete(threadCollections)
|
|
1425
1482
|
.where(
|
|
1426
1483
|
and(
|
|
1427
|
-
eq(
|
|
1428
|
-
eq(
|
|
1484
|
+
eq(threadCollections.siteId, siteId),
|
|
1485
|
+
eq(threadCollections.threadId, threadId),
|
|
1429
1486
|
),
|
|
1430
1487
|
);
|
|
1431
1488
|
if (insertValues.length > 0) {
|
|
1432
|
-
await tx.insert(
|
|
1489
|
+
await tx.insert(threadCollections).values(insertValues);
|
|
1433
1490
|
}
|
|
1434
1491
|
});
|
|
1435
1492
|
},
|