@jant/core 0.3.22 → 0.3.24
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 +23 -5
- 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 -6
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +62 -73
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- 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 +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -16
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
- package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
- package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
- package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
- package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +27 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +30 -15
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +217 -67
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +81 -83
- package/src/preset.css +45 -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/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- 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__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- 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 +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/theme/components/index.ts +0 -13
- package/src/theme/index.ts +10 -16
- package/src/theme/layouts/index.ts +0 -1
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/threads/index.ts +100 -0
- package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
- package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
- package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
- package/src/themes/threads/pages/SinglePage.tsx +23 -0
- package/src/themes/threads/style.css +336 -0
- package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/theme/components/timeline/ArticleCard.js +0 -46
- package/dist/theme/components/timeline/ImageCard.js +0 -83
- package/dist/theme/components/timeline/NoteCard.js +0 -34
- package/dist/theme/components/timeline/QuoteCard.js +0 -48
- package/dist/theme/components/timeline/TimelineFeed.js +0 -46
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -131
- package/dist/theme/pages/CollectionPage.js +0 -63
- package/dist/theme/pages/index.js +0 -11
- package/src/routes/api/timeline.tsx +0 -159
- package/src/theme/components/timeline/ArticleCard.tsx +0 -45
- package/src/theme/components/timeline/ImageCard.tsx +0 -70
- package/src/theme/components/timeline/NoteCard.tsx +0 -34
- package/src/theme/components/timeline/QuoteCard.tsx +0 -48
- package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -132
- package/src/theme/pages/CollectionPage.tsx +0 -60
- package/src/theme/pages/SinglePage.tsx +0 -24
- package/src/theme/pages/index.ts +0 -13
|
@@ -16,62 +16,59 @@ describe("PostService", () => {
|
|
|
16
16
|
describe("create", () => {
|
|
17
17
|
it("creates a note post with required fields", async () => {
|
|
18
18
|
const post = await postService.create({
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
format: "note",
|
|
20
|
+
body: "Hello world",
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
expect(post.id).toBe(1);
|
|
24
|
-
expect(post.
|
|
25
|
-
expect(post.
|
|
26
|
-
expect(post.
|
|
27
|
-
expect(post.
|
|
24
|
+
expect(post.format).toBe("note");
|
|
25
|
+
expect(post.body).toBe("Hello world");
|
|
26
|
+
expect(post.status).toBe("published"); // default
|
|
27
|
+
expect(post.featured).toBe(0);
|
|
28
|
+
expect(post.pinned).toBe(0);
|
|
29
|
+
expect(post.bodyHtml).toContain("<p>Hello world</p>");
|
|
28
30
|
expect(post.deletedAt).toBeNull();
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
it("creates a post with all fields", async () => {
|
|
32
34
|
const post = await postService.create({
|
|
33
|
-
|
|
34
|
-
title: "My
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
expect(post.
|
|
46
|
-
expect(post.
|
|
47
|
-
expect(post.
|
|
48
|
-
expect(post.
|
|
49
|
-
expect(post.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
format: "link",
|
|
36
|
+
title: "My Link",
|
|
37
|
+
body: "# Introduction\n\nSome content.",
|
|
38
|
+
status: "published",
|
|
39
|
+
featured: true,
|
|
40
|
+
pinned: true,
|
|
41
|
+
slug: "my-link",
|
|
42
|
+
url: "https://example.com/source",
|
|
43
|
+
quoteText: "A notable quote",
|
|
44
|
+
rating: 5,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(post.format).toBe("link");
|
|
48
|
+
expect(post.title).toBe("My Link");
|
|
49
|
+
expect(post.status).toBe("published");
|
|
50
|
+
expect(post.featured).toBe(1);
|
|
51
|
+
expect(post.pinned).toBe(1);
|
|
52
|
+
expect(post.slug).toBe("my-link");
|
|
53
|
+
expect(post.url).toBe("https://example.com/source");
|
|
54
|
+
expect(post.quoteText).toBe("A notable quote");
|
|
55
|
+
expect(post.rating).toBe(5);
|
|
56
|
+
expect(post.bodyHtml).toContain("<h1>");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders markdown body to HTML", async () => {
|
|
53
60
|
const post = await postService.create({
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
format: "note",
|
|
62
|
+
body: "This is **bold** text",
|
|
56
63
|
});
|
|
57
64
|
|
|
58
|
-
expect(post.
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("extracts domain from source URL", async () => {
|
|
62
|
-
const post = await postService.create({
|
|
63
|
-
type: "link",
|
|
64
|
-
content: "Check this out",
|
|
65
|
-
sourceUrl: "https://blog.example.org/article",
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
expect(post.sourceDomain).toBe("blog.example.org");
|
|
65
|
+
expect(post.bodyHtml).toContain("<strong>bold</strong>");
|
|
69
66
|
});
|
|
70
67
|
|
|
71
68
|
it("sets publishedAt and timestamps", async () => {
|
|
72
69
|
const post = await postService.create({
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
format: "note",
|
|
71
|
+
body: "test",
|
|
75
72
|
});
|
|
76
73
|
|
|
77
74
|
expect(post.publishedAt).toBeGreaterThan(0);
|
|
@@ -82,8 +79,8 @@ describe("PostService", () => {
|
|
|
82
79
|
it("allows custom publishedAt", async () => {
|
|
83
80
|
const customTime = 1706745600;
|
|
84
81
|
const post = await postService.create({
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
format: "note",
|
|
83
|
+
body: "test",
|
|
87
84
|
publishedAt: customTime,
|
|
88
85
|
});
|
|
89
86
|
|
|
@@ -92,29 +89,52 @@ describe("PostService", () => {
|
|
|
92
89
|
|
|
93
90
|
it("creates incrementing IDs", async () => {
|
|
94
91
|
const post1 = await postService.create({
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
format: "note",
|
|
93
|
+
body: "first",
|
|
97
94
|
});
|
|
98
95
|
const post2 = await postService.create({
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
format: "note",
|
|
97
|
+
body: "second",
|
|
101
98
|
});
|
|
102
99
|
|
|
103
100
|
expect(post2.id).toBeGreaterThan(post1.id);
|
|
104
101
|
});
|
|
102
|
+
|
|
103
|
+
it("creates a quote post", async () => {
|
|
104
|
+
const post = await postService.create({
|
|
105
|
+
format: "quote",
|
|
106
|
+
quoteText: "To be or not to be",
|
|
107
|
+
body: "Shakespeare's famous line",
|
|
108
|
+
url: "https://example.com/hamlet",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(post.format).toBe("quote");
|
|
112
|
+
expect(post.quoteText).toBe("To be or not to be");
|
|
113
|
+
expect(post.url).toBe("https://example.com/hamlet");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("creates a draft post", async () => {
|
|
117
|
+
const post = await postService.create({
|
|
118
|
+
format: "note",
|
|
119
|
+
body: "draft content",
|
|
120
|
+
status: "draft",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(post.status).toBe("draft");
|
|
124
|
+
});
|
|
105
125
|
});
|
|
106
126
|
|
|
107
127
|
describe("getById", () => {
|
|
108
128
|
it("returns a post by ID", async () => {
|
|
109
129
|
const created = await postService.create({
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
format: "note",
|
|
131
|
+
body: "test",
|
|
112
132
|
});
|
|
113
133
|
|
|
114
134
|
const found = await postService.getById(created.id);
|
|
115
135
|
expect(found).not.toBeNull();
|
|
116
136
|
expect(found?.id).toBe(created.id);
|
|
117
|
-
expect(found?.
|
|
137
|
+
expect(found?.body).toBe("test");
|
|
118
138
|
});
|
|
119
139
|
|
|
120
140
|
it("returns null for non-existent ID", async () => {
|
|
@@ -124,8 +144,8 @@ describe("PostService", () => {
|
|
|
124
144
|
|
|
125
145
|
it("excludes soft-deleted posts", async () => {
|
|
126
146
|
const post = await postService.create({
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
format: "note",
|
|
148
|
+
body: "test",
|
|
129
149
|
});
|
|
130
150
|
await postService.delete(post.id);
|
|
131
151
|
|
|
@@ -134,33 +154,33 @@ describe("PostService", () => {
|
|
|
134
154
|
});
|
|
135
155
|
});
|
|
136
156
|
|
|
137
|
-
describe("
|
|
138
|
-
it("returns a post by
|
|
157
|
+
describe("getBySlug", () => {
|
|
158
|
+
it("returns a post by slug", async () => {
|
|
139
159
|
await postService.create({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
format: "note",
|
|
161
|
+
body: "About page",
|
|
162
|
+
slug: "about",
|
|
143
163
|
});
|
|
144
164
|
|
|
145
|
-
const found = await postService.
|
|
165
|
+
const found = await postService.getBySlug("about");
|
|
146
166
|
expect(found).not.toBeNull();
|
|
147
|
-
expect(found?.
|
|
167
|
+
expect(found?.slug).toBe("about");
|
|
148
168
|
});
|
|
149
169
|
|
|
150
|
-
it("returns null for non-existent
|
|
151
|
-
const found = await postService.
|
|
170
|
+
it("returns null for non-existent slug", async () => {
|
|
171
|
+
const found = await postService.getBySlug("nonexistent");
|
|
152
172
|
expect(found).toBeNull();
|
|
153
173
|
});
|
|
154
174
|
|
|
155
175
|
it("excludes soft-deleted posts", async () => {
|
|
156
176
|
const post = await postService.create({
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
format: "note",
|
|
178
|
+
body: "test",
|
|
179
|
+
slug: "test-page",
|
|
160
180
|
});
|
|
161
181
|
await postService.delete(post.id);
|
|
162
182
|
|
|
163
|
-
const found = await postService.
|
|
183
|
+
const found = await postService.getBySlug("test-page");
|
|
164
184
|
expect(found).toBeNull();
|
|
165
185
|
});
|
|
166
186
|
});
|
|
@@ -172,9 +192,9 @@ describe("PostService", () => {
|
|
|
172
192
|
});
|
|
173
193
|
|
|
174
194
|
it("returns all non-deleted posts", async () => {
|
|
175
|
-
await postService.create({
|
|
176
|
-
await postService.create({
|
|
177
|
-
await postService.create({
|
|
195
|
+
await postService.create({ format: "note", body: "first" });
|
|
196
|
+
await postService.create({ format: "note", body: "second" });
|
|
197
|
+
await postService.create({ format: "note", body: "third" });
|
|
178
198
|
|
|
179
199
|
const posts = await postService.list();
|
|
180
200
|
expect(posts).toHaveLength(3);
|
|
@@ -182,91 +202,113 @@ describe("PostService", () => {
|
|
|
182
202
|
|
|
183
203
|
it("orders by publishedAt descending", async () => {
|
|
184
204
|
await postService.create({
|
|
185
|
-
|
|
186
|
-
|
|
205
|
+
format: "note",
|
|
206
|
+
body: "old",
|
|
187
207
|
publishedAt: 1000,
|
|
188
208
|
});
|
|
189
209
|
await postService.create({
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
format: "note",
|
|
211
|
+
body: "new",
|
|
192
212
|
publishedAt: 2000,
|
|
193
213
|
});
|
|
194
214
|
|
|
195
215
|
const posts = await postService.list();
|
|
196
|
-
expect(posts[0]?.
|
|
197
|
-
expect(posts[1]?.
|
|
216
|
+
expect(posts[0]?.body).toBe("new");
|
|
217
|
+
expect(posts[1]?.body).toBe("old");
|
|
198
218
|
});
|
|
199
219
|
|
|
200
|
-
it("filters by
|
|
201
|
-
await postService.create({
|
|
220
|
+
it("filters by format", async () => {
|
|
221
|
+
await postService.create({ format: "note", body: "a note" });
|
|
202
222
|
await postService.create({
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
title: "
|
|
223
|
+
format: "link",
|
|
224
|
+
body: "a link",
|
|
225
|
+
title: "Link",
|
|
226
|
+
url: "https://example.com",
|
|
206
227
|
});
|
|
207
228
|
|
|
208
|
-
const notes = await postService.list({
|
|
229
|
+
const notes = await postService.list({ format: "note" });
|
|
209
230
|
expect(notes).toHaveLength(1);
|
|
210
|
-
expect(notes[0]?.
|
|
231
|
+
expect(notes[0]?.format).toBe("note");
|
|
211
232
|
});
|
|
212
233
|
|
|
213
|
-
it("filters by
|
|
234
|
+
it("filters by status", async () => {
|
|
214
235
|
await postService.create({
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
format: "note",
|
|
237
|
+
body: "published post",
|
|
238
|
+
status: "published",
|
|
218
239
|
});
|
|
219
240
|
await postService.create({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
format: "note",
|
|
242
|
+
body: "draft post",
|
|
243
|
+
status: "draft",
|
|
223
244
|
});
|
|
224
245
|
|
|
225
|
-
const
|
|
226
|
-
expect(
|
|
227
|
-
expect(
|
|
246
|
+
const published = await postService.list({ status: "published" });
|
|
247
|
+
expect(published).toHaveLength(1);
|
|
248
|
+
expect(published[0]?.status).toBe("published");
|
|
228
249
|
});
|
|
229
250
|
|
|
230
|
-
it("filters by
|
|
251
|
+
it("filters by featured", async () => {
|
|
252
|
+
await postService.create({
|
|
253
|
+
format: "note",
|
|
254
|
+
body: "featured post",
|
|
255
|
+
featured: true,
|
|
256
|
+
});
|
|
231
257
|
await postService.create({
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
visibility: "featured",
|
|
258
|
+
format: "note",
|
|
259
|
+
body: "normal post",
|
|
235
260
|
});
|
|
261
|
+
|
|
262
|
+
const featured = await postService.list({ featured: true });
|
|
263
|
+
expect(featured).toHaveLength(1);
|
|
264
|
+
expect(featured[0]?.featured).toBe(1);
|
|
265
|
+
expect(featured[0]?.body).toBe("featured post");
|
|
266
|
+
|
|
267
|
+
const notFeatured = await postService.list({ featured: false });
|
|
268
|
+
expect(notFeatured).toHaveLength(1);
|
|
269
|
+
expect(notFeatured[0]?.featured).toBe(0);
|
|
270
|
+
expect(notFeatured[0]?.body).toBe("normal post");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("filters by pinned", async () => {
|
|
236
274
|
await postService.create({
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
275
|
+
format: "note",
|
|
276
|
+
body: "pinned post",
|
|
277
|
+
pinned: true,
|
|
240
278
|
});
|
|
241
279
|
await postService.create({
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
visibility: "draft",
|
|
280
|
+
format: "note",
|
|
281
|
+
body: "normal post",
|
|
245
282
|
});
|
|
246
283
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
expect(
|
|
284
|
+
const pinned = await postService.list({ pinned: true });
|
|
285
|
+
expect(pinned).toHaveLength(1);
|
|
286
|
+
expect(pinned[0]?.pinned).toBe(1);
|
|
287
|
+
expect(pinned[0]?.body).toBe("pinned post");
|
|
288
|
+
|
|
289
|
+
const notPinned = await postService.list({ pinned: false });
|
|
290
|
+
expect(notPinned).toHaveLength(1);
|
|
291
|
+
expect(notPinned[0]?.pinned).toBe(0);
|
|
292
|
+
expect(notPinned[0]?.body).toBe("normal post");
|
|
251
293
|
});
|
|
252
294
|
|
|
253
295
|
it("excludes deleted posts by default", async () => {
|
|
254
296
|
const post = await postService.create({
|
|
255
|
-
|
|
256
|
-
|
|
297
|
+
format: "note",
|
|
298
|
+
body: "test",
|
|
257
299
|
});
|
|
258
|
-
await postService.create({
|
|
300
|
+
await postService.create({ format: "note", body: "kept" });
|
|
259
301
|
await postService.delete(post.id);
|
|
260
302
|
|
|
261
303
|
const posts = await postService.list();
|
|
262
304
|
expect(posts).toHaveLength(1);
|
|
263
|
-
expect(posts[0]?.
|
|
305
|
+
expect(posts[0]?.body).toBe("kept");
|
|
264
306
|
});
|
|
265
307
|
|
|
266
308
|
it("includes deleted posts when requested", async () => {
|
|
267
309
|
const post = await postService.create({
|
|
268
|
-
|
|
269
|
-
|
|
310
|
+
format: "note",
|
|
311
|
+
body: "test",
|
|
270
312
|
});
|
|
271
313
|
await postService.delete(post.id);
|
|
272
314
|
|
|
@@ -276,7 +318,7 @@ describe("PostService", () => {
|
|
|
276
318
|
|
|
277
319
|
it("supports limit", async () => {
|
|
278
320
|
for (let i = 0; i < 5; i++) {
|
|
279
|
-
await postService.create({
|
|
321
|
+
await postService.create({ format: "note", body: `post ${i}` });
|
|
280
322
|
}
|
|
281
323
|
|
|
282
324
|
const posts = await postService.list({ limit: 2 });
|
|
@@ -288,8 +330,8 @@ describe("PostService", () => {
|
|
|
288
330
|
for (let i = 0; i < 5; i++) {
|
|
289
331
|
created.push(
|
|
290
332
|
await postService.create({
|
|
291
|
-
|
|
292
|
-
|
|
333
|
+
format: "note",
|
|
334
|
+
body: `post ${i}`,
|
|
293
335
|
publishedAt: 1000 + i,
|
|
294
336
|
}),
|
|
295
337
|
);
|
|
@@ -303,42 +345,43 @@ describe("PostService", () => {
|
|
|
303
345
|
|
|
304
346
|
it("excludes replies when requested", async () => {
|
|
305
347
|
const root = await postService.create({
|
|
306
|
-
|
|
307
|
-
|
|
348
|
+
format: "note",
|
|
349
|
+
body: "root post",
|
|
308
350
|
});
|
|
309
351
|
await postService.create({
|
|
310
|
-
|
|
311
|
-
|
|
352
|
+
format: "note",
|
|
353
|
+
body: "reply",
|
|
312
354
|
replyToId: root.id,
|
|
313
355
|
});
|
|
314
356
|
|
|
315
357
|
const posts = await postService.list({ excludeReplies: true });
|
|
316
358
|
expect(posts).toHaveLength(1);
|
|
317
|
-
expect(posts[0]?.
|
|
359
|
+
expect(posts[0]?.body).toBe("root post");
|
|
318
360
|
});
|
|
319
361
|
});
|
|
320
362
|
|
|
321
363
|
describe("update", () => {
|
|
322
|
-
it("updates post
|
|
364
|
+
it("updates post body", async () => {
|
|
323
365
|
const post = await postService.create({
|
|
324
|
-
|
|
325
|
-
|
|
366
|
+
format: "note",
|
|
367
|
+
body: "original",
|
|
326
368
|
});
|
|
327
369
|
|
|
328
370
|
const updated = await postService.update(post.id, {
|
|
329
|
-
|
|
371
|
+
body: "updated content",
|
|
330
372
|
});
|
|
331
373
|
|
|
332
374
|
expect(updated).not.toBeNull();
|
|
333
|
-
expect(updated?.
|
|
334
|
-
expect(updated?.
|
|
375
|
+
expect(updated?.body).toBe("updated content");
|
|
376
|
+
expect(updated?.bodyHtml).toContain("updated content");
|
|
335
377
|
});
|
|
336
378
|
|
|
337
379
|
it("updates post title", async () => {
|
|
338
380
|
const post = await postService.create({
|
|
339
|
-
|
|
340
|
-
|
|
381
|
+
format: "link",
|
|
382
|
+
body: "body",
|
|
341
383
|
title: "Original Title",
|
|
384
|
+
url: "https://example.com",
|
|
342
385
|
});
|
|
343
386
|
|
|
344
387
|
const updated = await postService.update(post.id, {
|
|
@@ -348,44 +391,43 @@ describe("PostService", () => {
|
|
|
348
391
|
expect(updated?.title).toBe("New Title");
|
|
349
392
|
});
|
|
350
393
|
|
|
351
|
-
it("updates
|
|
394
|
+
it("updates post url", async () => {
|
|
352
395
|
const post = await postService.create({
|
|
353
|
-
|
|
354
|
-
|
|
396
|
+
format: "link",
|
|
397
|
+
body: "link post",
|
|
398
|
+
url: "https://old.com",
|
|
355
399
|
});
|
|
356
400
|
|
|
357
401
|
const updated = await postService.update(post.id, {
|
|
358
|
-
|
|
402
|
+
url: "https://new-source.com/path",
|
|
359
403
|
});
|
|
360
404
|
|
|
361
|
-
expect(updated?.
|
|
362
|
-
expect(updated?.sourceDomain).toBe("new-source.com");
|
|
405
|
+
expect(updated?.url).toBe("https://new-source.com/path");
|
|
363
406
|
});
|
|
364
407
|
|
|
365
|
-
it("clears
|
|
408
|
+
it("clears url when set to null", async () => {
|
|
366
409
|
const post = await postService.create({
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
410
|
+
format: "link",
|
|
411
|
+
body: "test",
|
|
412
|
+
url: "https://example.com",
|
|
370
413
|
});
|
|
371
414
|
|
|
372
415
|
const updated = await postService.update(post.id, {
|
|
373
|
-
|
|
416
|
+
url: null,
|
|
374
417
|
});
|
|
375
418
|
|
|
376
|
-
expect(updated?.
|
|
377
|
-
expect(updated?.sourceDomain).toBeNull();
|
|
419
|
+
expect(updated?.url).toBeNull();
|
|
378
420
|
});
|
|
379
421
|
|
|
380
422
|
it("returns null for non-existent post", async () => {
|
|
381
|
-
const result = await postService.update(9999, {
|
|
423
|
+
const result = await postService.update(9999, { body: "test" });
|
|
382
424
|
expect(result).toBeNull();
|
|
383
425
|
});
|
|
384
426
|
|
|
385
427
|
it("updates updatedAt timestamp", async () => {
|
|
386
428
|
const post = await postService.create({
|
|
387
|
-
|
|
388
|
-
|
|
429
|
+
format: "note",
|
|
430
|
+
body: "test",
|
|
389
431
|
});
|
|
390
432
|
const originalUpdatedAt = post.updatedAt;
|
|
391
433
|
|
|
@@ -393,18 +435,78 @@ describe("PostService", () => {
|
|
|
393
435
|
await new Promise((r) => setTimeout(r, 1100));
|
|
394
436
|
|
|
395
437
|
const updated = await postService.update(post.id, {
|
|
396
|
-
|
|
438
|
+
body: "modified",
|
|
397
439
|
});
|
|
398
440
|
|
|
399
441
|
expect(updated?.updatedAt).toBeGreaterThanOrEqual(originalUpdatedAt);
|
|
400
442
|
});
|
|
443
|
+
|
|
444
|
+
it("updates featured flag", async () => {
|
|
445
|
+
const post = await postService.create({
|
|
446
|
+
format: "note",
|
|
447
|
+
body: "test",
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(post.featured).toBe(0);
|
|
451
|
+
|
|
452
|
+
const updated = await postService.update(post.id, {
|
|
453
|
+
featured: true,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(updated?.featured).toBe(1);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("updates pinned flag", async () => {
|
|
460
|
+
const post = await postService.create({
|
|
461
|
+
format: "note",
|
|
462
|
+
body: "test",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
expect(post.pinned).toBe(0);
|
|
466
|
+
|
|
467
|
+
const updated = await postService.update(post.id, {
|
|
468
|
+
pinned: true,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(updated?.pinned).toBe(1);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("updates slug", async () => {
|
|
475
|
+
const post = await postService.create({
|
|
476
|
+
format: "note",
|
|
477
|
+
body: "test",
|
|
478
|
+
slug: "old-slug",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const updated = await postService.update(post.id, {
|
|
482
|
+
slug: "new-slug",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(updated?.slug).toBe("new-slug");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("updates quoteText and rating", async () => {
|
|
489
|
+
const post = await postService.create({
|
|
490
|
+
format: "quote",
|
|
491
|
+
quoteText: "Original quote",
|
|
492
|
+
rating: 3,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const updated = await postService.update(post.id, {
|
|
496
|
+
quoteText: "Updated quote",
|
|
497
|
+
rating: 5,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(updated?.quoteText).toBe("Updated quote");
|
|
501
|
+
expect(updated?.rating).toBe(5);
|
|
502
|
+
});
|
|
401
503
|
});
|
|
402
504
|
|
|
403
505
|
describe("delete (soft delete)", () => {
|
|
404
506
|
it("soft-deletes a post", async () => {
|
|
405
507
|
const post = await postService.create({
|
|
406
|
-
|
|
407
|
-
|
|
508
|
+
format: "note",
|
|
509
|
+
body: "test",
|
|
408
510
|
});
|
|
409
511
|
|
|
410
512
|
const result = await postService.delete(post.id);
|
|
@@ -422,12 +524,12 @@ describe("PostService", () => {
|
|
|
422
524
|
|
|
423
525
|
it("cascade deletes thread when deleting root post", async () => {
|
|
424
526
|
const root = await postService.create({
|
|
425
|
-
|
|
426
|
-
|
|
527
|
+
format: "note",
|
|
528
|
+
body: "root",
|
|
427
529
|
});
|
|
428
530
|
const reply = await postService.create({
|
|
429
|
-
|
|
430
|
-
|
|
531
|
+
format: "note",
|
|
532
|
+
body: "reply",
|
|
431
533
|
replyToId: root.id,
|
|
432
534
|
});
|
|
433
535
|
|
|
@@ -440,17 +542,17 @@ describe("PostService", () => {
|
|
|
440
542
|
|
|
441
543
|
it("only deletes single post when deleting a reply", async () => {
|
|
442
544
|
const root = await postService.create({
|
|
443
|
-
|
|
444
|
-
|
|
545
|
+
format: "note",
|
|
546
|
+
body: "root",
|
|
445
547
|
});
|
|
446
548
|
const reply1 = await postService.create({
|
|
447
|
-
|
|
448
|
-
|
|
549
|
+
format: "note",
|
|
550
|
+
body: "reply1",
|
|
449
551
|
replyToId: root.id,
|
|
450
552
|
});
|
|
451
553
|
await postService.create({
|
|
452
|
-
|
|
453
|
-
|
|
554
|
+
format: "note",
|
|
555
|
+
body: "reply2",
|
|
454
556
|
replyToId: root.id,
|
|
455
557
|
});
|
|
456
558
|
|
|
@@ -466,12 +568,12 @@ describe("PostService", () => {
|
|
|
466
568
|
describe("threads", () => {
|
|
467
569
|
it("sets threadId on reply to a root post", async () => {
|
|
468
570
|
const root = await postService.create({
|
|
469
|
-
|
|
470
|
-
|
|
571
|
+
format: "note",
|
|
572
|
+
body: "root",
|
|
471
573
|
});
|
|
472
574
|
const reply = await postService.create({
|
|
473
|
-
|
|
474
|
-
|
|
575
|
+
format: "note",
|
|
576
|
+
body: "reply",
|
|
475
577
|
replyToId: root.id,
|
|
476
578
|
});
|
|
477
579
|
|
|
@@ -481,17 +583,17 @@ describe("PostService", () => {
|
|
|
481
583
|
|
|
482
584
|
it("inherits threadId from parent in nested replies", async () => {
|
|
483
585
|
const root = await postService.create({
|
|
484
|
-
|
|
485
|
-
|
|
586
|
+
format: "note",
|
|
587
|
+
body: "root",
|
|
486
588
|
});
|
|
487
589
|
const reply1 = await postService.create({
|
|
488
|
-
|
|
489
|
-
|
|
590
|
+
format: "note",
|
|
591
|
+
body: "reply1",
|
|
490
592
|
replyToId: root.id,
|
|
491
593
|
});
|
|
492
594
|
const reply2 = await postService.create({
|
|
493
|
-
|
|
494
|
-
|
|
595
|
+
format: "note",
|
|
596
|
+
body: "reply2",
|
|
495
597
|
replyToId: reply1.id,
|
|
496
598
|
});
|
|
497
599
|
|
|
@@ -500,51 +602,66 @@ describe("PostService", () => {
|
|
|
500
602
|
expect(reply2.threadId).toBe(root.id);
|
|
501
603
|
});
|
|
502
604
|
|
|
503
|
-
it("inherits
|
|
605
|
+
it("inherits status from root post", async () => {
|
|
504
606
|
const root = await postService.create({
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
607
|
+
format: "note",
|
|
608
|
+
body: "root",
|
|
609
|
+
status: "draft",
|
|
508
610
|
});
|
|
509
611
|
const reply = await postService.create({
|
|
510
|
-
|
|
511
|
-
|
|
612
|
+
format: "note",
|
|
613
|
+
body: "reply",
|
|
512
614
|
replyToId: root.id,
|
|
513
615
|
});
|
|
514
616
|
|
|
515
|
-
expect(reply.
|
|
617
|
+
expect(reply.status).toBe("draft");
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it("inherits featured from root post", async () => {
|
|
621
|
+
const root = await postService.create({
|
|
622
|
+
format: "note",
|
|
623
|
+
body: "root",
|
|
624
|
+
featured: true,
|
|
625
|
+
});
|
|
626
|
+
const reply = await postService.create({
|
|
627
|
+
format: "note",
|
|
628
|
+
body: "reply",
|
|
629
|
+
replyToId: root.id,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
expect(reply.featured).toBe(1);
|
|
516
633
|
});
|
|
517
634
|
|
|
518
635
|
it("getThread returns all posts in a thread", async () => {
|
|
519
636
|
const root = await postService.create({
|
|
520
|
-
|
|
521
|
-
|
|
637
|
+
format: "note",
|
|
638
|
+
body: "root",
|
|
522
639
|
});
|
|
523
640
|
await postService.create({
|
|
524
|
-
|
|
525
|
-
|
|
641
|
+
format: "note",
|
|
642
|
+
body: "reply1",
|
|
526
643
|
replyToId: root.id,
|
|
527
644
|
});
|
|
528
645
|
await postService.create({
|
|
529
|
-
|
|
530
|
-
|
|
646
|
+
format: "note",
|
|
647
|
+
body: "reply2",
|
|
531
648
|
replyToId: root.id,
|
|
532
649
|
});
|
|
533
650
|
|
|
534
651
|
const thread = await postService.getThread(root.id);
|
|
535
652
|
expect(thread).toHaveLength(3);
|
|
536
653
|
// Ordered by createdAt
|
|
537
|
-
expect(thread[0]?.
|
|
654
|
+
expect(thread[0]?.body).toBe("root");
|
|
538
655
|
});
|
|
539
656
|
|
|
540
657
|
it("getThread excludes deleted posts", async () => {
|
|
541
658
|
const root = await postService.create({
|
|
542
|
-
|
|
543
|
-
|
|
659
|
+
format: "note",
|
|
660
|
+
body: "root",
|
|
544
661
|
});
|
|
545
662
|
const reply = await postService.create({
|
|
546
|
-
|
|
547
|
-
|
|
663
|
+
format: "note",
|
|
664
|
+
body: "reply",
|
|
548
665
|
replyToId: root.id,
|
|
549
666
|
});
|
|
550
667
|
|
|
@@ -554,23 +671,42 @@ describe("PostService", () => {
|
|
|
554
671
|
expect(thread).toHaveLength(1);
|
|
555
672
|
});
|
|
556
673
|
|
|
557
|
-
it("cascades
|
|
674
|
+
it("cascades status changes from root to thread", async () => {
|
|
675
|
+
const root = await postService.create({
|
|
676
|
+
format: "note",
|
|
677
|
+
body: "root",
|
|
678
|
+
status: "published",
|
|
679
|
+
});
|
|
680
|
+
await postService.create({
|
|
681
|
+
format: "note",
|
|
682
|
+
body: "reply",
|
|
683
|
+
replyToId: root.id,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
await postService.update(root.id, { status: "draft" });
|
|
687
|
+
|
|
688
|
+
const thread = await postService.getThread(root.id);
|
|
689
|
+
for (const post of thread) {
|
|
690
|
+
expect(post.status).toBe("draft");
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it("cascades featured changes from root to thread", async () => {
|
|
558
695
|
const root = await postService.create({
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
visibility: "quiet",
|
|
696
|
+
format: "note",
|
|
697
|
+
body: "root",
|
|
562
698
|
});
|
|
563
699
|
await postService.create({
|
|
564
|
-
|
|
565
|
-
|
|
700
|
+
format: "note",
|
|
701
|
+
body: "reply",
|
|
566
702
|
replyToId: root.id,
|
|
567
703
|
});
|
|
568
704
|
|
|
569
|
-
await postService.update(root.id, {
|
|
705
|
+
await postService.update(root.id, { featured: true });
|
|
570
706
|
|
|
571
707
|
const thread = await postService.getThread(root.id);
|
|
572
708
|
for (const post of thread) {
|
|
573
|
-
expect(post.
|
|
709
|
+
expect(post.featured).toBe(1);
|
|
574
710
|
}
|
|
575
711
|
});
|
|
576
712
|
});
|
|
@@ -583,17 +719,17 @@ describe("PostService", () => {
|
|
|
583
719
|
|
|
584
720
|
it("returns reply counts for posts", async () => {
|
|
585
721
|
const root = await postService.create({
|
|
586
|
-
|
|
587
|
-
|
|
722
|
+
format: "note",
|
|
723
|
+
body: "root",
|
|
588
724
|
});
|
|
589
725
|
await postService.create({
|
|
590
|
-
|
|
591
|
-
|
|
726
|
+
format: "note",
|
|
727
|
+
body: "reply1",
|
|
592
728
|
replyToId: root.id,
|
|
593
729
|
});
|
|
594
730
|
await postService.create({
|
|
595
|
-
|
|
596
|
-
|
|
731
|
+
format: "note",
|
|
732
|
+
body: "reply2",
|
|
597
733
|
replyToId: root.id,
|
|
598
734
|
});
|
|
599
735
|
|
|
@@ -603,8 +739,8 @@ describe("PostService", () => {
|
|
|
603
739
|
|
|
604
740
|
it("returns 0 (missing) for posts without replies", async () => {
|
|
605
741
|
const post = await postService.create({
|
|
606
|
-
|
|
607
|
-
|
|
742
|
+
format: "note",
|
|
743
|
+
body: "no replies",
|
|
608
744
|
});
|
|
609
745
|
|
|
610
746
|
const counts = await postService.getReplyCounts([post.id]);
|
|
@@ -613,17 +749,17 @@ describe("PostService", () => {
|
|
|
613
749
|
|
|
614
750
|
it("excludes deleted replies from count", async () => {
|
|
615
751
|
const root = await postService.create({
|
|
616
|
-
|
|
617
|
-
|
|
752
|
+
format: "note",
|
|
753
|
+
body: "root",
|
|
618
754
|
});
|
|
619
755
|
const reply = await postService.create({
|
|
620
|
-
|
|
621
|
-
|
|
756
|
+
format: "note",
|
|
757
|
+
body: "reply",
|
|
622
758
|
replyToId: root.id,
|
|
623
759
|
});
|
|
624
760
|
await postService.create({
|
|
625
|
-
|
|
626
|
-
|
|
761
|
+
format: "note",
|
|
762
|
+
body: "reply2",
|
|
627
763
|
replyToId: root.id,
|
|
628
764
|
});
|
|
629
765
|
|