@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
|
@@ -22,16 +22,15 @@ describe("Posts API Routes", () => {
|
|
|
22
22
|
app.route("/api/posts", postsApiRoutes);
|
|
23
23
|
|
|
24
24
|
await services.posts.create({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
visibility: "featured",
|
|
25
|
+
format: "note",
|
|
26
|
+
body: "Hello world",
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
const res = await app.request("/api/posts");
|
|
31
30
|
const body = await res.json();
|
|
32
31
|
|
|
33
32
|
expect(body.posts).toHaveLength(1);
|
|
34
|
-
expect(body.posts[0].
|
|
33
|
+
expect(body.posts[0].body).toBe("Hello world");
|
|
35
34
|
expect(body.posts[0].sqid).toBeTruthy();
|
|
36
35
|
});
|
|
37
36
|
|
|
@@ -40,9 +39,8 @@ describe("Posts API Routes", () => {
|
|
|
40
39
|
app.route("/api/posts", postsApiRoutes);
|
|
41
40
|
|
|
42
41
|
const post = await services.posts.create({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
visibility: "featured",
|
|
42
|
+
format: "note",
|
|
43
|
+
body: "with media",
|
|
46
44
|
});
|
|
47
45
|
|
|
48
46
|
const media = await services.media.create({
|
|
@@ -68,26 +66,25 @@ describe("Posts API Routes", () => {
|
|
|
68
66
|
expect(body.posts[0].mediaAttachments[0].position).toBe(0);
|
|
69
67
|
});
|
|
70
68
|
|
|
71
|
-
it("filters by
|
|
69
|
+
it("filters by status", async () => {
|
|
72
70
|
const { app, services } = createTestApp();
|
|
73
71
|
app.route("/api/posts", postsApiRoutes);
|
|
74
72
|
|
|
75
73
|
await services.posts.create({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
visibility: "featured",
|
|
74
|
+
format: "note",
|
|
75
|
+
body: "published post",
|
|
79
76
|
});
|
|
80
77
|
await services.posts.create({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
format: "note",
|
|
79
|
+
body: "draft post",
|
|
80
|
+
status: "draft",
|
|
84
81
|
});
|
|
85
82
|
|
|
86
|
-
const res = await app.request("/api/posts?
|
|
83
|
+
const res = await app.request("/api/posts?status=draft");
|
|
87
84
|
const body = await res.json();
|
|
88
85
|
|
|
89
86
|
expect(body.posts).toHaveLength(1);
|
|
90
|
-
expect(body.posts[0].
|
|
87
|
+
expect(body.posts[0].status).toBe("draft");
|
|
91
88
|
});
|
|
92
89
|
|
|
93
90
|
it("supports limit parameter", async () => {
|
|
@@ -96,9 +93,8 @@ describe("Posts API Routes", () => {
|
|
|
96
93
|
|
|
97
94
|
for (let i = 0; i < 5; i++) {
|
|
98
95
|
await services.posts.create({
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
visibility: "featured",
|
|
96
|
+
format: "note",
|
|
97
|
+
body: `post ${i}`,
|
|
102
98
|
});
|
|
103
99
|
}
|
|
104
100
|
|
|
@@ -116,9 +112,8 @@ describe("Posts API Routes", () => {
|
|
|
116
112
|
app.route("/api/posts", postsApiRoutes);
|
|
117
113
|
|
|
118
114
|
const post = await services.posts.create({
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
visibility: "featured",
|
|
115
|
+
format: "note",
|
|
116
|
+
body: "test post",
|
|
122
117
|
});
|
|
123
118
|
const id = sqid.encode(post.id);
|
|
124
119
|
|
|
@@ -126,7 +121,7 @@ describe("Posts API Routes", () => {
|
|
|
126
121
|
expect(res.status).toBe(200);
|
|
127
122
|
|
|
128
123
|
const body = await res.json();
|
|
129
|
-
expect(body.
|
|
124
|
+
expect(body.body).toBe("test post");
|
|
130
125
|
expect(body.sqid).toBe(id);
|
|
131
126
|
});
|
|
132
127
|
|
|
@@ -135,9 +130,8 @@ describe("Posts API Routes", () => {
|
|
|
135
130
|
app.route("/api/posts", postsApiRoutes);
|
|
136
131
|
|
|
137
132
|
const post = await services.posts.create({
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
visibility: "featured",
|
|
133
|
+
format: "note",
|
|
134
|
+
body: "with media",
|
|
141
135
|
});
|
|
142
136
|
|
|
143
137
|
const media = await services.media.create({
|
|
@@ -183,9 +177,8 @@ describe("Posts API Routes", () => {
|
|
|
183
177
|
method: "POST",
|
|
184
178
|
headers: { "Content-Type": "application/json" },
|
|
185
179
|
body: JSON.stringify({
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
visibility: "quiet",
|
|
180
|
+
format: "note",
|
|
181
|
+
body: "test",
|
|
189
182
|
}),
|
|
190
183
|
});
|
|
191
184
|
|
|
@@ -200,16 +193,15 @@ describe("Posts API Routes", () => {
|
|
|
200
193
|
method: "POST",
|
|
201
194
|
headers: { "Content-Type": "application/json" },
|
|
202
195
|
body: JSON.stringify({
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
visibility: "quiet",
|
|
196
|
+
format: "note",
|
|
197
|
+
body: "Hello from API",
|
|
206
198
|
}),
|
|
207
199
|
});
|
|
208
200
|
|
|
209
201
|
expect(res.status).toBe(201);
|
|
210
202
|
|
|
211
203
|
const body = await res.json();
|
|
212
|
-
expect(body.
|
|
204
|
+
expect(body.body).toBe("Hello from API");
|
|
213
205
|
expect(body.sqid).toBeTruthy();
|
|
214
206
|
expect(body.mediaAttachments).toEqual([]);
|
|
215
207
|
});
|
|
@@ -237,9 +229,8 @@ describe("Posts API Routes", () => {
|
|
|
237
229
|
method: "POST",
|
|
238
230
|
headers: { "Content-Type": "application/json" },
|
|
239
231
|
body: JSON.stringify({
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
visibility: "quiet",
|
|
232
|
+
format: "note",
|
|
233
|
+
body: "with images",
|
|
243
234
|
mediaIds: [m1.id, m2.id],
|
|
244
235
|
}),
|
|
245
236
|
});
|
|
@@ -253,54 +244,6 @@ describe("Posts API Routes", () => {
|
|
|
253
244
|
expect(body.mediaAttachments[1].position).toBe(1);
|
|
254
245
|
});
|
|
255
246
|
|
|
256
|
-
it("returns 400 for image type without media", async () => {
|
|
257
|
-
const { app } = createTestApp({ authenticated: true });
|
|
258
|
-
app.route("/api/posts", postsApiRoutes);
|
|
259
|
-
|
|
260
|
-
const res = await app.request("/api/posts", {
|
|
261
|
-
method: "POST",
|
|
262
|
-
headers: { "Content-Type": "application/json" },
|
|
263
|
-
body: JSON.stringify({
|
|
264
|
-
type: "image",
|
|
265
|
-
content: "should fail",
|
|
266
|
-
visibility: "quiet",
|
|
267
|
-
mediaIds: [],
|
|
268
|
-
}),
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
expect(res.status).toBe(400);
|
|
272
|
-
const body = await res.json();
|
|
273
|
-
expect(body.error).toContain("image posts require at least 1");
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("returns 400 for page type with media", async () => {
|
|
277
|
-
const { app, services } = createTestApp({ authenticated: true });
|
|
278
|
-
app.route("/api/posts", postsApiRoutes);
|
|
279
|
-
|
|
280
|
-
const m1 = await services.media.create({
|
|
281
|
-
filename: "a.jpg",
|
|
282
|
-
originalName: "a.jpg",
|
|
283
|
-
mimeType: "image/jpeg",
|
|
284
|
-
size: 1024,
|
|
285
|
-
storageKey: "media/2025/01/a.jpg",
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const res = await app.request("/api/posts", {
|
|
289
|
-
method: "POST",
|
|
290
|
-
headers: { "Content-Type": "application/json" },
|
|
291
|
-
body: JSON.stringify({
|
|
292
|
-
type: "page",
|
|
293
|
-
content: "test",
|
|
294
|
-
visibility: "quiet",
|
|
295
|
-
mediaIds: [m1.id],
|
|
296
|
-
}),
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
expect(res.status).toBe(400);
|
|
300
|
-
const body = await res.json();
|
|
301
|
-
expect(body.error).toContain("page posts do not allow");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
247
|
it("returns 400 for invalid media IDs", async () => {
|
|
305
248
|
const { app } = createTestApp({ authenticated: true });
|
|
306
249
|
app.route("/api/posts", postsApiRoutes);
|
|
@@ -309,9 +252,8 @@ describe("Posts API Routes", () => {
|
|
|
309
252
|
method: "POST",
|
|
310
253
|
headers: { "Content-Type": "application/json" },
|
|
311
254
|
body: JSON.stringify({
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
visibility: "quiet",
|
|
255
|
+
format: "note",
|
|
256
|
+
body: "test",
|
|
315
257
|
mediaIds: ["nonexistent-id"],
|
|
316
258
|
}),
|
|
317
259
|
});
|
|
@@ -328,7 +270,7 @@ describe("Posts API Routes", () => {
|
|
|
328
270
|
const res = await app.request("/api/posts", {
|
|
329
271
|
method: "POST",
|
|
330
272
|
headers: { "Content-Type": "application/json" },
|
|
331
|
-
body: JSON.stringify({
|
|
273
|
+
body: JSON.stringify({ format: "invalid-type" }),
|
|
332
274
|
});
|
|
333
275
|
|
|
334
276
|
expect(res.status).toBe(400);
|
|
@@ -356,14 +298,14 @@ describe("Posts API Routes", () => {
|
|
|
356
298
|
app.route("/api/posts", postsApiRoutes);
|
|
357
299
|
|
|
358
300
|
const post = await services.posts.create({
|
|
359
|
-
|
|
360
|
-
|
|
301
|
+
format: "note",
|
|
302
|
+
body: "original",
|
|
361
303
|
});
|
|
362
304
|
|
|
363
305
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
364
306
|
method: "PUT",
|
|
365
307
|
headers: { "Content-Type": "application/json" },
|
|
366
|
-
body: JSON.stringify({
|
|
308
|
+
body: JSON.stringify({ body: "updated" }),
|
|
367
309
|
});
|
|
368
310
|
|
|
369
311
|
expect(res.status).toBe(401);
|
|
@@ -374,19 +316,19 @@ describe("Posts API Routes", () => {
|
|
|
374
316
|
app.route("/api/posts", postsApiRoutes);
|
|
375
317
|
|
|
376
318
|
const post = await services.posts.create({
|
|
377
|
-
|
|
378
|
-
|
|
319
|
+
format: "note",
|
|
320
|
+
body: "original",
|
|
379
321
|
});
|
|
380
322
|
|
|
381
323
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
382
324
|
method: "PUT",
|
|
383
325
|
headers: { "Content-Type": "application/json" },
|
|
384
|
-
body: JSON.stringify({
|
|
326
|
+
body: JSON.stringify({ body: "updated" }),
|
|
385
327
|
});
|
|
386
328
|
|
|
387
329
|
expect(res.status).toBe(200);
|
|
388
330
|
const body = await res.json();
|
|
389
|
-
expect(body.
|
|
331
|
+
expect(body.body).toBe("updated");
|
|
390
332
|
expect(body.mediaAttachments).toEqual([]);
|
|
391
333
|
});
|
|
392
334
|
|
|
@@ -395,8 +337,8 @@ describe("Posts API Routes", () => {
|
|
|
395
337
|
app.route("/api/posts", postsApiRoutes);
|
|
396
338
|
|
|
397
339
|
const post = await services.posts.create({
|
|
398
|
-
|
|
399
|
-
|
|
340
|
+
format: "note",
|
|
341
|
+
body: "test",
|
|
400
342
|
});
|
|
401
343
|
|
|
402
344
|
const m1 = await services.media.create({
|
|
@@ -434,8 +376,8 @@ describe("Posts API Routes", () => {
|
|
|
434
376
|
app.route("/api/posts", postsApiRoutes);
|
|
435
377
|
|
|
436
378
|
const post = await services.posts.create({
|
|
437
|
-
|
|
438
|
-
|
|
379
|
+
format: "note",
|
|
380
|
+
body: "test",
|
|
439
381
|
});
|
|
440
382
|
|
|
441
383
|
const m1 = await services.media.create({
|
|
@@ -451,7 +393,7 @@ describe("Posts API Routes", () => {
|
|
|
451
393
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
452
394
|
method: "PUT",
|
|
453
395
|
headers: { "Content-Type": "application/json" },
|
|
454
|
-
body: JSON.stringify({
|
|
396
|
+
body: JSON.stringify({ body: "updated content" }),
|
|
455
397
|
});
|
|
456
398
|
|
|
457
399
|
expect(res.status).toBe(200);
|
|
@@ -467,7 +409,7 @@ describe("Posts API Routes", () => {
|
|
|
467
409
|
const res = await app.request(`/api/posts/${sqid.encode(9999)}`, {
|
|
468
410
|
method: "PUT",
|
|
469
411
|
headers: { "Content-Type": "application/json" },
|
|
470
|
-
body: JSON.stringify({
|
|
412
|
+
body: JSON.stringify({ body: "test" }),
|
|
471
413
|
});
|
|
472
414
|
|
|
473
415
|
expect(res.status).toBe(404);
|
|
@@ -478,14 +420,14 @@ describe("Posts API Routes", () => {
|
|
|
478
420
|
app.route("/api/posts", postsApiRoutes);
|
|
479
421
|
|
|
480
422
|
const post = await services.posts.create({
|
|
481
|
-
|
|
482
|
-
|
|
423
|
+
format: "note",
|
|
424
|
+
body: "test",
|
|
483
425
|
});
|
|
484
426
|
|
|
485
427
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
486
428
|
method: "PUT",
|
|
487
429
|
headers: { "Content-Type": "application/json" },
|
|
488
|
-
body: JSON.stringify({
|
|
430
|
+
body: JSON.stringify({ format: "invalid-type" }),
|
|
489
431
|
});
|
|
490
432
|
|
|
491
433
|
expect(res.status).toBe(400);
|
|
@@ -498,8 +440,8 @@ describe("Posts API Routes", () => {
|
|
|
498
440
|
app.route("/api/posts", postsApiRoutes);
|
|
499
441
|
|
|
500
442
|
const post = await services.posts.create({
|
|
501
|
-
|
|
502
|
-
|
|
443
|
+
format: "note",
|
|
444
|
+
body: "test",
|
|
503
445
|
});
|
|
504
446
|
|
|
505
447
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
@@ -514,8 +456,8 @@ describe("Posts API Routes", () => {
|
|
|
514
456
|
app.route("/api/posts", postsApiRoutes);
|
|
515
457
|
|
|
516
458
|
const post = await services.posts.create({
|
|
517
|
-
|
|
518
|
-
|
|
459
|
+
format: "note",
|
|
460
|
+
body: "to be deleted",
|
|
519
461
|
});
|
|
520
462
|
|
|
521
463
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
@@ -39,9 +39,8 @@ describe("Search API Routes", () => {
|
|
|
39
39
|
app.route("/api/search", searchApiRoutes);
|
|
40
40
|
|
|
41
41
|
await services.posts.create({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
visibility: "featured",
|
|
42
|
+
format: "note",
|
|
43
|
+
body: "Testing search functionality in jant",
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
const res = await app.request("/api/search?q=jant");
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import { settingsApiRoutes } from "../settings.js";
|
|
4
|
+
|
|
5
|
+
describe("Settings API Routes", () => {
|
|
6
|
+
describe("GET /api/settings", () => {
|
|
7
|
+
it("returns 401 when not authenticated", async () => {
|
|
8
|
+
const { app } = createTestApp({ authenticated: false });
|
|
9
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
10
|
+
|
|
11
|
+
const res = await app.request("/api/settings");
|
|
12
|
+
expect(res.status).toBe(401);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns default settings when none are stored", async () => {
|
|
16
|
+
const { app } = createTestApp({ authenticated: true });
|
|
17
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
18
|
+
|
|
19
|
+
const res = await app.request("/api/settings");
|
|
20
|
+
expect(res.status).toBe(200);
|
|
21
|
+
|
|
22
|
+
const body = await res.json();
|
|
23
|
+
expect(body.settings).toBeDefined();
|
|
24
|
+
expect(body.settings.SITE_NAME).toBe("Jant");
|
|
25
|
+
expect(body.settings.SITE_DESCRIPTION).toBe(
|
|
26
|
+
"A microblog powered by Jant",
|
|
27
|
+
);
|
|
28
|
+
expect(body.settings.SITE_LANGUAGE).toBe("en");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns stored settings overriding defaults", async () => {
|
|
32
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
33
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
34
|
+
|
|
35
|
+
await services.settings.set("SITE_NAME" as never, "My Blog");
|
|
36
|
+
|
|
37
|
+
const res = await app.request("/api/settings");
|
|
38
|
+
const body = await res.json();
|
|
39
|
+
|
|
40
|
+
expect(body.settings.SITE_NAME).toBe("My Blog");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not include env-only settings", async () => {
|
|
44
|
+
const { app } = createTestApp({ authenticated: true });
|
|
45
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
46
|
+
|
|
47
|
+
const res = await app.request("/api/settings");
|
|
48
|
+
const body = await res.json();
|
|
49
|
+
|
|
50
|
+
// Env-only keys should not be in the response
|
|
51
|
+
expect(body.settings.AUTH_SECRET).toBeUndefined();
|
|
52
|
+
expect(body.settings.SITE_URL).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("PUT /api/settings", () => {
|
|
57
|
+
it("returns 401 when not authenticated", async () => {
|
|
58
|
+
const { app } = createTestApp({ authenticated: false });
|
|
59
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
60
|
+
|
|
61
|
+
const res = await app.request("/api/settings", {
|
|
62
|
+
method: "PUT",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({ SITE_NAME: "New Name" }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(res.status).toBe(401);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("updates editable settings", async () => {
|
|
71
|
+
const { app } = createTestApp({ authenticated: true });
|
|
72
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
73
|
+
|
|
74
|
+
const res = await app.request("/api/settings", {
|
|
75
|
+
method: "PUT",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify({ SITE_NAME: "Updated Blog" }),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(res.status).toBe(200);
|
|
81
|
+
const body = await res.json();
|
|
82
|
+
expect(body.settings.SITE_NAME).toBe("Updated Blog");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("rejects env-only keys", async () => {
|
|
86
|
+
const { app } = createTestApp({ authenticated: true });
|
|
87
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
88
|
+
|
|
89
|
+
const res = await app.request("/api/settings", {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ AUTH_SECRET: "should-not-work" }),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(res.status).toBe(400);
|
|
96
|
+
const body = await res.json();
|
|
97
|
+
expect(body.rejectedKeys).toContain("AUTH_SECRET");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("partially applies when mixing editable and env-only keys", async () => {
|
|
101
|
+
const { app } = createTestApp({ authenticated: true });
|
|
102
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
103
|
+
|
|
104
|
+
const res = await app.request("/api/settings", {
|
|
105
|
+
method: "PUT",
|
|
106
|
+
headers: { "Content-Type": "application/json" },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
SITE_NAME: "Mixed Update",
|
|
109
|
+
AUTH_SECRET: "ignored",
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
const body = await res.json();
|
|
115
|
+
expect(body.settings.SITE_NAME).toBe("Mixed Update");
|
|
116
|
+
expect(body.rejectedKeys).toContain("AUTH_SECRET");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns 400 for invalid body", async () => {
|
|
120
|
+
const { app } = createTestApp({ authenticated: true });
|
|
121
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
122
|
+
|
|
123
|
+
const res = await app.request("/api/settings", {
|
|
124
|
+
method: "PUT",
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
body: JSON.stringify("not an object"),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(res.status).toBe(400);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import type { Bindings, SortOrder } from "../../types.js";
|
|
7
|
+
import type { AppVariables } from "../../app.js";
|
|
8
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { SORT_ORDERS } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
|
+
|
|
14
|
+
export const collectionsApiRoutes = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
const SortOrderSchema = z.enum(SORT_ORDERS);
|
|
17
|
+
|
|
18
|
+
const CreateCollectionSchema = z.object({
|
|
19
|
+
slug: z.string().min(1),
|
|
20
|
+
title: z.string().min(1),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
icon: z.string().optional(),
|
|
23
|
+
sortOrder: SortOrderSchema.optional(),
|
|
24
|
+
position: z.number().int().min(0).optional(),
|
|
25
|
+
showDivider: z.boolean().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const UpdateCollectionSchema = z.object({
|
|
29
|
+
slug: z.string().min(1).optional(),
|
|
30
|
+
title: z.string().min(1).optional(),
|
|
31
|
+
description: z.string().nullable().optional(),
|
|
32
|
+
icon: z.string().nullable().optional(),
|
|
33
|
+
sortOrder: SortOrderSchema.optional(),
|
|
34
|
+
position: z.number().int().min(0).optional(),
|
|
35
|
+
showDivider: z.boolean().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const ReorderSchema = z.object({
|
|
39
|
+
ids: z.array(z.number().int().positive()),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// List collections (includes post counts)
|
|
43
|
+
collectionsApiRoutes.get("/", async (c) => {
|
|
44
|
+
const collections = await c.var.services.collections.list();
|
|
45
|
+
const postCounts = await c.var.services.collections.getPostCounts();
|
|
46
|
+
|
|
47
|
+
return c.json({
|
|
48
|
+
collections: collections.map((col) => ({
|
|
49
|
+
...col,
|
|
50
|
+
postCount: postCounts.get(col.id) ?? 0,
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Get single collection
|
|
56
|
+
collectionsApiRoutes.get("/:id", async (c) => {
|
|
57
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
58
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
59
|
+
|
|
60
|
+
const collection = await c.var.services.collections.getById(id);
|
|
61
|
+
if (!collection) return c.json({ error: "Not found" }, 404);
|
|
62
|
+
|
|
63
|
+
return c.json(collection);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Reorder collections (requires auth) — must be before /:id
|
|
67
|
+
collectionsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
|
|
68
|
+
const rawBody = await c.req.json();
|
|
69
|
+
|
|
70
|
+
const parseResult = ReorderSchema.safeParse(rawBody);
|
|
71
|
+
if (!parseResult.success) {
|
|
72
|
+
return c.json(
|
|
73
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
74
|
+
400,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await c.var.services.collections.reorder(parseResult.data.ids);
|
|
79
|
+
const collections = await c.var.services.collections.list();
|
|
80
|
+
return c.json({ collections });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Create collection (requires auth)
|
|
84
|
+
collectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
85
|
+
const rawBody = await c.req.json();
|
|
86
|
+
|
|
87
|
+
const parseResult = CreateCollectionSchema.safeParse(rawBody);
|
|
88
|
+
if (!parseResult.success) {
|
|
89
|
+
return c.json(
|
|
90
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
91
|
+
400,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const body = parseResult.data;
|
|
96
|
+
|
|
97
|
+
const collection = await c.var.services.collections.create({
|
|
98
|
+
slug: body.slug,
|
|
99
|
+
title: body.title,
|
|
100
|
+
description: body.description,
|
|
101
|
+
icon: body.icon,
|
|
102
|
+
sortOrder: body.sortOrder as SortOrder | undefined,
|
|
103
|
+
position: body.position,
|
|
104
|
+
showDivider: body.showDivider,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return c.json(collection, 201);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Update collection (requires auth)
|
|
111
|
+
collectionsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
112
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
113
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
114
|
+
|
|
115
|
+
const rawBody = await c.req.json();
|
|
116
|
+
|
|
117
|
+
const parseResult = UpdateCollectionSchema.safeParse(rawBody);
|
|
118
|
+
if (!parseResult.success) {
|
|
119
|
+
return c.json(
|
|
120
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
121
|
+
400,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const collection = await c.var.services.collections.update(
|
|
126
|
+
id,
|
|
127
|
+
parseResult.data,
|
|
128
|
+
);
|
|
129
|
+
if (!collection) return c.json({ error: "Not found" }, 404);
|
|
130
|
+
|
|
131
|
+
return c.json(collection);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Delete collection (requires auth)
|
|
135
|
+
collectionsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
136
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
137
|
+
if (isNaN(id)) return c.json({ error: "Invalid ID" }, 400);
|
|
138
|
+
|
|
139
|
+
const success = await c.var.services.collections.delete(id);
|
|
140
|
+
if (!success) return c.json({ error: "Not found" }, 404);
|
|
141
|
+
|
|
142
|
+
return c.json({ success: true });
|
|
143
|
+
});
|