@jant/core 0.3.23 → 0.3.25
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/dist/app.js +50 -26
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
package/dist/services/post.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Post Service
|
|
2
|
+
* Post Service (v2)
|
|
3
3
|
*
|
|
4
|
-
* CRUD operations for posts with Thread support
|
|
5
|
-
|
|
4
|
+
* CRUD operations for posts with Thread support.
|
|
5
|
+
* Posts have format (note/link/quote), status (draft/published),
|
|
6
|
+
* featured flag, and pinned flag.
|
|
7
|
+
*/ import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
|
|
6
8
|
import { posts } from "../db/schema.js";
|
|
7
9
|
import { now } from "../lib/time.js";
|
|
8
|
-
import { extractDomain } from "../lib/url.js";
|
|
9
10
|
import { render as renderMarkdown } from "../lib/markdown.js";
|
|
10
11
|
export function createPostService(db) {
|
|
11
|
-
// Helper to map DB row to Post type
|
|
12
12
|
function toPost(row) {
|
|
13
13
|
return {
|
|
14
14
|
id: row.id,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
format: row.format,
|
|
16
|
+
status: row.status,
|
|
17
|
+
featured: row.featured,
|
|
18
|
+
pinned: row.pinned,
|
|
18
19
|
path: row.path,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
title: row.title,
|
|
21
|
+
url: row.url,
|
|
22
|
+
body: row.body,
|
|
23
|
+
bodyHtml: row.bodyHtml,
|
|
24
|
+
quoteText: row.quoteText,
|
|
25
|
+
rating: row.rating,
|
|
26
|
+
collectionId: row.collectionId,
|
|
24
27
|
replyToId: row.replyToId,
|
|
25
28
|
threadId: row.threadId,
|
|
26
29
|
deletedAt: row.deletedAt,
|
|
@@ -40,73 +43,103 @@ export function createPostService(db) {
|
|
|
40
43
|
},
|
|
41
44
|
async list (filters = {}) {
|
|
42
45
|
const conditions = [];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (Array.isArray(filters.visibility)) {
|
|
46
|
-
conditions.push(inArray(posts.visibility, filters.visibility));
|
|
47
|
-
} else {
|
|
48
|
-
conditions.push(eq(posts.visibility, filters.visibility));
|
|
49
|
-
}
|
|
46
|
+
if (filters.status) {
|
|
47
|
+
conditions.push(eq(posts.status, filters.status));
|
|
50
48
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
conditions.push(eq(posts.type, filters.type));
|
|
49
|
+
if (filters.featured !== undefined) {
|
|
50
|
+
conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
if (filters.pinned !== undefined) {
|
|
53
|
+
conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
|
|
54
|
+
}
|
|
55
|
+
if (filters.format) {
|
|
56
|
+
conditions.push(eq(posts.format, filters.format));
|
|
57
|
+
}
|
|
58
|
+
if (filters.collectionId !== undefined) {
|
|
59
|
+
conditions.push(eq(posts.collectionId, filters.collectionId));
|
|
58
60
|
}
|
|
59
|
-
// Thread filter
|
|
60
61
|
if (filters.threadId) {
|
|
61
62
|
conditions.push(eq(posts.threadId, filters.threadId));
|
|
62
63
|
}
|
|
63
|
-
// Exclude replies (posts that are part of a thread but not the root)
|
|
64
64
|
if (filters.excludeReplies) {
|
|
65
65
|
conditions.push(isNull(posts.threadId));
|
|
66
66
|
}
|
|
67
|
-
// Exclude deleted unless specified
|
|
68
67
|
if (!filters.includeDeleted) {
|
|
69
68
|
conditions.push(isNull(posts.deletedAt));
|
|
70
69
|
}
|
|
71
|
-
// Cursor pagination
|
|
72
70
|
if (filters.cursor) {
|
|
73
71
|
conditions.push(sql`${posts.id} < ${filters.cursor}`);
|
|
74
72
|
}
|
|
75
|
-
|
|
73
|
+
let query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
|
|
74
|
+
if (filters.offset !== undefined) {
|
|
75
|
+
query = query.offset(filters.offset);
|
|
76
|
+
}
|
|
76
77
|
const rows = await query;
|
|
77
78
|
return rows.map(toPost);
|
|
78
79
|
},
|
|
80
|
+
async count (filters = {}) {
|
|
81
|
+
const conditions = [];
|
|
82
|
+
if (filters.status) {
|
|
83
|
+
conditions.push(eq(posts.status, filters.status));
|
|
84
|
+
}
|
|
85
|
+
if (filters.featured !== undefined) {
|
|
86
|
+
conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
|
|
87
|
+
}
|
|
88
|
+
if (filters.pinned !== undefined) {
|
|
89
|
+
conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
|
|
90
|
+
}
|
|
91
|
+
if (filters.format) {
|
|
92
|
+
conditions.push(eq(posts.format, filters.format));
|
|
93
|
+
}
|
|
94
|
+
if (filters.collectionId !== undefined) {
|
|
95
|
+
conditions.push(eq(posts.collectionId, filters.collectionId));
|
|
96
|
+
}
|
|
97
|
+
if (filters.threadId) {
|
|
98
|
+
conditions.push(eq(posts.threadId, filters.threadId));
|
|
99
|
+
}
|
|
100
|
+
if (filters.excludeReplies) {
|
|
101
|
+
conditions.push(isNull(posts.threadId));
|
|
102
|
+
}
|
|
103
|
+
if (!filters.includeDeleted) {
|
|
104
|
+
conditions.push(isNull(posts.deletedAt));
|
|
105
|
+
}
|
|
106
|
+
const result = await db.select({
|
|
107
|
+
count: sql`count(*)`.as("count")
|
|
108
|
+
}).from(posts).where(conditions.length > 0 ? and(...conditions) : undefined);
|
|
109
|
+
return result[0]?.count ?? 0;
|
|
110
|
+
},
|
|
79
111
|
async create (data) {
|
|
80
112
|
const timestamp = now();
|
|
81
|
-
|
|
82
|
-
const contentHtml = data.content ? renderMarkdown(data.content) : null;
|
|
83
|
-
// Extract domain from source URL
|
|
84
|
-
const sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
|
|
113
|
+
const bodyHtml = data.body ? renderMarkdown(data.body) : null;
|
|
85
114
|
// Handle thread relationship
|
|
86
115
|
let threadId = null;
|
|
87
|
-
let
|
|
116
|
+
let status = data.status ?? "published";
|
|
117
|
+
let featured = data.featured ?? false;
|
|
88
118
|
if (data.replyToId) {
|
|
89
119
|
const parent = await this.getById(data.replyToId);
|
|
90
120
|
if (parent) {
|
|
91
|
-
// thread_id = parent's thread_id or parent's id (if parent is root)
|
|
92
121
|
threadId = parent.threadId ?? parent.id;
|
|
93
|
-
// Inherit
|
|
122
|
+
// Inherit status and featured from root
|
|
94
123
|
const root = parent.threadId ? await this.getById(parent.threadId) : parent;
|
|
95
124
|
if (root) {
|
|
96
|
-
|
|
125
|
+
status = root.status;
|
|
126
|
+
featured = root.featured === 1;
|
|
97
127
|
}
|
|
98
128
|
}
|
|
99
129
|
}
|
|
100
130
|
const result = await db.insert(posts).values({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
format: data.format,
|
|
132
|
+
status,
|
|
133
|
+
featured: featured ? 1 : 0,
|
|
134
|
+
pinned: data.pinned ? 1 : 0,
|
|
104
135
|
path: data.path ?? null,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
title: data.title ?? null,
|
|
137
|
+
url: data.url ?? null,
|
|
138
|
+
body: data.body ?? null,
|
|
139
|
+
bodyHtml,
|
|
140
|
+
quoteText: data.quoteText ?? null,
|
|
141
|
+
rating: data.rating ?? null,
|
|
142
|
+
collectionId: data.collectionId ?? null,
|
|
110
143
|
replyToId: data.replyToId ?? null,
|
|
111
144
|
threadId,
|
|
112
145
|
publishedAt: data.publishedAt ?? timestamp,
|
|
@@ -123,26 +156,27 @@ export function createPostService(db) {
|
|
|
123
156
|
const updates = {
|
|
124
157
|
updatedAt: timestamp
|
|
125
158
|
};
|
|
126
|
-
if (data.
|
|
127
|
-
if (data.title !== undefined) updates.title = data.title;
|
|
159
|
+
if (data.format !== undefined) updates.format = data.format;
|
|
128
160
|
if (data.path !== undefined) updates.path = data.path;
|
|
161
|
+
if (data.title !== undefined) updates.title = data.title;
|
|
162
|
+
if (data.url !== undefined) updates.url = data.url;
|
|
163
|
+
if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
|
|
164
|
+
if (data.rating !== undefined) updates.rating = data.rating;
|
|
165
|
+
if (data.collectionId !== undefined) updates.collectionId = data.collectionId;
|
|
129
166
|
if (data.publishedAt !== undefined) updates.publishedAt = data.publishedAt;
|
|
130
|
-
if (data.
|
|
131
|
-
|
|
132
|
-
updates.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!existing.threadId) {
|
|
144
|
-
await this.updateThreadVisibility(id, data.visibility);
|
|
145
|
-
}
|
|
167
|
+
if (data.pinned !== undefined) updates.pinned = data.pinned ? 1 : 0;
|
|
168
|
+
if (data.body !== undefined) {
|
|
169
|
+
updates.body = data.body;
|
|
170
|
+
updates.bodyHtml = data.body ? renderMarkdown(data.body) : null;
|
|
171
|
+
}
|
|
172
|
+
// Handle status/featured change - cascade to thread if this is root
|
|
173
|
+
const statusChanged = data.status !== undefined && data.status !== existing.status;
|
|
174
|
+
const featuredChanged = data.featured !== undefined && (data.featured ? 1 : 0) !== existing.featured;
|
|
175
|
+
if (statusChanged) updates.status = data.status;
|
|
176
|
+
if (featuredChanged) updates.featured = data.featured ? 1 : 0;
|
|
177
|
+
// If this is a root post and status/featured changed, cascade to thread
|
|
178
|
+
if ((statusChanged || featuredChanged) && !existing.threadId) {
|
|
179
|
+
await this.updateThreadStatusAndFeatured(id, data.status ?? existing.status, data.featured !== undefined ? data.featured : existing.featured === 1);
|
|
146
180
|
}
|
|
147
181
|
const result = await db.update(posts).set(updates).where(eq(posts.id, id)).returning();
|
|
148
182
|
return result[0] ? toPost(result[0]) : null;
|
|
@@ -158,7 +192,6 @@ export function createPostService(db) {
|
|
|
158
192
|
updatedAt: timestamp
|
|
159
193
|
}).where(or(eq(posts.id, id), eq(posts.threadId, id)));
|
|
160
194
|
} else {
|
|
161
|
-
// Just delete this single post
|
|
162
195
|
await db.update(posts).set({
|
|
163
196
|
deletedAt: timestamp,
|
|
164
197
|
updatedAt: timestamp
|
|
@@ -170,10 +203,11 @@ export function createPostService(db) {
|
|
|
170
203
|
const rows = await db.select().from(posts).where(and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt))).orderBy(posts.createdAt);
|
|
171
204
|
return rows.map(toPost);
|
|
172
205
|
},
|
|
173
|
-
async
|
|
206
|
+
async updateThreadStatusAndFeatured (rootId, status, featured) {
|
|
174
207
|
const timestamp = now();
|
|
175
208
|
await db.update(posts).set({
|
|
176
|
-
|
|
209
|
+
status,
|
|
210
|
+
featured: featured ? 1 : 0,
|
|
177
211
|
updatedAt: timestamp
|
|
178
212
|
}).where(eq(posts.threadId, rootId));
|
|
179
213
|
},
|
|
@@ -194,7 +228,6 @@ export function createPostService(db) {
|
|
|
194
228
|
async getThreadPreviews (rootIds, previewCount = 3) {
|
|
195
229
|
if (rootIds.length === 0) return new Map();
|
|
196
230
|
const rows = await db.select().from(posts).where(and(inArray(posts.threadId, rootIds), isNull(posts.deletedAt))).orderBy(posts.threadId, posts.createdAt);
|
|
197
|
-
// Partition by threadId, take first previewCount per thread
|
|
198
231
|
const result = new Map();
|
|
199
232
|
for (const row of rows){
|
|
200
233
|
const post = toPost(row);
|
package/dist/services/search.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Search Service
|
|
2
|
+
* Search Service (v2)
|
|
3
3
|
*
|
|
4
4
|
* Full-text search using FTS5
|
|
5
5
|
*/ export function createSearchService(d1) {
|
|
@@ -7,19 +7,21 @@
|
|
|
7
7
|
async search (query, options = {}) {
|
|
8
8
|
const limit = options.limit ?? 20;
|
|
9
9
|
const offset = options.offset ?? 0;
|
|
10
|
-
const
|
|
11
|
-
"
|
|
12
|
-
"quiet"
|
|
10
|
+
const status = options.status ?? [
|
|
11
|
+
"published"
|
|
13
12
|
];
|
|
14
13
|
// Escape and prepare the query for FTS5
|
|
15
|
-
// FTS5 uses * for prefix matching
|
|
16
14
|
const ftsQuery = query.trim().split(/\s+/).filter((term)=>term.length > 0).map((term)=>`"${term.replace(/"/g, '""')}"*`).join(" ");
|
|
17
15
|
if (!ftsQuery) {
|
|
18
16
|
return [];
|
|
19
17
|
}
|
|
20
|
-
// Build
|
|
21
|
-
const
|
|
22
|
-
//
|
|
18
|
+
// Build status placeholders
|
|
19
|
+
const statusPlaceholders = status.map(()=>"?").join(", ");
|
|
20
|
+
// Build format filter
|
|
21
|
+
const formatFilter = options.format ? "AND posts.format = ?" : "";
|
|
22
|
+
const formatParams = options.format ? [
|
|
23
|
+
options.format
|
|
24
|
+
] : [];
|
|
23
25
|
const stmt = d1.prepare(`
|
|
24
26
|
SELECT
|
|
25
27
|
posts.*,
|
|
@@ -29,23 +31,27 @@
|
|
|
29
31
|
JOIN posts ON posts.id = posts_fts.rowid
|
|
30
32
|
WHERE posts_fts MATCH ?
|
|
31
33
|
AND posts.deleted_at IS NULL
|
|
32
|
-
AND posts.
|
|
34
|
+
AND posts.status IN (${statusPlaceholders})
|
|
35
|
+
${formatFilter}
|
|
33
36
|
ORDER BY posts_fts.rank
|
|
34
37
|
LIMIT ? OFFSET ?
|
|
35
38
|
`);
|
|
36
|
-
const { results } = await stmt.bind(ftsQuery, ...
|
|
39
|
+
const { results } = await stmt.bind(ftsQuery, ...status, ...formatParams, limit, offset).all();
|
|
37
40
|
return (results || []).map((row)=>({
|
|
38
41
|
post: {
|
|
39
42
|
id: row.id,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
format: row.format,
|
|
44
|
+
status: row.status,
|
|
45
|
+
featured: row.featured,
|
|
46
|
+
pinned: row.pinned,
|
|
43
47
|
path: row.path,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
title: row.title,
|
|
49
|
+
url: row.url,
|
|
50
|
+
body: row.body,
|
|
51
|
+
bodyHtml: row.body_html,
|
|
52
|
+
quoteText: row.quote_text,
|
|
53
|
+
rating: row.rating,
|
|
54
|
+
collectionId: row.collection_id,
|
|
49
55
|
replyToId: row.reply_to_id,
|
|
50
56
|
threadId: row.thread_id,
|
|
51
57
|
deletedAt: row.deleted_at,
|
package/dist/types.js
CHANGED
|
@@ -1,53 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Jant Type Definitions
|
|
2
|
+
* Jant Type Definitions (v2)
|
|
3
3
|
*/ // =============================================================================
|
|
4
4
|
// Content Types
|
|
5
5
|
// =============================================================================
|
|
6
|
-
export const
|
|
6
|
+
export const FORMATS = [
|
|
7
7
|
"note",
|
|
8
|
-
"article",
|
|
9
8
|
"link",
|
|
10
|
-
"quote"
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
"quote"
|
|
10
|
+
];
|
|
11
|
+
export const STATUSES = [
|
|
12
|
+
"draft",
|
|
13
|
+
"published"
|
|
14
|
+
];
|
|
15
|
+
export const SORT_ORDERS = [
|
|
16
|
+
"newest",
|
|
17
|
+
"oldest",
|
|
18
|
+
"rating_desc",
|
|
19
|
+
"rating_asc"
|
|
20
|
+
];
|
|
21
|
+
export const NAV_ITEM_TYPES = [
|
|
22
|
+
"page",
|
|
23
|
+
"link"
|
|
13
24
|
];
|
|
14
25
|
export const MAX_MEDIA_ATTACHMENTS = 20;
|
|
15
|
-
|
|
16
|
-
* Media attachment rules per post type.
|
|
17
|
-
* Each entry is [min, max] or null (no media allowed).
|
|
18
|
-
*/ export const POST_TYPE_MEDIA_RULES = {
|
|
19
|
-
note: [
|
|
20
|
-
0,
|
|
21
|
-
20
|
|
22
|
-
],
|
|
23
|
-
article: [
|
|
24
|
-
0,
|
|
25
|
-
20
|
|
26
|
-
],
|
|
27
|
-
image: [
|
|
28
|
-
1,
|
|
29
|
-
20
|
|
30
|
-
],
|
|
31
|
-
link: [
|
|
32
|
-
0,
|
|
33
|
-
1
|
|
34
|
-
],
|
|
35
|
-
quote: [
|
|
36
|
-
0,
|
|
37
|
-
20
|
|
38
|
-
],
|
|
39
|
-
page: null
|
|
40
|
-
};
|
|
26
|
+
export const MAX_PINNED_POSTS = 3;
|
|
41
27
|
export const STORAGE_DRIVERS = [
|
|
42
28
|
"r2",
|
|
43
29
|
"s3"
|
|
44
30
|
];
|
|
45
|
-
export const VISIBILITY_LEVELS = [
|
|
46
|
-
"featured",
|
|
47
|
-
"quiet",
|
|
48
|
-
"unlisted",
|
|
49
|
-
"draft"
|
|
50
|
-
];
|
|
51
31
|
// =============================================================================
|
|
52
32
|
// Configuration System
|
|
53
33
|
// =============================================================================
|
|
@@ -58,8 +38,8 @@ export const VISIBILITY_LEVELS = [
|
|
|
58
38
|
* Add new fields here, and they'll automatically work everywhere.
|
|
59
39
|
*
|
|
60
40
|
* Priority logic:
|
|
61
|
-
* - envOnly: false
|
|
62
|
-
* - envOnly: true
|
|
41
|
+
* - envOnly: false -> User-configurable (DB > ENV > Default)
|
|
42
|
+
* - envOnly: true -> Environment-only (ENV > Default)
|
|
63
43
|
*/ export const CONFIG_FIELDS = {
|
|
64
44
|
// User-configurable (can be modified in dashboard)
|
|
65
45
|
SITE_NAME: {
|
|
@@ -99,6 +79,10 @@ export const VISIBILITY_LEVELS = [
|
|
|
99
79
|
defaultValue: "",
|
|
100
80
|
envOnly: true
|
|
101
81
|
},
|
|
82
|
+
PAGE_SIZE: {
|
|
83
|
+
defaultValue: "20",
|
|
84
|
+
envOnly: true
|
|
85
|
+
},
|
|
102
86
|
STORAGE_DRIVER: {
|
|
103
87
|
defaultValue: "r2",
|
|
104
88
|
envOnly: true
|