@jant/core 0.6.14 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -1,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
|
*/
|
|
@@ -33,7 +34,11 @@ import type {
|
|
|
33
34
|
UpdateCollectionDirectoryEntry,
|
|
34
35
|
CollectionSortOrder,
|
|
35
36
|
} from "../types.js";
|
|
36
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
ConflictError,
|
|
39
|
+
NotFoundError,
|
|
40
|
+
ValidationError,
|
|
41
|
+
} from "../lib/errors.js";
|
|
37
42
|
import {
|
|
38
43
|
createPathService,
|
|
39
44
|
toCollectionPath,
|
|
@@ -91,7 +96,7 @@ export interface CollectionService {
|
|
|
91
96
|
): Promise<ResolvedCollectionSelection | null>;
|
|
92
97
|
list(): Promise<Collection[]>;
|
|
93
98
|
listDirectoryData(): Promise<CollectionsDirectoryData>;
|
|
94
|
-
/** List collections sorted by most recent
|
|
99
|
+
/** List collections sorted by most recent Thread addition (for compose dialog) */
|
|
95
100
|
listByRecentActivity(): Promise<Collection[]>;
|
|
96
101
|
create(data: CreateCollection): Promise<Collection>;
|
|
97
102
|
update(id: string, data: UpdateCollection): Promise<Collection | null>;
|
|
@@ -115,16 +120,16 @@ export interface CollectionService {
|
|
|
115
120
|
after: string | null,
|
|
116
121
|
before: string | null,
|
|
117
122
|
): Promise<CollectionDirectoryEntry | null>;
|
|
118
|
-
/** Get
|
|
119
|
-
|
|
123
|
+
/** Get Thread count per collection */
|
|
124
|
+
getThreadCounts(): Promise<Map<string, number>>;
|
|
120
125
|
/**
|
|
121
|
-
* Add a
|
|
126
|
+
* Add a Thread to a collection. Optional metadata lets the caller set the
|
|
122
127
|
* per-row `createdAt` / `position` / `pinnedAt` explicitly; omitted
|
|
123
128
|
* fields default to `now()` / append-at-end / `null`.
|
|
124
129
|
*/
|
|
125
|
-
|
|
130
|
+
addThread(
|
|
126
131
|
collectionId: string,
|
|
127
|
-
|
|
132
|
+
threadId: string,
|
|
128
133
|
opts?: {
|
|
129
134
|
createdAt?: number;
|
|
130
135
|
position?: number;
|
|
@@ -132,13 +137,13 @@ export interface CollectionService {
|
|
|
132
137
|
},
|
|
133
138
|
): Promise<void>;
|
|
134
139
|
/**
|
|
135
|
-
* Replace all collection memberships of a
|
|
140
|
+
* Replace all collection memberships of a Thread in one transaction,
|
|
136
141
|
* preserving each entry's `createdAt` / `position` / `pinnedAt`.
|
|
137
142
|
* Used by the Hugo import path so round-tripping through
|
|
138
143
|
* `jant site export | jant site import` is lossless.
|
|
139
144
|
*/
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
syncThreadCollectionsWithMeta(
|
|
146
|
+
threadId: string,
|
|
142
147
|
entries: {
|
|
143
148
|
collectionId: string;
|
|
144
149
|
createdAt?: number;
|
|
@@ -146,39 +151,39 @@ export interface CollectionService {
|
|
|
146
151
|
pinnedAt?: number | null;
|
|
147
152
|
}[],
|
|
148
153
|
): Promise<void>;
|
|
149
|
-
/** Remove a
|
|
150
|
-
|
|
151
|
-
/** Pin a
|
|
152
|
-
|
|
153
|
-
/** Unpin a
|
|
154
|
-
|
|
155
|
-
/** Get pinned
|
|
156
|
-
|
|
157
|
-
/** Get
|
|
154
|
+
/** Remove a Thread from a collection */
|
|
155
|
+
removeThread(collectionId: string, threadId: string): Promise<void>;
|
|
156
|
+
/** Pin a Thread within a collection */
|
|
157
|
+
pinThread(collectionId: string, threadId: string): Promise<void>;
|
|
158
|
+
/** Unpin a Thread within a collection */
|
|
159
|
+
unpinThread(collectionId: string, threadId: string): Promise<void>;
|
|
160
|
+
/** Get pinned Thread IDs for given collections */
|
|
161
|
+
getPinnedThreadIds(collectionIds: string[]): Promise<Set<string>>;
|
|
162
|
+
/** Get the Thread-level collections shared by a post and its siblings */
|
|
158
163
|
getCollectionsByPostId(postId: string): Promise<Collection[]>;
|
|
159
|
-
/** Batch
|
|
164
|
+
/** Batch project Thread-level collections onto multiple posts */
|
|
160
165
|
getCollectionsByPostIds(
|
|
161
166
|
postIds: string[],
|
|
162
167
|
): Promise<Map<string, Collection[]>>;
|
|
163
168
|
/**
|
|
164
|
-
* Batch get the set of collection IDs each
|
|
169
|
+
* Batch get the set of collection IDs each Thread is pinned in.
|
|
165
170
|
*
|
|
166
171
|
* Used by the Hugo export to surface per-collection pins so the static
|
|
167
|
-
* collection page can sort pinned
|
|
172
|
+
* collection page can sort pinned Threads to the top, mirroring the live
|
|
168
173
|
* site behavior.
|
|
169
174
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
getCollectionPinsByThreadIds(
|
|
176
|
+
threadIds: string[],
|
|
172
177
|
): Promise<Map<string, Set<string>>>;
|
|
173
178
|
/**
|
|
174
|
-
* Batch fetch the full per-entry collection metadata for each
|
|
179
|
+
* Batch fetch the full per-entry collection metadata for each Thread: the
|
|
175
180
|
* `createdAt` timestamp, `position`, and per-collection `pinnedAt`.
|
|
176
181
|
*
|
|
177
182
|
* Used by the Hugo export to emit lossless `collections` front-matter
|
|
178
183
|
* entries so round-tripping through `jant site export | jant site import`
|
|
179
184
|
* preserves per-entry state.
|
|
180
185
|
*/
|
|
181
|
-
|
|
186
|
+
getCollectionEntriesByThreadIds(threadIds: string[]): Promise<
|
|
182
187
|
Map<
|
|
183
188
|
string,
|
|
184
189
|
{
|
|
@@ -189,10 +194,13 @@ export interface CollectionService {
|
|
|
189
194
|
}[]
|
|
190
195
|
>
|
|
191
196
|
>;
|
|
192
|
-
/** Get all
|
|
193
|
-
|
|
194
|
-
/** Sync a
|
|
195
|
-
|
|
197
|
+
/** Get all Thread IDs in a collection */
|
|
198
|
+
getThreadIds(collectionId: string): Promise<string[]>;
|
|
199
|
+
/** Sync a Thread's collection memberships (replace all with given IDs) */
|
|
200
|
+
syncThreadCollections(
|
|
201
|
+
threadId: string,
|
|
202
|
+
collectionIds: string[],
|
|
203
|
+
): Promise<void>;
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
export function createCollectionService(
|
|
@@ -207,7 +215,7 @@ export function createCollectionService(
|
|
|
207
215
|
collections,
|
|
208
216
|
pathRegistry,
|
|
209
217
|
collectionDirectoryItems: directoryItemsTable,
|
|
210
|
-
|
|
218
|
+
threadCollections,
|
|
211
219
|
posts,
|
|
212
220
|
navItems,
|
|
213
221
|
} = databaseSchema;
|
|
@@ -373,6 +381,17 @@ export function createCollectionService(
|
|
|
373
381
|
return rows.length > 0;
|
|
374
382
|
}
|
|
375
383
|
|
|
384
|
+
async function resolveThreadRootId(postId: string): Promise<string> {
|
|
385
|
+
const rows = await db
|
|
386
|
+
.select({ threadId: posts.threadId })
|
|
387
|
+
.from(posts)
|
|
388
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.id, postId)))
|
|
389
|
+
.limit(1);
|
|
390
|
+
const threadId = rows[0]?.threadId;
|
|
391
|
+
if (!threadId) throw new NotFoundError("Thread");
|
|
392
|
+
return threadId;
|
|
393
|
+
}
|
|
394
|
+
|
|
376
395
|
async function getDirectoryMovePosition(
|
|
377
396
|
id: string,
|
|
378
397
|
afterId: string | null,
|
|
@@ -429,22 +448,28 @@ export function createCollectionService(
|
|
|
429
448
|
async function listDirectoryCollections(): Promise<
|
|
430
449
|
CollectionDirectoryCollection[]
|
|
431
450
|
> {
|
|
432
|
-
const
|
|
451
|
+
const threadActivityAt = sql<number>`CASE
|
|
452
|
+
WHEN ${posts.updatedAt} > ${posts.createdAt}
|
|
453
|
+
AND ${posts.updatedAt} > COALESCE(${posts.lastActivityAt}, -1)
|
|
454
|
+
THEN ${posts.updatedAt}
|
|
455
|
+
ELSE COALESCE(
|
|
456
|
+
${posts.lastActivityAt},
|
|
457
|
+
${posts.publishedAt},
|
|
458
|
+
${posts.updatedAt}
|
|
459
|
+
)
|
|
460
|
+
END`;
|
|
461
|
+
const threadCount = sql<number>`
|
|
433
462
|
CAST(COUNT(
|
|
434
463
|
CASE
|
|
435
464
|
WHEN ${posts.id} IS NOT NULL THEN 1
|
|
436
465
|
END
|
|
437
466
|
) AS INTEGER)
|
|
438
|
-
`.as("
|
|
467
|
+
`.as("thread_count");
|
|
439
468
|
const recentActivityAt = sql<number | null>`
|
|
440
469
|
MAX(
|
|
441
470
|
CASE
|
|
442
471
|
WHEN ${posts.id} IS NOT NULL
|
|
443
|
-
THEN
|
|
444
|
-
${posts.lastActivityAt},
|
|
445
|
-
${posts.publishedAt},
|
|
446
|
-
${posts.updatedAt}
|
|
447
|
-
)
|
|
472
|
+
THEN ${threadActivityAt}
|
|
448
473
|
END
|
|
449
474
|
)
|
|
450
475
|
`.as("recent_activity_at");
|
|
@@ -452,20 +477,23 @@ export function createCollectionService(
|
|
|
452
477
|
const rows = await db
|
|
453
478
|
.select({
|
|
454
479
|
collection: collections,
|
|
455
|
-
|
|
480
|
+
threadCount,
|
|
456
481
|
recentActivityAt,
|
|
457
482
|
})
|
|
458
483
|
.from(collections)
|
|
459
484
|
.leftJoin(
|
|
460
|
-
|
|
485
|
+
threadCollections,
|
|
461
486
|
and(
|
|
462
|
-
eq(
|
|
463
|
-
eq(collections.id,
|
|
487
|
+
eq(threadCollections.siteId, siteId),
|
|
488
|
+
eq(collections.id, threadCollections.collectionId),
|
|
464
489
|
),
|
|
465
490
|
)
|
|
466
491
|
.leftJoin(
|
|
467
492
|
posts,
|
|
468
|
-
and(
|
|
493
|
+
and(
|
|
494
|
+
eq(posts.siteId, threadCollections.siteId),
|
|
495
|
+
eq(threadCollections.threadId, posts.id),
|
|
496
|
+
),
|
|
469
497
|
)
|
|
470
498
|
.where(eq(collections.siteId, siteId))
|
|
471
499
|
.groupBy(collections.id)
|
|
@@ -484,7 +512,7 @@ export function createCollectionService(
|
|
|
484
512
|
|
|
485
513
|
return {
|
|
486
514
|
...toCollection(row.collection, slug),
|
|
487
|
-
|
|
515
|
+
threadCount: row.threadCount,
|
|
488
516
|
recentActivityAt: row.recentActivityAt ?? row.collection.updatedAt,
|
|
489
517
|
};
|
|
490
518
|
})
|
|
@@ -616,21 +644,21 @@ export function createCollectionService(
|
|
|
616
644
|
async listByRecentActivity() {
|
|
617
645
|
const lastAddedAt = sql<
|
|
618
646
|
number | null
|
|
619
|
-
>`MAX(${
|
|
647
|
+
>`MAX(${threadCollections.createdAt})`.as("last_added_at");
|
|
620
648
|
const rows = await db
|
|
621
649
|
.select({ collection: collections, lastAddedAt })
|
|
622
650
|
.from(collections)
|
|
623
651
|
.leftJoin(
|
|
624
|
-
|
|
652
|
+
threadCollections,
|
|
625
653
|
and(
|
|
626
|
-
eq(
|
|
627
|
-
eq(collections.id,
|
|
654
|
+
eq(threadCollections.siteId, siteId),
|
|
655
|
+
eq(collections.id, threadCollections.collectionId),
|
|
628
656
|
),
|
|
629
657
|
)
|
|
630
658
|
.where(eq(collections.siteId, siteId))
|
|
631
659
|
.groupBy(collections.id)
|
|
632
660
|
.orderBy(
|
|
633
|
-
desc(sql`COALESCE(MAX(${
|
|
661
|
+
desc(sql`COALESCE(MAX(${threadCollections.createdAt}), 0)`),
|
|
634
662
|
desc(collections.createdAt),
|
|
635
663
|
);
|
|
636
664
|
return hydrateCollections(rows.map((row) => row.collection));
|
|
@@ -814,11 +842,11 @@ export function createCollectionService(
|
|
|
814
842
|
|
|
815
843
|
async delete(id) {
|
|
816
844
|
await db
|
|
817
|
-
.delete(
|
|
845
|
+
.delete(threadCollections)
|
|
818
846
|
.where(
|
|
819
847
|
and(
|
|
820
|
-
eq(
|
|
821
|
-
eq(
|
|
848
|
+
eq(threadCollections.siteId, siteId),
|
|
849
|
+
eq(threadCollections.collectionId, id),
|
|
822
850
|
),
|
|
823
851
|
);
|
|
824
852
|
await db
|
|
@@ -1041,19 +1069,15 @@ export function createCollectionService(
|
|
|
1041
1069
|
throw new Error("Failed to assign a unique directory item position");
|
|
1042
1070
|
},
|
|
1043
1071
|
|
|
1044
|
-
async
|
|
1072
|
+
async getThreadCounts() {
|
|
1045
1073
|
const rows = await db
|
|
1046
1074
|
.select({
|
|
1047
|
-
collectionId:
|
|
1075
|
+
collectionId: threadCollections.collectionId,
|
|
1048
1076
|
count: sql<number>`CAST(count(*) AS INTEGER)`.as("count"),
|
|
1049
1077
|
})
|
|
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);
|
|
1078
|
+
.from(threadCollections)
|
|
1079
|
+
.where(eq(threadCollections.siteId, siteId))
|
|
1080
|
+
.groupBy(threadCollections.collectionId);
|
|
1057
1081
|
|
|
1058
1082
|
const counts = new Map<string, number>();
|
|
1059
1083
|
for (const row of rows) {
|
|
@@ -1062,24 +1086,25 @@ export function createCollectionService(
|
|
|
1062
1086
|
return counts;
|
|
1063
1087
|
},
|
|
1064
1088
|
|
|
1065
|
-
async
|
|
1089
|
+
async addThread(collectionId, postId, opts) {
|
|
1090
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1066
1091
|
const [maxRow] = await db
|
|
1067
1092
|
.select({
|
|
1068
|
-
maxPos: sql<number>`COALESCE(MAX(${
|
|
1093
|
+
maxPos: sql<number>`COALESCE(MAX(${threadCollections.position}), -1)`,
|
|
1069
1094
|
})
|
|
1070
|
-
.from(
|
|
1095
|
+
.from(threadCollections)
|
|
1071
1096
|
.where(
|
|
1072
1097
|
and(
|
|
1073
|
-
eq(
|
|
1074
|
-
eq(
|
|
1098
|
+
eq(threadCollections.siteId, siteId),
|
|
1099
|
+
eq(threadCollections.threadId, threadId),
|
|
1075
1100
|
),
|
|
1076
1101
|
);
|
|
1077
1102
|
const nextPosition = (maxRow?.maxPos ?? -1) + 1;
|
|
1078
1103
|
await db
|
|
1079
|
-
.insert(
|
|
1104
|
+
.insert(threadCollections)
|
|
1080
1105
|
.values({
|
|
1081
1106
|
siteId,
|
|
1082
|
-
|
|
1107
|
+
threadId,
|
|
1083
1108
|
collectionId,
|
|
1084
1109
|
createdAt: opts?.createdAt ?? now(),
|
|
1085
1110
|
position: opts?.position ?? nextPosition,
|
|
@@ -1088,12 +1113,12 @@ export function createCollectionService(
|
|
|
1088
1113
|
.onConflictDoNothing();
|
|
1089
1114
|
},
|
|
1090
1115
|
|
|
1091
|
-
async
|
|
1116
|
+
async syncThreadCollectionsWithMeta(threadId, entries) {
|
|
1092
1117
|
const seen = new Set<string>();
|
|
1093
1118
|
const timestamp = now();
|
|
1094
1119
|
const insertValues: {
|
|
1095
1120
|
siteId: string;
|
|
1096
|
-
|
|
1121
|
+
threadId: string;
|
|
1097
1122
|
collectionId: string;
|
|
1098
1123
|
createdAt: number;
|
|
1099
1124
|
position: number;
|
|
@@ -1105,7 +1130,7 @@ export function createCollectionService(
|
|
|
1105
1130
|
seen.add(entry.collectionId);
|
|
1106
1131
|
insertValues.push({
|
|
1107
1132
|
siteId,
|
|
1108
|
-
|
|
1133
|
+
threadId,
|
|
1109
1134
|
collectionId: entry.collectionId,
|
|
1110
1135
|
createdAt: entry.createdAt ?? timestamp,
|
|
1111
1136
|
position: entry.position ?? fallbackPosition,
|
|
@@ -1115,11 +1140,11 @@ export function createCollectionService(
|
|
|
1115
1140
|
}
|
|
1116
1141
|
|
|
1117
1142
|
const deleteQuery = db
|
|
1118
|
-
.delete(
|
|
1143
|
+
.delete(threadCollections)
|
|
1119
1144
|
.where(
|
|
1120
1145
|
and(
|
|
1121
|
-
eq(
|
|
1122
|
-
eq(
|
|
1146
|
+
eq(threadCollections.siteId, siteId),
|
|
1147
|
+
eq(threadCollections.threadId, threadId),
|
|
1123
1148
|
),
|
|
1124
1149
|
);
|
|
1125
1150
|
|
|
@@ -1127,7 +1152,7 @@ export function createCollectionService(
|
|
|
1127
1152
|
const writeQueries = [];
|
|
1128
1153
|
writeQueries.push(deleteQuery);
|
|
1129
1154
|
if (insertValues.length > 0) {
|
|
1130
|
-
writeQueries.push(db.insert(
|
|
1155
|
+
writeQueries.push(db.insert(threadCollections).values(insertValues));
|
|
1131
1156
|
}
|
|
1132
1157
|
await db.batch(
|
|
1133
1158
|
writeQueries as [
|
|
@@ -1140,88 +1165,101 @@ export function createCollectionService(
|
|
|
1140
1165
|
|
|
1141
1166
|
await db.transaction(async (tx) => {
|
|
1142
1167
|
await tx
|
|
1143
|
-
.delete(
|
|
1168
|
+
.delete(threadCollections)
|
|
1144
1169
|
.where(
|
|
1145
1170
|
and(
|
|
1146
|
-
eq(
|
|
1147
|
-
eq(
|
|
1171
|
+
eq(threadCollections.siteId, siteId),
|
|
1172
|
+
eq(threadCollections.threadId, threadId),
|
|
1148
1173
|
),
|
|
1149
1174
|
);
|
|
1150
1175
|
if (insertValues.length > 0) {
|
|
1151
|
-
await tx.insert(
|
|
1176
|
+
await tx.insert(threadCollections).values(insertValues);
|
|
1152
1177
|
}
|
|
1153
1178
|
});
|
|
1154
1179
|
},
|
|
1155
1180
|
|
|
1156
|
-
async
|
|
1181
|
+
async removeThread(collectionId, postId) {
|
|
1182
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1157
1183
|
await db
|
|
1158
|
-
.delete(
|
|
1184
|
+
.delete(threadCollections)
|
|
1159
1185
|
.where(
|
|
1160
1186
|
and(
|
|
1161
|
-
eq(
|
|
1162
|
-
eq(
|
|
1163
|
-
eq(
|
|
1187
|
+
eq(threadCollections.siteId, siteId),
|
|
1188
|
+
eq(threadCollections.threadId, threadId),
|
|
1189
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1164
1190
|
),
|
|
1165
1191
|
);
|
|
1166
1192
|
},
|
|
1167
1193
|
|
|
1168
|
-
async
|
|
1194
|
+
async pinThread(collectionId, postId) {
|
|
1195
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1169
1196
|
await db
|
|
1170
|
-
.update(
|
|
1197
|
+
.update(threadCollections)
|
|
1171
1198
|
.set({ pinnedAt: now() })
|
|
1172
1199
|
.where(
|
|
1173
1200
|
and(
|
|
1174
|
-
eq(
|
|
1175
|
-
eq(
|
|
1176
|
-
eq(
|
|
1201
|
+
eq(threadCollections.siteId, siteId),
|
|
1202
|
+
eq(threadCollections.threadId, threadId),
|
|
1203
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1177
1204
|
),
|
|
1178
1205
|
);
|
|
1179
1206
|
},
|
|
1180
1207
|
|
|
1181
|
-
async
|
|
1208
|
+
async unpinThread(collectionId, postId) {
|
|
1209
|
+
const threadId = await resolveThreadRootId(postId);
|
|
1182
1210
|
await db
|
|
1183
|
-
.update(
|
|
1211
|
+
.update(threadCollections)
|
|
1184
1212
|
.set({ pinnedAt: null })
|
|
1185
1213
|
.where(
|
|
1186
1214
|
and(
|
|
1187
|
-
eq(
|
|
1188
|
-
eq(
|
|
1189
|
-
eq(
|
|
1215
|
+
eq(threadCollections.siteId, siteId),
|
|
1216
|
+
eq(threadCollections.threadId, threadId),
|
|
1217
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1190
1218
|
),
|
|
1191
1219
|
);
|
|
1192
1220
|
},
|
|
1193
1221
|
|
|
1194
|
-
async
|
|
1222
|
+
async getPinnedThreadIds(collectionIds) {
|
|
1195
1223
|
if (collectionIds.length === 0) return new Set<string>();
|
|
1196
1224
|
const rows = await db
|
|
1197
|
-
.select({
|
|
1198
|
-
.from(
|
|
1225
|
+
.select({ threadId: threadCollections.threadId })
|
|
1226
|
+
.from(threadCollections)
|
|
1199
1227
|
.where(
|
|
1200
1228
|
and(
|
|
1201
|
-
eq(
|
|
1202
|
-
inArray(
|
|
1203
|
-
sql`${
|
|
1229
|
+
eq(threadCollections.siteId, siteId),
|
|
1230
|
+
inArray(threadCollections.collectionId, collectionIds),
|
|
1231
|
+
sql`${threadCollections.pinnedAt} IS NOT NULL`,
|
|
1204
1232
|
),
|
|
1205
1233
|
);
|
|
1206
|
-
return new Set(rows.map((
|
|
1234
|
+
return new Set(rows.map((row) => row.threadId));
|
|
1207
1235
|
},
|
|
1208
1236
|
|
|
1209
1237
|
async getCollectionsByPostId(postId) {
|
|
1210
1238
|
const rows = await db
|
|
1211
1239
|
.select({ collection: collections })
|
|
1212
|
-
.from(
|
|
1240
|
+
.from(posts)
|
|
1241
|
+
.innerJoin(
|
|
1242
|
+
threadCollections,
|
|
1243
|
+
and(
|
|
1244
|
+
eq(threadCollections.siteId, posts.siteId),
|
|
1245
|
+
eq(threadCollections.threadId, posts.threadId),
|
|
1246
|
+
),
|
|
1247
|
+
)
|
|
1213
1248
|
.innerJoin(
|
|
1214
1249
|
collections,
|
|
1215
|
-
eq(
|
|
1250
|
+
eq(threadCollections.collectionId, collections.id),
|
|
1216
1251
|
)
|
|
1217
1252
|
.where(
|
|
1218
1253
|
and(
|
|
1219
|
-
eq(
|
|
1254
|
+
eq(posts.siteId, siteId),
|
|
1220
1255
|
eq(collections.siteId, siteId),
|
|
1221
|
-
eq(
|
|
1256
|
+
eq(posts.id, postId),
|
|
1222
1257
|
),
|
|
1223
1258
|
)
|
|
1224
|
-
.orderBy(
|
|
1259
|
+
.orderBy(
|
|
1260
|
+
asc(threadCollections.position),
|
|
1261
|
+
asc(threadCollections.createdAt),
|
|
1262
|
+
);
|
|
1225
1263
|
|
|
1226
1264
|
return hydrateCollections(rows.map((row) => row.collection));
|
|
1227
1265
|
},
|
|
@@ -1230,27 +1268,38 @@ export function createCollectionService(
|
|
|
1230
1268
|
const result = new Map<string, Collection[]>();
|
|
1231
1269
|
if (postIds.length === 0) return result;
|
|
1232
1270
|
|
|
1233
|
-
const
|
|
1271
|
+
const postRows = await batchQueryRows(postIds, (chunk) =>
|
|
1272
|
+
db
|
|
1273
|
+
.select({
|
|
1274
|
+
postId: posts.id,
|
|
1275
|
+
threadId: posts.threadId,
|
|
1276
|
+
})
|
|
1277
|
+
.from(posts)
|
|
1278
|
+
.where(and(eq(posts.siteId, siteId), inArray(posts.id, chunk))),
|
|
1279
|
+
);
|
|
1280
|
+
const threadIds = [...new Set(postRows.map((row) => row.threadId))];
|
|
1281
|
+
if (threadIds.length === 0) return result;
|
|
1282
|
+
const rows = await batchQueryRows(threadIds, (chunk) =>
|
|
1234
1283
|
db
|
|
1235
1284
|
.select({
|
|
1236
|
-
|
|
1285
|
+
threadId: threadCollections.threadId,
|
|
1237
1286
|
collection: collections,
|
|
1238
1287
|
})
|
|
1239
|
-
.from(
|
|
1288
|
+
.from(threadCollections)
|
|
1240
1289
|
.innerJoin(
|
|
1241
1290
|
collections,
|
|
1242
|
-
eq(
|
|
1291
|
+
eq(threadCollections.collectionId, collections.id),
|
|
1243
1292
|
)
|
|
1244
1293
|
.where(
|
|
1245
1294
|
and(
|
|
1246
|
-
eq(
|
|
1295
|
+
eq(threadCollections.siteId, siteId),
|
|
1247
1296
|
eq(collections.siteId, siteId),
|
|
1248
|
-
inArray(
|
|
1297
|
+
inArray(threadCollections.threadId, chunk),
|
|
1249
1298
|
),
|
|
1250
1299
|
)
|
|
1251
1300
|
.orderBy(
|
|
1252
|
-
asc(
|
|
1253
|
-
asc(
|
|
1301
|
+
asc(threadCollections.position),
|
|
1302
|
+
asc(threadCollections.createdAt),
|
|
1254
1303
|
),
|
|
1255
1304
|
);
|
|
1256
1305
|
|
|
@@ -1259,61 +1308,69 @@ export function createCollectionService(
|
|
|
1259
1308
|
collectionRows.map((row) => row.id),
|
|
1260
1309
|
);
|
|
1261
1310
|
|
|
1311
|
+
const collectionsByThread = new Map<string, Collection[]>();
|
|
1262
1312
|
for (const row of rows) {
|
|
1263
1313
|
const slug = slugMap.get(row.collection.id);
|
|
1264
1314
|
if (!slug) continue;
|
|
1265
|
-
const existing =
|
|
1315
|
+
const existing = collectionsByThread.get(row.threadId) ?? [];
|
|
1266
1316
|
existing.push(toCollection(row.collection, slug));
|
|
1267
|
-
|
|
1317
|
+
collectionsByThread.set(row.threadId, existing);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
for (const row of postRows) {
|
|
1321
|
+
const threadMemberships = collectionsByThread.get(row.threadId);
|
|
1322
|
+
if (threadMemberships) {
|
|
1323
|
+
result.set(row.postId, threadMemberships);
|
|
1324
|
+
}
|
|
1268
1325
|
}
|
|
1269
1326
|
|
|
1270
1327
|
return result;
|
|
1271
1328
|
},
|
|
1272
1329
|
|
|
1273
|
-
async
|
|
1330
|
+
async getCollectionPinsByThreadIds(threadIds) {
|
|
1274
1331
|
const result = new Map<string, Set<string>>();
|
|
1275
|
-
if (
|
|
1332
|
+
if (threadIds.length === 0) return result;
|
|
1276
1333
|
|
|
1277
|
-
const rows = await batchQueryRows(
|
|
1334
|
+
const rows = await batchQueryRows(threadIds, (chunk) =>
|
|
1278
1335
|
db
|
|
1279
1336
|
.select({
|
|
1280
|
-
|
|
1281
|
-
collectionId:
|
|
1337
|
+
threadId: threadCollections.threadId,
|
|
1338
|
+
collectionId: threadCollections.collectionId,
|
|
1282
1339
|
})
|
|
1283
|
-
.from(
|
|
1340
|
+
.from(threadCollections)
|
|
1284
1341
|
.where(
|
|
1285
1342
|
and(
|
|
1286
|
-
eq(
|
|
1287
|
-
inArray(
|
|
1288
|
-
sql`${
|
|
1343
|
+
eq(threadCollections.siteId, siteId),
|
|
1344
|
+
inArray(threadCollections.threadId, chunk),
|
|
1345
|
+
sql`${threadCollections.pinnedAt} IS NOT NULL`,
|
|
1289
1346
|
),
|
|
1290
1347
|
),
|
|
1291
1348
|
);
|
|
1292
1349
|
|
|
1293
1350
|
for (const row of rows) {
|
|
1294
|
-
const existing = result.get(row.
|
|
1351
|
+
const existing = result.get(row.threadId) ?? new Set<string>();
|
|
1295
1352
|
existing.add(row.collectionId);
|
|
1296
|
-
result.set(row.
|
|
1353
|
+
result.set(row.threadId, existing);
|
|
1297
1354
|
}
|
|
1298
1355
|
|
|
1299
1356
|
return result;
|
|
1300
1357
|
},
|
|
1301
1358
|
|
|
1302
|
-
async
|
|
1359
|
+
async getThreadIds(collectionId) {
|
|
1303
1360
|
const rows = await db
|
|
1304
|
-
.select({
|
|
1305
|
-
.from(
|
|
1361
|
+
.select({ threadId: threadCollections.threadId })
|
|
1362
|
+
.from(threadCollections)
|
|
1306
1363
|
.where(
|
|
1307
1364
|
and(
|
|
1308
|
-
eq(
|
|
1309
|
-
eq(
|
|
1365
|
+
eq(threadCollections.siteId, siteId),
|
|
1366
|
+
eq(threadCollections.collectionId, collectionId),
|
|
1310
1367
|
),
|
|
1311
1368
|
);
|
|
1312
1369
|
|
|
1313
|
-
return rows.map((row) => row.
|
|
1370
|
+
return rows.map((row) => row.threadId);
|
|
1314
1371
|
},
|
|
1315
1372
|
|
|
1316
|
-
async
|
|
1373
|
+
async getCollectionEntriesByThreadIds(threadIds) {
|
|
1317
1374
|
const result = new Map<
|
|
1318
1375
|
string,
|
|
1319
1376
|
{
|
|
@@ -1323,58 +1380,59 @@ export function createCollectionService(
|
|
|
1323
1380
|
pinnedAt: number | null;
|
|
1324
1381
|
}[]
|
|
1325
1382
|
>();
|
|
1326
|
-
if (
|
|
1383
|
+
if (threadIds.length === 0) return result;
|
|
1327
1384
|
|
|
1328
|
-
const rows = await batchQueryRows(
|
|
1385
|
+
const rows = await batchQueryRows(threadIds, (chunk) =>
|
|
1329
1386
|
db
|
|
1330
1387
|
.select({
|
|
1331
|
-
|
|
1332
|
-
collectionId:
|
|
1333
|
-
createdAt:
|
|
1334
|
-
position:
|
|
1335
|
-
pinnedAt:
|
|
1388
|
+
threadId: threadCollections.threadId,
|
|
1389
|
+
collectionId: threadCollections.collectionId,
|
|
1390
|
+
createdAt: threadCollections.createdAt,
|
|
1391
|
+
position: threadCollections.position,
|
|
1392
|
+
pinnedAt: threadCollections.pinnedAt,
|
|
1336
1393
|
})
|
|
1337
|
-
.from(
|
|
1394
|
+
.from(threadCollections)
|
|
1338
1395
|
.where(
|
|
1339
1396
|
and(
|
|
1340
|
-
eq(
|
|
1341
|
-
inArray(
|
|
1397
|
+
eq(threadCollections.siteId, siteId),
|
|
1398
|
+
inArray(threadCollections.threadId, chunk),
|
|
1342
1399
|
),
|
|
1343
1400
|
)
|
|
1344
1401
|
.orderBy(
|
|
1345
|
-
asc(
|
|
1346
|
-
asc(
|
|
1402
|
+
asc(threadCollections.position),
|
|
1403
|
+
asc(threadCollections.createdAt),
|
|
1347
1404
|
),
|
|
1348
1405
|
);
|
|
1349
1406
|
|
|
1350
1407
|
for (const row of rows) {
|
|
1351
|
-
const existing = result.get(row.
|
|
1408
|
+
const existing = result.get(row.threadId) ?? [];
|
|
1352
1409
|
existing.push({
|
|
1353
1410
|
collectionId: row.collectionId,
|
|
1354
1411
|
createdAt: row.createdAt,
|
|
1355
1412
|
position: row.position,
|
|
1356
1413
|
pinnedAt: row.pinnedAt,
|
|
1357
1414
|
});
|
|
1358
|
-
result.set(row.
|
|
1415
|
+
result.set(row.threadId, existing);
|
|
1359
1416
|
}
|
|
1360
1417
|
|
|
1361
1418
|
return result;
|
|
1362
1419
|
},
|
|
1363
1420
|
|
|
1364
|
-
async
|
|
1421
|
+
async syncThreadCollections(threadId, collectionIds) {
|
|
1365
1422
|
const nextCollectionIds = [...new Set(collectionIds)];
|
|
1366
1423
|
|
|
1367
1424
|
// Fetch existing rows to preserve createdAt for retained collections
|
|
1368
1425
|
const existingRows = await db
|
|
1369
1426
|
.select({
|
|
1370
|
-
collectionId:
|
|
1371
|
-
createdAt:
|
|
1427
|
+
collectionId: threadCollections.collectionId,
|
|
1428
|
+
createdAt: threadCollections.createdAt,
|
|
1429
|
+
pinnedAt: threadCollections.pinnedAt,
|
|
1372
1430
|
})
|
|
1373
|
-
.from(
|
|
1431
|
+
.from(threadCollections)
|
|
1374
1432
|
.where(
|
|
1375
1433
|
and(
|
|
1376
|
-
eq(
|
|
1377
|
-
eq(
|
|
1434
|
+
eq(threadCollections.siteId, siteId),
|
|
1435
|
+
eq(threadCollections.threadId, threadId),
|
|
1378
1436
|
),
|
|
1379
1437
|
);
|
|
1380
1438
|
|
|
@@ -1382,25 +1440,26 @@ export function createCollectionService(
|
|
|
1382
1440
|
return;
|
|
1383
1441
|
}
|
|
1384
1442
|
|
|
1385
|
-
const
|
|
1386
|
-
existingRows.map((
|
|
1443
|
+
const existingEntries = new Map(
|
|
1444
|
+
existingRows.map((row) => [row.collectionId, row]),
|
|
1387
1445
|
);
|
|
1388
1446
|
const timestamp = now();
|
|
1389
1447
|
const insertValues = nextCollectionIds.map((collectionId, index) => ({
|
|
1390
1448
|
siteId,
|
|
1391
|
-
|
|
1449
|
+
threadId,
|
|
1392
1450
|
collectionId,
|
|
1393
|
-
createdAt:
|
|
1451
|
+
createdAt: existingEntries.get(collectionId)?.createdAt ?? timestamp,
|
|
1394
1452
|
position: index,
|
|
1453
|
+
pinnedAt: existingEntries.get(collectionId)?.pinnedAt ?? null,
|
|
1395
1454
|
}));
|
|
1396
1455
|
|
|
1397
1456
|
// Delete all and re-insert to preserve user-specified ordering
|
|
1398
1457
|
const deleteQuery = db
|
|
1399
|
-
.delete(
|
|
1458
|
+
.delete(threadCollections)
|
|
1400
1459
|
.where(
|
|
1401
1460
|
and(
|
|
1402
|
-
eq(
|
|
1403
|
-
eq(
|
|
1461
|
+
eq(threadCollections.siteId, siteId),
|
|
1462
|
+
eq(threadCollections.threadId, threadId),
|
|
1404
1463
|
),
|
|
1405
1464
|
);
|
|
1406
1465
|
|
|
@@ -1408,7 +1467,7 @@ export function createCollectionService(
|
|
|
1408
1467
|
const writeQueries = [];
|
|
1409
1468
|
writeQueries.push(deleteQuery);
|
|
1410
1469
|
if (insertValues.length > 0) {
|
|
1411
|
-
writeQueries.push(db.insert(
|
|
1470
|
+
writeQueries.push(db.insert(threadCollections).values(insertValues));
|
|
1412
1471
|
}
|
|
1413
1472
|
await db.batch(
|
|
1414
1473
|
writeQueries as [
|
|
@@ -1421,15 +1480,15 @@ export function createCollectionService(
|
|
|
1421
1480
|
|
|
1422
1481
|
await db.transaction(async (tx) => {
|
|
1423
1482
|
await tx
|
|
1424
|
-
.delete(
|
|
1483
|
+
.delete(threadCollections)
|
|
1425
1484
|
.where(
|
|
1426
1485
|
and(
|
|
1427
|
-
eq(
|
|
1428
|
-
eq(
|
|
1486
|
+
eq(threadCollections.siteId, siteId),
|
|
1487
|
+
eq(threadCollections.threadId, threadId),
|
|
1429
1488
|
),
|
|
1430
1489
|
);
|
|
1431
1490
|
if (insertValues.length > 0) {
|
|
1432
|
-
await tx.insert(
|
|
1491
|
+
await tx.insert(threadCollections).values(insertValues);
|
|
1433
1492
|
}
|
|
1434
1493
|
});
|
|
1435
1494
|
},
|