@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.
Files changed (178) hide show
  1. package/dist/app.js +23 -5
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -6
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +62 -73
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/theme/components/index.js +0 -2
  47. package/dist/theme/index.js +10 -16
  48. package/dist/theme/layouts/index.js +0 -1
  49. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  50. package/dist/themes/threads/index.js +81 -0
  51. package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
  52. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  53. package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
  54. package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
  55. package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
  56. package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
  57. package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
  58. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  59. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  60. package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
  61. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  62. package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
  63. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  64. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  65. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  66. package/dist/types.js +24 -40
  67. package/package.json +2 -1
  68. package/src/__tests__/helpers/app.ts +4 -0
  69. package/src/__tests__/helpers/db.ts +51 -74
  70. package/src/app.tsx +27 -6
  71. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  72. package/src/db/migrations/meta/_journal.json +7 -0
  73. package/src/db/schema.ts +63 -46
  74. package/src/i18n/locales/en.po +216 -164
  75. package/src/i18n/locales/en.ts +1 -1
  76. package/src/i18n/locales/zh-Hans.po +216 -164
  77. package/src/i18n/locales/zh-Hans.ts +1 -1
  78. package/src/i18n/locales/zh-Hant.po +216 -164
  79. package/src/i18n/locales/zh-Hant.ts +1 -1
  80. package/src/index.ts +30 -15
  81. package/src/lib/__tests__/excerpt.test.ts +125 -0
  82. package/src/lib/__tests__/schemas.test.ts +166 -105
  83. package/src/lib/__tests__/theme-components.test.ts +4 -25
  84. package/src/lib/__tests__/time.test.ts +62 -0
  85. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  86. package/src/lib/__tests__/view.test.ts +217 -67
  87. package/src/lib/constants.ts +1 -4
  88. package/src/lib/excerpt.ts +87 -0
  89. package/src/lib/feed.ts +22 -7
  90. package/src/lib/navigation.ts +6 -7
  91. package/src/lib/render.tsx +1 -1
  92. package/src/lib/schemas.ts +118 -52
  93. package/src/lib/theme-components.ts +10 -13
  94. package/src/lib/time.ts +64 -0
  95. package/src/lib/timeline.ts +170 -0
  96. package/src/lib/view.ts +81 -83
  97. package/src/preset.css +45 -0
  98. package/src/routes/api/__tests__/posts.test.ts +50 -108
  99. package/src/routes/api/__tests__/search.test.ts +2 -3
  100. package/src/routes/api/posts.ts +30 -30
  101. package/src/routes/api/search.ts +4 -4
  102. package/src/routes/api/upload.ts +16 -6
  103. package/src/routes/dash/collections.tsx +18 -40
  104. package/src/routes/dash/index.tsx +2 -2
  105. package/src/routes/dash/navigation.tsx +27 -26
  106. package/src/routes/dash/pages.tsx +45 -60
  107. package/src/routes/dash/posts.tsx +44 -52
  108. package/src/routes/feed/rss.ts +2 -1
  109. package/src/routes/feed/sitemap.ts +14 -4
  110. package/src/routes/pages/archive.tsx +14 -10
  111. package/src/routes/pages/collection.tsx +17 -6
  112. package/src/routes/pages/home.tsx +56 -81
  113. package/src/routes/pages/page.tsx +64 -27
  114. package/src/routes/pages/post.tsx +5 -14
  115. package/src/routes/pages/search.tsx +2 -2
  116. package/src/services/__tests__/collection.test.ts +257 -158
  117. package/src/services/__tests__/media.test.ts +18 -18
  118. package/src/services/__tests__/navigation.test.ts +161 -87
  119. package/src/services/__tests__/post-timeline.test.ts +92 -88
  120. package/src/services/__tests__/post.test.ts +342 -206
  121. package/src/services/__tests__/search.test.ts +19 -25
  122. package/src/services/collection.ts +71 -113
  123. package/src/services/index.ts +9 -8
  124. package/src/services/navigation.ts +38 -71
  125. package/src/services/page.ts +124 -0
  126. package/src/services/post.ts +93 -103
  127. package/src/services/search.ts +38 -27
  128. package/src/styles/components.css +0 -54
  129. package/src/theme/components/MediaGallery.tsx +27 -96
  130. package/src/theme/components/PageForm.tsx +21 -21
  131. package/src/theme/components/PostForm.tsx +122 -118
  132. package/src/theme/components/PostList.tsx +58 -49
  133. package/src/theme/components/ThreadView.tsx +6 -3
  134. package/src/theme/components/TypeBadge.tsx +9 -17
  135. package/src/theme/components/VisibilityBadge.tsx +40 -23
  136. package/src/theme/components/index.ts +0 -13
  137. package/src/theme/index.ts +10 -16
  138. package/src/theme/layouts/index.ts +0 -1
  139. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  140. package/src/themes/threads/index.ts +100 -0
  141. package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
  142. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  143. package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
  144. package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
  145. package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
  146. package/src/themes/threads/pages/SinglePage.tsx +23 -0
  147. package/src/themes/threads/style.css +336 -0
  148. package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
  149. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  150. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  151. package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
  152. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  153. package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
  154. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  155. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  156. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  157. package/src/types.ts +242 -98
  158. package/dist/routes/api/timeline.js +0 -120
  159. package/dist/theme/components/timeline/ArticleCard.js +0 -46
  160. package/dist/theme/components/timeline/ImageCard.js +0 -83
  161. package/dist/theme/components/timeline/NoteCard.js +0 -34
  162. package/dist/theme/components/timeline/QuoteCard.js +0 -48
  163. package/dist/theme/components/timeline/TimelineFeed.js +0 -46
  164. package/dist/theme/components/timeline/index.js +0 -8
  165. package/dist/theme/layouts/SiteLayout.js +0 -131
  166. package/dist/theme/pages/CollectionPage.js +0 -63
  167. package/dist/theme/pages/index.js +0 -11
  168. package/src/routes/api/timeline.tsx +0 -159
  169. package/src/theme/components/timeline/ArticleCard.tsx +0 -45
  170. package/src/theme/components/timeline/ImageCard.tsx +0 -70
  171. package/src/theme/components/timeline/NoteCard.tsx +0 -34
  172. package/src/theme/components/timeline/QuoteCard.tsx +0 -48
  173. package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
  174. package/src/theme/components/timeline/index.ts +0 -8
  175. package/src/theme/layouts/SiteLayout.tsx +0 -132
  176. package/src/theme/pages/CollectionPage.tsx +0 -60
  177. package/src/theme/pages/SinglePage.tsx +0 -24
  178. 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
- type: "note",
20
- content: "Hello world",
19
+ format: "note",
20
+ body: "Hello world",
21
21
  });
22
22
 
23
23
  expect(post.id).toBe(1);
24
- expect(post.type).toBe("note");
25
- expect(post.content).toBe("Hello world");
26
- expect(post.visibility).toBe("quiet"); // default
27
- expect(post.contentHtml).toContain("<p>Hello world</p>");
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
- type: "article",
34
- title: "My Article",
35
- content: "# Introduction\n\nSome content.",
36
- visibility: "featured",
37
- path: "my-article",
38
- sourceUrl: "https://example.com/source",
39
- sourceName: "Example",
40
- });
41
-
42
- expect(post.type).toBe("article");
43
- expect(post.title).toBe("My Article");
44
- expect(post.visibility).toBe("featured");
45
- expect(post.path).toBe("my-article");
46
- expect(post.sourceUrl).toBe("https://example.com/source");
47
- expect(post.sourceName).toBe("Example");
48
- expect(post.sourceDomain).toBe("example.com");
49
- expect(post.contentHtml).toContain("<h1>");
50
- });
51
-
52
- it("renders markdown content to HTML", async () => {
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
- type: "note",
55
- content: "This is **bold** text",
61
+ format: "note",
62
+ body: "This is **bold** text",
56
63
  });
57
64
 
58
- expect(post.contentHtml).toContain("<strong>bold</strong>");
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
- type: "note",
74
- content: "test",
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
- type: "note",
86
- content: "test",
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
- type: "note",
96
- content: "first",
92
+ format: "note",
93
+ body: "first",
97
94
  });
98
95
  const post2 = await postService.create({
99
- type: "note",
100
- content: "second",
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
- type: "note",
111
- content: "test",
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?.content).toBe("test");
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
- type: "note",
128
- content: "test",
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("getByPath", () => {
138
- it("returns a post by path", async () => {
157
+ describe("getBySlug", () => {
158
+ it("returns a post by slug", async () => {
139
159
  await postService.create({
140
- type: "page",
141
- content: "About page",
142
- path: "about",
160
+ format: "note",
161
+ body: "About page",
162
+ slug: "about",
143
163
  });
144
164
 
145
- const found = await postService.getByPath("about");
165
+ const found = await postService.getBySlug("about");
146
166
  expect(found).not.toBeNull();
147
- expect(found?.path).toBe("about");
167
+ expect(found?.slug).toBe("about");
148
168
  });
149
169
 
150
- it("returns null for non-existent path", async () => {
151
- const found = await postService.getByPath("nonexistent");
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
- type: "page",
158
- content: "test",
159
- path: "test-page",
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.getByPath("test-page");
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({ type: "note", content: "first" });
176
- await postService.create({ type: "note", content: "second" });
177
- await postService.create({ type: "note", content: "third" });
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
- type: "note",
186
- content: "old",
205
+ format: "note",
206
+ body: "old",
187
207
  publishedAt: 1000,
188
208
  });
189
209
  await postService.create({
190
- type: "note",
191
- content: "new",
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]?.content).toBe("new");
197
- expect(posts[1]?.content).toBe("old");
216
+ expect(posts[0]?.body).toBe("new");
217
+ expect(posts[1]?.body).toBe("old");
198
218
  });
199
219
 
200
- it("filters by type", async () => {
201
- await postService.create({ type: "note", content: "a note" });
220
+ it("filters by format", async () => {
221
+ await postService.create({ format: "note", body: "a note" });
202
222
  await postService.create({
203
- type: "article",
204
- content: "an article",
205
- title: "Article",
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({ type: "note" });
229
+ const notes = await postService.list({ format: "note" });
209
230
  expect(notes).toHaveLength(1);
210
- expect(notes[0]?.type).toBe("note");
231
+ expect(notes[0]?.format).toBe("note");
211
232
  });
212
233
 
213
- it("filters by single visibility", async () => {
234
+ it("filters by status", async () => {
214
235
  await postService.create({
215
- type: "note",
216
- content: "featured",
217
- visibility: "featured",
236
+ format: "note",
237
+ body: "published post",
238
+ status: "published",
218
239
  });
219
240
  await postService.create({
220
- type: "note",
221
- content: "draft",
222
- visibility: "draft",
241
+ format: "note",
242
+ body: "draft post",
243
+ status: "draft",
223
244
  });
224
245
 
225
- const featured = await postService.list({ visibility: "featured" });
226
- expect(featured).toHaveLength(1);
227
- expect(featured[0]?.visibility).toBe("featured");
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 multiple visibility levels", async () => {
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
- type: "note",
233
- content: "featured",
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
- type: "note",
238
- content: "quiet",
239
- visibility: "quiet",
275
+ format: "note",
276
+ body: "pinned post",
277
+ pinned: true,
240
278
  });
241
279
  await postService.create({
242
- type: "note",
243
- content: "draft",
244
- visibility: "draft",
280
+ format: "note",
281
+ body: "normal post",
245
282
  });
246
283
 
247
- const publicPosts = await postService.list({
248
- visibility: ["featured", "quiet"],
249
- });
250
- expect(publicPosts).toHaveLength(2);
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
- type: "note",
256
- content: "test",
297
+ format: "note",
298
+ body: "test",
257
299
  });
258
- await postService.create({ type: "note", content: "kept" });
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]?.content).toBe("kept");
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
- type: "note",
269
- content: "test",
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({ type: "note", content: `post ${i}` });
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
- type: "note",
292
- content: `post ${i}`,
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
- type: "note",
307
- content: "root post",
348
+ format: "note",
349
+ body: "root post",
308
350
  });
309
351
  await postService.create({
310
- type: "note",
311
- content: "reply",
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]?.content).toBe("root post");
359
+ expect(posts[0]?.body).toBe("root post");
318
360
  });
319
361
  });
320
362
 
321
363
  describe("update", () => {
322
- it("updates post content", async () => {
364
+ it("updates post body", async () => {
323
365
  const post = await postService.create({
324
- type: "note",
325
- content: "original",
366
+ format: "note",
367
+ body: "original",
326
368
  });
327
369
 
328
370
  const updated = await postService.update(post.id, {
329
- content: "updated content",
371
+ body: "updated content",
330
372
  });
331
373
 
332
374
  expect(updated).not.toBeNull();
333
- expect(updated?.content).toBe("updated content");
334
- expect(updated?.contentHtml).toContain("updated content");
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
- type: "article",
340
- content: "body",
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 source URL and extracts domain", async () => {
394
+ it("updates post url", async () => {
352
395
  const post = await postService.create({
353
- type: "link",
354
- content: "link post",
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
- sourceUrl: "https://new-source.com/path",
402
+ url: "https://new-source.com/path",
359
403
  });
360
404
 
361
- expect(updated?.sourceUrl).toBe("https://new-source.com/path");
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 source domain when URL cleared", async () => {
408
+ it("clears url when set to null", async () => {
366
409
  const post = await postService.create({
367
- type: "link",
368
- content: "test",
369
- sourceUrl: "https://example.com",
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
- sourceUrl: null,
416
+ url: null,
374
417
  });
375
418
 
376
- expect(updated?.sourceUrl).toBeNull();
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, { content: "test" });
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
- type: "note",
388
- content: "test",
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
- content: "modified",
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
- type: "note",
407
- content: "test",
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
- type: "note",
426
- content: "root",
527
+ format: "note",
528
+ body: "root",
427
529
  });
428
530
  const reply = await postService.create({
429
- type: "note",
430
- content: "reply",
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
- type: "note",
444
- content: "root",
545
+ format: "note",
546
+ body: "root",
445
547
  });
446
548
  const reply1 = await postService.create({
447
- type: "note",
448
- content: "reply1",
549
+ format: "note",
550
+ body: "reply1",
449
551
  replyToId: root.id,
450
552
  });
451
553
  await postService.create({
452
- type: "note",
453
- content: "reply2",
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
- type: "note",
470
- content: "root",
571
+ format: "note",
572
+ body: "root",
471
573
  });
472
574
  const reply = await postService.create({
473
- type: "note",
474
- content: "reply",
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
- type: "note",
485
- content: "root",
586
+ format: "note",
587
+ body: "root",
486
588
  });
487
589
  const reply1 = await postService.create({
488
- type: "note",
489
- content: "reply1",
590
+ format: "note",
591
+ body: "reply1",
490
592
  replyToId: root.id,
491
593
  });
492
594
  const reply2 = await postService.create({
493
- type: "note",
494
- content: "reply2",
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 visibility from root post", async () => {
605
+ it("inherits status from root post", async () => {
504
606
  const root = await postService.create({
505
- type: "note",
506
- content: "root",
507
- visibility: "featured",
607
+ format: "note",
608
+ body: "root",
609
+ status: "draft",
508
610
  });
509
611
  const reply = await postService.create({
510
- type: "note",
511
- content: "reply",
612
+ format: "note",
613
+ body: "reply",
512
614
  replyToId: root.id,
513
615
  });
514
616
 
515
- expect(reply.visibility).toBe("featured");
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
- type: "note",
521
- content: "root",
637
+ format: "note",
638
+ body: "root",
522
639
  });
523
640
  await postService.create({
524
- type: "note",
525
- content: "reply1",
641
+ format: "note",
642
+ body: "reply1",
526
643
  replyToId: root.id,
527
644
  });
528
645
  await postService.create({
529
- type: "note",
530
- content: "reply2",
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]?.content).toBe("root");
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
- type: "note",
543
- content: "root",
659
+ format: "note",
660
+ body: "root",
544
661
  });
545
662
  const reply = await postService.create({
546
- type: "note",
547
- content: "reply",
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 visibility changes from root to thread", async () => {
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
- type: "note",
560
- content: "root",
561
- visibility: "quiet",
696
+ format: "note",
697
+ body: "root",
562
698
  });
563
699
  await postService.create({
564
- type: "note",
565
- content: "reply",
700
+ format: "note",
701
+ body: "reply",
566
702
  replyToId: root.id,
567
703
  });
568
704
 
569
- await postService.update(root.id, { visibility: "featured" });
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.visibility).toBe("featured");
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
- type: "note",
587
- content: "root",
722
+ format: "note",
723
+ body: "root",
588
724
  });
589
725
  await postService.create({
590
- type: "note",
591
- content: "reply1",
726
+ format: "note",
727
+ body: "reply1",
592
728
  replyToId: root.id,
593
729
  });
594
730
  await postService.create({
595
- type: "note",
596
- content: "reply2",
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
- type: "note",
607
- content: "no replies",
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
- type: "note",
617
- content: "root",
752
+ format: "note",
753
+ body: "root",
618
754
  });
619
755
  const reply = await postService.create({
620
- type: "note",
621
- content: "reply",
756
+ format: "note",
757
+ body: "reply",
622
758
  replyToId: root.id,
623
759
  });
624
760
  await postService.create({
625
- type: "note",
626
- content: "reply2",
761
+ format: "note",
762
+ body: "reply2",
627
763
  replyToId: root.id,
628
764
  });
629
765