@jant/core 0.3.27 → 0.3.29
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/reset-password.js +22 -0
- package/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +25 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collection Service (v2)
|
|
3
3
|
*
|
|
4
|
-
* Manages collections. Posts belong to collections via
|
|
4
|
+
* Manages collections. Posts belong to collections via post_collections junction table (M:N).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { eq, asc, sql, desc } from "drizzle-orm";
|
|
7
|
+
import { eq, asc, sql, desc, and } from "drizzle-orm";
|
|
8
8
|
import type { Database } from "../db/index.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
collections,
|
|
11
|
+
collectionDividers,
|
|
12
|
+
postCollections,
|
|
13
|
+
} from "../db/schema.js";
|
|
10
14
|
import { now } from "../lib/time.js";
|
|
11
15
|
import type {
|
|
12
16
|
Collection,
|
|
17
|
+
CollectionDivider,
|
|
13
18
|
CreateCollection,
|
|
14
19
|
UpdateCollection,
|
|
15
20
|
SortOrder,
|
|
@@ -23,8 +28,26 @@ export interface CollectionService {
|
|
|
23
28
|
update(id: number, data: UpdateCollection): Promise<Collection | null>;
|
|
24
29
|
delete(id: number): Promise<boolean>;
|
|
25
30
|
reorder(ids: number[]): Promise<void>;
|
|
31
|
+
/** Reorder mixed collections and dividers using prefixed IDs (e.g. "c-1", "d-2") */
|
|
32
|
+
reorderAll(items: string[]): Promise<void>;
|
|
33
|
+
/** Create a standalone divider with auto-assigned position */
|
|
34
|
+
createDivider(): Promise<CollectionDivider>;
|
|
35
|
+
/** Delete a divider by ID */
|
|
36
|
+
deleteDivider(id: number): Promise<boolean>;
|
|
37
|
+
/** List all dividers ordered by position */
|
|
38
|
+
listDividers(): Promise<CollectionDivider[]>;
|
|
26
39
|
/** Get post count per collection */
|
|
27
40
|
getPostCounts(): Promise<Map<number, number>>;
|
|
41
|
+
/** Add a post to a collection */
|
|
42
|
+
addPost(collectionId: number, postId: number): Promise<void>;
|
|
43
|
+
/** Remove a post from a collection */
|
|
44
|
+
removePost(collectionId: number, postId: number): Promise<void>;
|
|
45
|
+
/** Get all collections a post belongs to */
|
|
46
|
+
getCollectionsByPostId(postId: number): Promise<Collection[]>;
|
|
47
|
+
/** Get all post IDs in a collection */
|
|
48
|
+
getPostIds(collectionId: number): Promise<number[]>;
|
|
49
|
+
/** Sync a post's collection memberships (replace all with given IDs) */
|
|
50
|
+
syncPostCollections(postId: number, collectionIds: number[]): Promise<void>;
|
|
28
51
|
}
|
|
29
52
|
|
|
30
53
|
export function createCollectionService(db: Database): CollectionService {
|
|
@@ -37,7 +60,17 @@ export function createCollectionService(db: Database): CollectionService {
|
|
|
37
60
|
icon: row.icon,
|
|
38
61
|
sortOrder: row.sortOrder as SortOrder,
|
|
39
62
|
position: row.position,
|
|
40
|
-
|
|
63
|
+
createdAt: row.createdAt,
|
|
64
|
+
updatedAt: row.updatedAt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toDivider(
|
|
69
|
+
row: typeof collectionDividers.$inferSelect,
|
|
70
|
+
): CollectionDivider {
|
|
71
|
+
return {
|
|
72
|
+
id: row.id,
|
|
73
|
+
position: row.position,
|
|
41
74
|
createdAt: row.createdAt,
|
|
42
75
|
updatedAt: row.updatedAt,
|
|
43
76
|
};
|
|
@@ -75,11 +108,11 @@ export function createCollectionService(db: Database): CollectionService {
|
|
|
75
108
|
|
|
76
109
|
let position = data.position;
|
|
77
110
|
if (position === undefined) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
const result = await db.all<{ maxPos: number }>(
|
|
112
|
+
sql`SELECT COALESCE(MAX(pos), -1) AS maxPos FROM (SELECT position AS pos FROM ${collections} UNION ALL SELECT position AS pos FROM ${collectionDividers})`,
|
|
113
|
+
);
|
|
81
114
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
|
|
82
|
-
position =
|
|
115
|
+
position = result[0]!.maxPos + 1;
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
const result = await db
|
|
@@ -91,7 +124,6 @@ export function createCollectionService(db: Database): CollectionService {
|
|
|
91
124
|
icon: data.icon ?? null,
|
|
92
125
|
sortOrder: data.sortOrder ?? "newest",
|
|
93
126
|
position,
|
|
94
|
-
showDivider: data.showDivider ? 1 : 0,
|
|
95
127
|
createdAt: timestamp,
|
|
96
128
|
updatedAt: timestamp,
|
|
97
129
|
})
|
|
@@ -117,8 +149,6 @@ export function createCollectionService(db: Database): CollectionService {
|
|
|
117
149
|
if (data.icon !== undefined) updates.icon = data.icon;
|
|
118
150
|
if (data.sortOrder !== undefined) updates.sortOrder = data.sortOrder;
|
|
119
151
|
if (data.position !== undefined) updates.position = data.position;
|
|
120
|
-
if (data.showDivider !== undefined)
|
|
121
|
-
updates.showDivider = data.showDivider ? 1 : 0;
|
|
122
152
|
|
|
123
153
|
const result = await db
|
|
124
154
|
.update(collections)
|
|
@@ -130,12 +160,7 @@ export function createCollectionService(db: Database): CollectionService {
|
|
|
130
160
|
},
|
|
131
161
|
|
|
132
162
|
async delete(id) {
|
|
133
|
-
//
|
|
134
|
-
await db
|
|
135
|
-
.update(posts)
|
|
136
|
-
.set({ collectionId: null })
|
|
137
|
-
.where(eq(posts.collectionId, id));
|
|
138
|
-
|
|
163
|
+
// Junction table entries are cleaned up by ON DELETE CASCADE
|
|
139
164
|
const result = await db
|
|
140
165
|
.delete(collections)
|
|
141
166
|
.where(eq(collections.id, id))
|
|
@@ -144,35 +169,149 @@ export function createCollectionService(db: Database): CollectionService {
|
|
|
144
169
|
},
|
|
145
170
|
|
|
146
171
|
async reorder(ids) {
|
|
172
|
+
// Delegate to reorderAll with "c-" prefix for backward compat
|
|
173
|
+
await this.reorderAll(ids.map((id) => `c-${id}`));
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async reorderAll(items) {
|
|
177
|
+
if (items.length === 0) return;
|
|
147
178
|
const timestamp = now();
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
const queries = items.map((item, i) => {
|
|
180
|
+
const [prefix, idStr] = item.split("-");
|
|
181
|
+
const id = Number(idStr);
|
|
182
|
+
if (prefix === "d") {
|
|
183
|
+
return db
|
|
184
|
+
.update(collectionDividers)
|
|
185
|
+
.set({ position: i, updatedAt: timestamp })
|
|
186
|
+
.where(eq(collectionDividers.id, id));
|
|
187
|
+
}
|
|
188
|
+
return db
|
|
150
189
|
.update(collections)
|
|
151
190
|
.set({ position: i, updatedAt: timestamp })
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
191
|
+
.where(eq(collections.id, id));
|
|
192
|
+
});
|
|
193
|
+
await db.batch(
|
|
194
|
+
queries as [(typeof queries)[number], ...(typeof queries)[number][]],
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async createDivider() {
|
|
199
|
+
const timestamp = now();
|
|
200
|
+
|
|
201
|
+
const maxResult = await db.all<{ maxPos: number }>(
|
|
202
|
+
sql`SELECT COALESCE(MAX(pos), -1) AS maxPos FROM (SELECT position AS pos FROM ${collections} UNION ALL SELECT position AS pos FROM ${collectionDividers})`,
|
|
203
|
+
);
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
|
|
205
|
+
const position = maxResult[0]!.maxPos + 1;
|
|
206
|
+
|
|
207
|
+
const result = await db
|
|
208
|
+
.insert(collectionDividers)
|
|
209
|
+
.values({
|
|
210
|
+
position,
|
|
211
|
+
createdAt: timestamp,
|
|
212
|
+
updatedAt: timestamp,
|
|
213
|
+
})
|
|
214
|
+
.returning();
|
|
215
|
+
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
217
|
+
return toDivider(result[0]!);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
async deleteDivider(id) {
|
|
221
|
+
const result = await db
|
|
222
|
+
.delete(collectionDividers)
|
|
223
|
+
.where(eq(collectionDividers.id, id))
|
|
224
|
+
.returning();
|
|
225
|
+
return result.length > 0;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
async listDividers() {
|
|
229
|
+
const rows = await db
|
|
230
|
+
.select()
|
|
231
|
+
.from(collectionDividers)
|
|
232
|
+
.orderBy(asc(collectionDividers.position));
|
|
233
|
+
return rows.map(toDivider);
|
|
155
234
|
},
|
|
156
235
|
|
|
157
236
|
async getPostCounts() {
|
|
158
237
|
const rows = await db
|
|
159
238
|
.select({
|
|
160
|
-
collectionId:
|
|
239
|
+
collectionId: postCollections.collectionId,
|
|
161
240
|
count: sql<number>`count(*)`.as("count"),
|
|
162
241
|
})
|
|
163
|
-
.from(
|
|
164
|
-
.
|
|
165
|
-
sql
|
|
242
|
+
.from(postCollections)
|
|
243
|
+
.innerJoin(
|
|
244
|
+
sql`posts`,
|
|
245
|
+
sql`posts.id = ${postCollections.postId} AND posts.deleted_at IS NULL`,
|
|
166
246
|
)
|
|
167
|
-
.groupBy(
|
|
247
|
+
.groupBy(postCollections.collectionId);
|
|
168
248
|
|
|
169
249
|
const counts = new Map<number, number>();
|
|
170
250
|
for (const row of rows) {
|
|
171
|
-
|
|
172
|
-
counts.set(row.collectionId, row.count);
|
|
173
|
-
}
|
|
251
|
+
counts.set(row.collectionId, row.count);
|
|
174
252
|
}
|
|
175
253
|
return counts;
|
|
176
254
|
},
|
|
255
|
+
|
|
256
|
+
async addPost(collectionId, postId) {
|
|
257
|
+
await db
|
|
258
|
+
.insert(postCollections)
|
|
259
|
+
.values({ postId, collectionId })
|
|
260
|
+
.onConflictDoNothing();
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
async removePost(collectionId, postId) {
|
|
264
|
+
await db
|
|
265
|
+
.delete(postCollections)
|
|
266
|
+
.where(
|
|
267
|
+
and(
|
|
268
|
+
eq(postCollections.postId, postId),
|
|
269
|
+
eq(postCollections.collectionId, collectionId),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async getCollectionsByPostId(postId) {
|
|
275
|
+
const rows = await db
|
|
276
|
+
.select({ collection: collections })
|
|
277
|
+
.from(postCollections)
|
|
278
|
+
.innerJoin(
|
|
279
|
+
collections,
|
|
280
|
+
eq(postCollections.collectionId, collections.id),
|
|
281
|
+
)
|
|
282
|
+
.where(eq(postCollections.postId, postId))
|
|
283
|
+
.orderBy(asc(collections.position));
|
|
284
|
+
|
|
285
|
+
return rows.map((r) => toCollection(r.collection));
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
async getPostIds(collectionId) {
|
|
289
|
+
const rows = await db
|
|
290
|
+
.select({ postId: postCollections.postId })
|
|
291
|
+
.from(postCollections)
|
|
292
|
+
.where(eq(postCollections.collectionId, collectionId));
|
|
293
|
+
|
|
294
|
+
return rows.map((r) => r.postId);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
async syncPostCollections(postId, collectionIds) {
|
|
298
|
+
if (collectionIds.length === 0) {
|
|
299
|
+
// Only delete — single statement, no batch needed
|
|
300
|
+
await db
|
|
301
|
+
.delete(postCollections)
|
|
302
|
+
.where(eq(postCollections.postId, postId));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Delete existing + insert new atomically
|
|
306
|
+
const deleteQuery = db
|
|
307
|
+
.delete(postCollections)
|
|
308
|
+
.where(eq(postCollections.postId, postId));
|
|
309
|
+
const insertQuery = db
|
|
310
|
+
.insert(postCollections)
|
|
311
|
+
.values(
|
|
312
|
+
collectionIds.map((collectionId) => ({ postId, collectionId })),
|
|
313
|
+
);
|
|
314
|
+
await db.batch([deleteQuery, insertQuery]);
|
|
315
|
+
},
|
|
177
316
|
};
|
|
178
317
|
}
|
package/src/services/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./collection.js";
|
|
17
17
|
import { createSearchService, type SearchService } from "./search.js";
|
|
18
18
|
import { createNavItemService, type NavItemService } from "./navigation.js";
|
|
19
|
+
import { createAuthService, type AuthService } from "./auth.js";
|
|
19
20
|
|
|
20
21
|
export interface Services {
|
|
21
22
|
settings: SettingsService;
|
|
@@ -26,11 +27,13 @@ export interface Services {
|
|
|
26
27
|
collections: CollectionService;
|
|
27
28
|
search: SearchService;
|
|
28
29
|
navItems: NavItemService;
|
|
30
|
+
auth: AuthService;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function createServices(db: Database, d1: D1Database): Services {
|
|
34
|
+
const settings = createSettingsService(db);
|
|
32
35
|
return {
|
|
33
|
-
settings
|
|
36
|
+
settings,
|
|
34
37
|
posts: createPostService(db),
|
|
35
38
|
pages: createPageService(db),
|
|
36
39
|
redirects: createRedirectService(db),
|
|
@@ -38,14 +41,16 @@ export function createServices(db: Database, d1: D1Database): Services {
|
|
|
38
41
|
collections: createCollectionService(db),
|
|
39
42
|
search: createSearchService(d1),
|
|
40
43
|
navItems: createNavItemService(db),
|
|
44
|
+
auth: createAuthService(db, settings),
|
|
41
45
|
};
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export type { SettingsService } from "./settings.js";
|
|
45
49
|
export type { PostService, PostFilters } from "./post.js";
|
|
46
|
-
export type { PageService } from "./page.js";
|
|
50
|
+
export type { PageService, PageFilters } from "./page.js";
|
|
47
51
|
export type { RedirectService } from "./redirect.js";
|
|
48
|
-
export type { MediaService } from "./media.js";
|
|
52
|
+
export type { MediaService, MediaFilters } from "./media.js";
|
|
49
53
|
export type { CollectionService } from "./collection.js";
|
|
50
54
|
export type { SearchService, SearchResult, SearchOptions } from "./search.js";
|
|
51
55
|
export type { NavItemService } from "./navigation.js";
|
|
56
|
+
export type { AuthService } from "./auth.js";
|
package/src/services/media.ts
CHANGED
|
@@ -4,24 +4,31 @@
|
|
|
4
4
|
* Handles media upload and management with pluggable storage backends.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { eq, desc, inArray, asc } from "drizzle-orm";
|
|
7
|
+
import { eq, desc, inArray, asc, sql, and } from "drizzle-orm";
|
|
8
8
|
import { uuidv7 } from "uuidv7";
|
|
9
9
|
import type { Database } from "../db/index.js";
|
|
10
10
|
import { media } from "../db/schema.js";
|
|
11
11
|
import { now } from "../lib/time.js";
|
|
12
12
|
import type { Media } from "../types.js";
|
|
13
13
|
|
|
14
|
+
export interface MediaFilters {
|
|
15
|
+
limit?: number;
|
|
16
|
+
/** Filter by MIME type prefix, e.g. "image/" */
|
|
17
|
+
mimePrefix?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export interface MediaService {
|
|
15
21
|
getById(id: string): Promise<Media | null>;
|
|
16
22
|
getByIds(ids: string[]): Promise<Media[]>;
|
|
17
23
|
getByPostId(postId: number): Promise<Media[]>;
|
|
18
24
|
getByPostIds(postIds: number[]): Promise<Map<number, Media[]>>;
|
|
19
|
-
list(
|
|
25
|
+
list(filters?: MediaFilters): Promise<Media[]>;
|
|
20
26
|
create(data: CreateMediaData): Promise<Media>;
|
|
21
27
|
delete(id: string): Promise<boolean>;
|
|
22
28
|
getByStorageKey(storageKey: string): Promise<Media | null>;
|
|
23
29
|
attachToPost(postId: number, mediaIds: string[]): Promise<void>;
|
|
24
30
|
detachFromPost(postId: number): Promise<void>;
|
|
31
|
+
updateAlt(id: string, alt: string): Promise<void>;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface CreateMediaData {
|
|
@@ -118,10 +125,18 @@ export function createMediaService(db: Database): MediaService {
|
|
|
118
125
|
return result[0] ? toMedia(result[0]) : null;
|
|
119
126
|
},
|
|
120
127
|
|
|
121
|
-
async list(
|
|
128
|
+
async list(filters?: MediaFilters) {
|
|
129
|
+
const limit = filters?.limit ?? 100;
|
|
130
|
+
const conditions = [];
|
|
131
|
+
if (filters?.mimePrefix) {
|
|
132
|
+
conditions.push(
|
|
133
|
+
sql`${media.mimeType} LIKE ${filters.mimePrefix + "%"}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
122
136
|
const rows = await db
|
|
123
137
|
.select()
|
|
124
138
|
.from(media)
|
|
139
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
125
140
|
.orderBy(desc(media.createdAt))
|
|
126
141
|
.limit(limit);
|
|
127
142
|
return rows.map(toMedia);
|
|
@@ -156,21 +171,29 @@ export function createMediaService(db: Database): MediaService {
|
|
|
156
171
|
},
|
|
157
172
|
|
|
158
173
|
async attachToPost(postId, mediaIds) {
|
|
159
|
-
|
|
160
|
-
await db
|
|
174
|
+
const clearQuery = db
|
|
161
175
|
.update(media)
|
|
162
176
|
.set({ postId: null, position: 0 })
|
|
163
177
|
.where(eq(media.postId, postId));
|
|
164
178
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
const validIds = mediaIds.filter((id): id is string => Boolean(id));
|
|
180
|
+
if (validIds.length === 0) {
|
|
181
|
+
// Only clear — single statement, no batch needed
|
|
182
|
+
await clearQuery;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Clear existing + re-attach atomically
|
|
187
|
+
const attachQueries = validIds.map((mediaId, i) =>
|
|
188
|
+
db
|
|
170
189
|
.update(media)
|
|
171
190
|
.set({ postId, position: i })
|
|
172
|
-
.where(eq(media.id, mediaId))
|
|
173
|
-
|
|
191
|
+
.where(eq(media.id, mediaId)),
|
|
192
|
+
);
|
|
193
|
+
await db.batch([clearQuery, ...attachQueries] as [
|
|
194
|
+
typeof clearQuery,
|
|
195
|
+
...(typeof attachQueries)[number][],
|
|
196
|
+
]);
|
|
174
197
|
},
|
|
175
198
|
|
|
176
199
|
async detachFromPost(postId) {
|
|
@@ -180,6 +203,10 @@ export function createMediaService(db: Database): MediaService {
|
|
|
180
203
|
.where(eq(media.postId, postId));
|
|
181
204
|
},
|
|
182
205
|
|
|
206
|
+
async updateAlt(id, alt) {
|
|
207
|
+
await db.update(media).set({ alt }).where(eq(media.id, id));
|
|
208
|
+
},
|
|
209
|
+
|
|
183
210
|
async delete(id) {
|
|
184
211
|
const result = await db.delete(media).where(eq(media.id, id)).returning();
|
|
185
212
|
return result.length > 0;
|
|
@@ -21,6 +21,7 @@ export interface NavItemService {
|
|
|
21
21
|
create(data: CreateNavItem): Promise<NavItem>;
|
|
22
22
|
update(id: number, data: UpdateNavItem): Promise<NavItem | null>;
|
|
23
23
|
delete(id: number): Promise<boolean>;
|
|
24
|
+
deleteByPageId(pageId: number): Promise<boolean>;
|
|
24
25
|
reorder(ids: number[]): Promise<void>;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -118,15 +119,26 @@ export function createNavItemService(db: Database): NavItemService {
|
|
|
118
119
|
return result.length > 0;
|
|
119
120
|
},
|
|
120
121
|
|
|
122
|
+
async deleteByPageId(pageId) {
|
|
123
|
+
const result = await db
|
|
124
|
+
.delete(navItems)
|
|
125
|
+
.where(eq(navItems.pageId, pageId))
|
|
126
|
+
.returning();
|
|
127
|
+
return result.length > 0;
|
|
128
|
+
},
|
|
129
|
+
|
|
121
130
|
async reorder(ids) {
|
|
131
|
+
if (ids.length === 0) return;
|
|
122
132
|
const timestamp = now();
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
const queries = ids.map((id, i) =>
|
|
134
|
+
db
|
|
125
135
|
.update(navItems)
|
|
126
136
|
.set({ position: i, updatedAt: timestamp })
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
.where(eq(navItems.id, id)),
|
|
138
|
+
);
|
|
139
|
+
await db.batch(
|
|
140
|
+
queries as [(typeof queries)[number], ...(typeof queries)[number][]],
|
|
141
|
+
);
|
|
130
142
|
},
|
|
131
143
|
};
|
|
132
144
|
}
|
package/src/services/page.ts
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
* CRUD operations for standalone pages (about, now, etc.)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { eq, desc, sql } from "drizzle-orm";
|
|
7
|
+
import { eq, desc, sql, and } from "drizzle-orm";
|
|
8
8
|
import type { Database } from "../db/index.js";
|
|
9
9
|
import { pages, navItems } from "../db/schema.js";
|
|
10
10
|
import { now } from "../lib/time.js";
|
|
11
11
|
import { render as renderMarkdown } from "../lib/markdown.js";
|
|
12
12
|
import type { Page, Status, CreatePage, UpdatePage } from "../types.js";
|
|
13
13
|
|
|
14
|
+
export interface PageFilters {
|
|
15
|
+
status?: Status;
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export interface PageService {
|
|
15
19
|
getById(id: number): Promise<Page | null>;
|
|
16
20
|
getBySlug(slug: string): Promise<Page | null>;
|
|
17
|
-
list(): Promise<Page[]>;
|
|
21
|
+
list(filters?: PageFilters): Promise<Page[]>;
|
|
18
22
|
listNotInNav(): Promise<Page[]>;
|
|
19
23
|
create(data: CreatePage): Promise<Page>;
|
|
20
24
|
update(id: number, data: UpdatePage): Promise<Page | null>;
|
|
@@ -54,8 +58,16 @@ export function createPageService(db: Database): PageService {
|
|
|
54
58
|
return result[0] ? toPage(result[0]) : null;
|
|
55
59
|
},
|
|
56
60
|
|
|
57
|
-
async list() {
|
|
58
|
-
const
|
|
61
|
+
async list(filters?: PageFilters) {
|
|
62
|
+
const conditions = [];
|
|
63
|
+
if (filters?.status) {
|
|
64
|
+
conditions.push(eq(pages.status, filters.status));
|
|
65
|
+
}
|
|
66
|
+
const rows = await db
|
|
67
|
+
.select()
|
|
68
|
+
.from(pages)
|
|
69
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
70
|
+
.orderBy(desc(pages.createdAt));
|
|
59
71
|
return rows.map(toPage);
|
|
60
72
|
},
|
|
61
73
|
|
|
@@ -118,6 +130,14 @@ export function createPageService(db: Database): PageService {
|
|
|
118
130
|
.where(eq(navItems.pageId, id));
|
|
119
131
|
}
|
|
120
132
|
|
|
133
|
+
// If title changed, update related nav_items label
|
|
134
|
+
if (data.title !== undefined && data.title !== existing.title) {
|
|
135
|
+
await db
|
|
136
|
+
.update(navItems)
|
|
137
|
+
.set({ label: data.title ?? existing.slug, updatedAt: timestamp })
|
|
138
|
+
.where(eq(navItems.pageId, id));
|
|
139
|
+
}
|
|
140
|
+
|
|
121
141
|
const result = await db
|
|
122
142
|
.update(pages)
|
|
123
143
|
.set(updates)
|
package/src/services/post.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
|
|
10
|
+
import type { BatchItem } from "drizzle-orm/batch";
|
|
10
11
|
import type { Database } from "../db/index.js";
|
|
11
|
-
import { posts } from "../db/schema.js";
|
|
12
|
+
import { posts, postCollections } from "../db/schema.js";
|
|
12
13
|
import { now } from "../lib/time.js";
|
|
13
14
|
import { render as renderMarkdown } from "../lib/markdown.js";
|
|
14
15
|
import type { Format, Status, Post, CreatePost, UpdatePost } from "../types.js";
|
|
@@ -70,7 +71,10 @@ export function createPostService(db: Database): PostService {
|
|
|
70
71
|
conditions.push(eq(posts.format, filters.format));
|
|
71
72
|
}
|
|
72
73
|
if (filters.collectionId !== undefined) {
|
|
73
|
-
|
|
74
|
+
// Filter by collection via junction table
|
|
75
|
+
conditions.push(
|
|
76
|
+
sql`${posts.id} IN (SELECT post_id FROM post_collections WHERE collection_id = ${filters.collectionId})`,
|
|
77
|
+
);
|
|
74
78
|
}
|
|
75
79
|
if (filters.threadId) {
|
|
76
80
|
conditions.push(eq(posts.threadId, filters.threadId));
|
|
@@ -99,7 +103,6 @@ export function createPostService(db: Database): PostService {
|
|
|
99
103
|
bodyHtml: row.bodyHtml,
|
|
100
104
|
quoteText: row.quoteText,
|
|
101
105
|
rating: row.rating,
|
|
102
|
-
collectionId: row.collectionId,
|
|
103
106
|
replyToId: row.replyToId,
|
|
104
107
|
threadId: row.threadId,
|
|
105
108
|
deletedAt: row.deletedAt,
|
|
@@ -200,7 +203,6 @@ export function createPostService(db: Database): PostService {
|
|
|
200
203
|
bodyHtml,
|
|
201
204
|
quoteText: data.quoteText ?? null,
|
|
202
205
|
rating: data.rating ?? null,
|
|
203
|
-
collectionId: data.collectionId ?? null,
|
|
204
206
|
replyToId: data.replyToId ?? null,
|
|
205
207
|
threadId,
|
|
206
208
|
publishedAt: data.publishedAt ?? timestamp,
|
|
@@ -210,7 +212,19 @@ export function createPostService(db: Database): PostService {
|
|
|
210
212
|
.returning();
|
|
211
213
|
|
|
212
214
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
213
|
-
|
|
215
|
+
const post = toPost(result[0]!);
|
|
216
|
+
|
|
217
|
+
// Sync collection memberships if provided
|
|
218
|
+
if (data.collectionIds && data.collectionIds.length > 0) {
|
|
219
|
+
await db.insert(postCollections).values(
|
|
220
|
+
data.collectionIds.map((collectionId) => ({
|
|
221
|
+
postId: post.id,
|
|
222
|
+
collectionId,
|
|
223
|
+
})),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return post;
|
|
214
228
|
},
|
|
215
229
|
|
|
216
230
|
async update(id, data) {
|
|
@@ -228,8 +242,6 @@ export function createPostService(db: Database): PostService {
|
|
|
228
242
|
if (data.url !== undefined) updates.url = data.url;
|
|
229
243
|
if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
|
|
230
244
|
if (data.rating !== undefined) updates.rating = data.rating;
|
|
231
|
-
if (data.collectionId !== undefined)
|
|
232
|
-
updates.collectionId = data.collectionId;
|
|
233
245
|
if (data.publishedAt !== undefined)
|
|
234
246
|
updates.publishedAt = data.publishedAt;
|
|
235
247
|
if (data.pinned !== undefined) updates.pinned = data.pinned ? 1 : 0;
|
|
@@ -249,22 +261,78 @@ export function createPostService(db: Database): PostService {
|
|
|
249
261
|
if (statusChanged) updates.status = data.status;
|
|
250
262
|
if (featuredChanged) updates.featured = data.featured ? 1 : 0;
|
|
251
263
|
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
264
|
+
// Build all write queries for atomic execution via D1 batch
|
|
265
|
+
const needsCascade =
|
|
266
|
+
(statusChanged || featuredChanged) && !existing.threadId;
|
|
267
|
+
const needsCollectionSync = data.collectionIds !== undefined;
|
|
268
|
+
const hasExtraWrites = needsCascade || needsCollectionSync;
|
|
269
|
+
|
|
270
|
+
if (!hasExtraWrites) {
|
|
271
|
+
// Simple case: only the post update
|
|
272
|
+
const result = await db
|
|
273
|
+
.update(posts)
|
|
274
|
+
.set(updates)
|
|
275
|
+
.where(eq(posts.id, id))
|
|
276
|
+
.returning();
|
|
277
|
+
return result[0] ? toPost(result[0]) : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Complex case: batch cascade + update + collection sync atomically
|
|
281
|
+
const writeQueries: BatchItem<"sqlite">[] = [];
|
|
282
|
+
|
|
283
|
+
if (needsCascade) {
|
|
284
|
+
writeQueries.push(
|
|
285
|
+
db
|
|
286
|
+
.update(posts)
|
|
287
|
+
.set({
|
|
288
|
+
status: data.status ?? (existing.status as Status),
|
|
289
|
+
featured: (
|
|
290
|
+
data.featured !== undefined
|
|
291
|
+
? data.featured
|
|
292
|
+
: existing.featured === 1
|
|
293
|
+
)
|
|
294
|
+
? 1
|
|
295
|
+
: 0,
|
|
296
|
+
updatedAt: timestamp,
|
|
297
|
+
})
|
|
298
|
+
.where(eq(posts.threadId, id)),
|
|
258
299
|
);
|
|
259
300
|
}
|
|
260
301
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
.where(eq(posts.id, id))
|
|
265
|
-
|
|
302
|
+
// Post update is always present; track its index for result extraction
|
|
303
|
+
const updateIdx = writeQueries.length;
|
|
304
|
+
writeQueries.push(
|
|
305
|
+
db.update(posts).set(updates).where(eq(posts.id, id)).returning(),
|
|
306
|
+
);
|
|
266
307
|
|
|
267
|
-
|
|
308
|
+
if (needsCollectionSync) {
|
|
309
|
+
writeQueries.push(
|
|
310
|
+
db.delete(postCollections).where(eq(postCollections.postId, id)),
|
|
311
|
+
);
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by needsCollectionSync
|
|
313
|
+
if (data.collectionIds!.length > 0) {
|
|
314
|
+
writeQueries.push(
|
|
315
|
+
db.insert(postCollections).values(
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by needsCollectionSync
|
|
317
|
+
data.collectionIds!.map((collectionId) => ({
|
|
318
|
+
postId: id,
|
|
319
|
+
collectionId,
|
|
320
|
+
})),
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const results = await db.batch(
|
|
327
|
+
writeQueries as [
|
|
328
|
+
(typeof writeQueries)[number],
|
|
329
|
+
...(typeof writeQueries)[number][],
|
|
330
|
+
],
|
|
331
|
+
);
|
|
332
|
+
const updateResult = results[updateIdx] as
|
|
333
|
+
| (typeof posts.$inferSelect)[]
|
|
334
|
+
| undefined;
|
|
335
|
+
return updateResult?.[0] ? toPost(updateResult[0]) : null;
|
|
268
336
|
},
|
|
269
337
|
|
|
270
338
|
async delete(id) {
|
package/src/services/search.ts
CHANGED
|
@@ -106,7 +106,6 @@ export function createSearchService(d1: D1Database): SearchService {
|
|
|
106
106
|
bodyHtml: row.body_html,
|
|
107
107
|
quoteText: row.quote_text,
|
|
108
108
|
rating: row.rating,
|
|
109
|
-
collectionId: row.collection_id,
|
|
110
109
|
replyToId: row.reply_to_id,
|
|
111
110
|
threadId: row.thread_id,
|
|
112
111
|
deletedAt: row.deleted_at,
|